Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions packages/workflows/src/authoring/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ import { type } from "arktype";
import { PackageJSON } from "@intx/types/package-json";

import { WorkflowAuthorError } from "./errors";
import { PACKAGE_JSON_PATH, validateWorkflowSourceTree } from "./source-tree";
import {
normalizeEntryPath,
PACKAGE_JSON_PATH,
validateWorkflowSourceTree,
} from "./source-tree";

const WORKFLOW_ASSET_KIND = "workflow";
const HUB_PRINCIPAL = { kind: "hub" } as const;
Expand Down Expand Up @@ -486,7 +490,12 @@ export function createWorkflowAuthorRegistry(
}
const files: Record<string, string> = {};
await collectTree(reads, "", files);
if (!(input.entry in files)) {
// Normalized the same way `validateWorkflowSourceTree` normalizes an
// author-time `interchange.workflow` entry: the tree's keys carry no
// leading `./`, but a caller (this same test suite included) may
// still pass the entry as written in `package.json`.
const entry = normalizeEntryPath(input.entry);
if (!(entry in files)) {
throw new WorkflowAuthorError(
"invalid",
`entry ${JSON.stringify(input.entry)} names no file in commit ${input.commitSha}`,
Expand Down Expand Up @@ -516,13 +525,13 @@ export function createWorkflowAuthorRegistry(
);
}
const packageName = manifest.name;
const entrySource = files[input.entry] ?? "";
const entrySource = files[entry] ?? "";
const inertLiteral = tryReadInertDefaultExport(entrySource);
const toolPackagePins = extractToolPackagePins(inertLiteral);

return {
commitSha: input.commitSha,
entry: input.entry,
entry,
files: Object.keys(files),
toolPackagePins,
packageName,
Expand Down
111 changes: 111 additions & 0 deletions scripts/e2e/browser/walkthrough.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1029,6 +1029,117 @@ async function run(): Promise<void> {
};
},
);

// --- Step 11 (CL-7366): the routines page's target picker must offer
// at least one real option (CL-7351 target discovery) and creation
// must stay blocked until one is picked (CL-7355) — the panel has no
// Save button, it autosaves on blur once a target is chosen.
await step(
() => page,
"11-routine-target-picker-blocks-until-picked",
async () => {
await page.goto(`${webBaseUrl}/routines`, {
waitUntil: "domcontentloaded",
timeout: 20_000,
});
await page.evaluate(() => {
const button = Array.from(document.querySelectorAll("button")).find(
(b) => (b.textContent ?? "").includes("New routine"),
);
(button as HTMLButtonElement | undefined)?.click();
});
await page.waitForSelector("#routine-panel-target", {
timeout: 15_000,
});
const optionCount = await countMatching(
page,
"#routine-panel-target option[value]:not([value=''])",
);
if (optionCount < 1) {
return {
status: "fail",
detail: `target picker rendered ${optionCount} options; expected >= 1`,
};
}

const name = `Walkthrough routine ${Date.now()}`;
await page.type("#routine-panel-name", name);
await page.keyboard.press("Tab"); // blur with no target picked yet
// A fixed grace period, not a race against the positive wait below:
// an asymmetric short/long timeout pair invites a flaky false pass
// (this negative wait could elapse with no autosave firing purely
// because it's short, even if the blocked-save bug exists). Poll
// for the same 15s window as the positive assertion, and read
// `role="status"` inside the panel — not `document.body` — so a
// stale "Saved" toast elsewhere on the page can't false-positive.
await Bun.sleep(1_500);
const savedBeforePick = await page
.waitForFunction(
() =>
document
.querySelector(".shell-routine-pane")
?.querySelector('[role="status"]')
?.textContent?.includes("Saved") ?? false,
{ timeout: 13_500 },
)
.then(() => true)
.catch(() => false);

await page.evaluate(() => {
const select = document.querySelector<HTMLSelectElement>(
"#routine-panel-target",
);
const option = select?.querySelector<HTMLOptionElement>(
"option[value]:not([value=''])",
);
if (select && option) {
select.value = option.value;
select.dispatchEvent(new Event("change", { bubbles: true }));
}
});
await page.type("#routine-panel-name", " picked");
await page.keyboard.press("Tab");
const saved = await page
.waitForFunction(
() =>
document
.querySelector(".shell-routine-pane")
?.querySelector('[role="status"]')
?.textContent?.includes("Saved") ?? false,
{ timeout: 15_000 },
)
.then(() => true)
.catch(() => false);
if (savedBeforePick || !saved) {
return {
status: "fail",
detail: `savedBeforePick=${String(savedBeforePick)} saved=${String(saved)} — expected blocked-then-created`,
};
}

await page.goto(`${webBaseUrl}/routines`, {
waitUntil: "domcontentloaded",
timeout: 20_000,
});
const rowAppeared = await page
.waitForFunction(
(needle: string) => document.body.textContent?.includes(needle),
{ timeout: 15_000 },
`${name} picked`,
)
.then(() => true)
.catch(() => false);
return rowAppeared
? {
status: "pass",
detail: `target picker offered ${optionCount} option(s); creation stayed blocked until one was picked, then autosaved and now lists in /routines`,
}
: {
status: "fail",
detail: `"${name} picked" never appeared in the routines list after saving`,
};
},
);
} finally {
for (const cleanup of cleanups.splice(0).reverse()) {
await cleanup().catch((error) => {
Expand Down
Loading
Loading