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
28 changes: 28 additions & 0 deletions harness/context/05-demo-saved-run-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
# Saved run readiness — actual acceptance

Base `9e630b8b2fe5720e5848d01cbf44476aecfe5b06`. PR #7 introduced the verified local candidate
workflow; both PR jobs passed. Its subsequent main run `33902484660` passed all 82 unit/process
checks but failed the saved-run browser scenario on Node 24. The original log is preserved in
ignored `output/main-9e630b8-ci-failure.log`.

Diagnosis: the UI presented a clickable saved-run link while `active` or `pendingControl` still
caused its handler to ignore clicks. The server could also return old active state with refreshed
terminal content. No evidence corruption or false differential was observed.

The new browser regression holds a real start response after backend completion. Before the fix
it failed because the saved link lacked its disabled state. After the fix, the link is disabled
and a forced click sends no saved-run request; releasing the start response enables it. Selection
then shows the saved result and survives a subsequent poll.

Actual checks:

- Focused regression before patch: failed, `aria-disabled` expected `true`, actual missing.
- `node node_modules/@playwright/test/cli.js test tests/playwright/demo.spec.ts`: 4/4 passed after
patch, including 390/900/1440 geometry, font/asset loading, keyboard and reduced-motion state.
- Ordinary-user pnpm 11.19.0 `pnpm check`: **82/82 unit/process + 5/5 browser passed**. Historical
evidence and recorded exports passed without changes. Log: `output/saved-run-readiness-check.log`.
- Parent brand and workspace validation: both passed.
- Independent read-only review: no remaining submission blocker.

No CSS, canonical assets, header geometry, original experiments or verifier contract changed.
The fix preserves source and product Gate separation; `SPIKE_CONDITIONAL` remains unchanged.
25 changes: 25 additions & 0 deletions plans/05-demo-saved-run-readiness.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Saved run readiness

Observed problem: main `9e630b8` CI run `33902484660`, Node 24 browser test at
`tests/playwright/demo.spec.ts:104`, displayed `Startup failed` after clicking its retained run.
All 82 unit/process tests passed; Node 22 passed. The failure log is preserved in local ignored
`output/main-9e630b8-ci-failure.log`.

The saved link is rendered as enabled while its click handler silently rejects active/pending
controls. Final verification appears before asynchronous final publication finishes; a state
response also snapshots `active` before awaiting refresh. This is an availability mismatch.

Sole writer on `codex/demo-saved-run-race`, clean base `9e630b8`; allowed paths are this plan,
matching phase context, Demo app.js/server.ts and focused browser regression. Canonical assets,
geometry, historical evidence, new verifier and the separate pre-only DrawDB experiment are excluded.

Add a deterministic browser test holding the start response while the backend finishes, proving
saved controls remain disabled until available. Then synchronize their accessible disabled state
with the existing guard, and read active after asynchronous state refresh. Re-run browser/resource/
390/900/1440 header checks, full required checks and parent brand checks; independent review,
commit/PR/CI/merge, sync D main. Do not hide this failure with a retry or a longer assertion timeout.
No product Gate changes. Roll back only this focused patch if it breaks live controls.

Completed locally: deterministic regression failed before the patch and passed after it; all
82 unit/process and 5 browser checks passed, parent brand/workspace checks passed and independent
review found no remaining blocker. Exact observations are in the matching phase context.
22 changes: 17 additions & 5 deletions src/demo/public/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,20 @@ async function control(action) {
return value;
}

function updateControls() {
const unavailable = active || pendingControl;
byId("start").disabled = unavailable;
byId("cancel").disabled = !active;
byId("check").disabled = unavailable;
for (const link of byId("retained-runs").querySelectorAll("a")) {
link.setAttribute("aria-disabled", String(unavailable));
link.tabIndex = unavailable ? -1 : 0;
link.title = unavailable
? "Available when the active operation finishes"
: "Inspect saved execution";
}
}

function display(current, stored = false) {
if (!current) return;
const { run, attempts, verification } = current;
Expand Down Expand Up @@ -110,9 +124,6 @@ async function refresh() {
active = state.active;
byId("pre-revision").textContent = state.case.revisions["pre-fix"];
byId("post-revision").textContent = state.case.revisions["post-fix"];
byId("start").disabled = state.active || pendingControl;
byId("cancel").disabled = !state.active;
byId("check").disabled = state.active;
if (!showingStored) display(state.current);
byId("history-verification").textContent = state.historical.verification.ok
? "Stored bundle: consistency and frozen inputs verified"
Expand Down Expand Up @@ -145,6 +156,7 @@ async function refresh() {
}),
);
}
updateControls();
} catch (error) {
byId("phase").textContent =
`${error.message}. Run state is unknown; success has not been established.`;
Expand All @@ -155,17 +167,17 @@ async function refresh() {

byId("start").addEventListener("click", async () => {
pendingControl = true;
byId("start").disabled = true;
updateControls();
showingStored = false;
viewGeneration += 1;
try {
await control("start");
await refresh();
} catch (error) {
byId("phase").textContent = error.message;
byId("start").disabled = false;
} finally {
pendingControl = false;
updateControls();
}
});
byId("cancel").addEventListener("click", async () => {
Expand Down
3 changes: 2 additions & 1 deletion src/demo/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,13 @@ export async function startDemo(options: {
} catch (error) {
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
}
const current = await runner.refresh();
send(200, {
case: CASE,
stage: "SPIKE_CONDITIONAL",
historical,
active: runner.active,
current: await runner.refresh(),
current,
runs,
timeoutMs: runner.config.timeoutMs,
});
Expand Down
36 changes: 36 additions & 0 deletions tests/playwright/demo.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,42 @@ test("saved startup failure survives header home activation without navigation o
}
});

test("saved run controls reflect a start request still pending after execution finishes", async ({
page,
}) => {
const release = Promise.withResolvers<void>();
const received = Promise.withResolvers<void>();
await page.route("**/api/start", async (route) => {
const response = await route.fetch();
received.resolve();
await release.promise;
await route.fulfill({ response });
});
try {
await page.goto(demo.address);
await page.getByRole("button", { name: "Run 20 + 20 checks" }).click();
await received.promise;
await expect(page.locator("#state-badge")).toHaveText("Startup failed");
const id = (await page.locator("#run-id").innerText()).replace("output/demo/", "");
const saved = page.locator("#retained-runs").getByText(id, { exact: true });
await expect(saved).toHaveAttribute("aria-disabled", "true");
let savedRequests = 0;
page.on("request", (request) => {
if (request.url().includes(`/api/run/${id}`)) savedRequests++;
});
await saved.dispatchEvent("click");
expect(savedRequests).toBe(0);
release.resolve();
await expect(saved).toHaveAttribute("aria-disabled", "false");
await saved.click();
await expect(page.locator("#state-badge")).toHaveText("Saved run · Startup failed");
await page.waitForResponse((response) => response.url() === `${demo.address}/api/state`);
await expect(page.locator("#state-badge")).toHaveText("Saved run · Startup failed");
} finally {
release.resolve();
}
});

test("concurrent controls admit exactly one configured run and reject foreign control requests", async ({
page,
}) => {
Expand Down