From 6868af676ed43819acfdf512c018107aeefde6c2 Mon Sep 17 00:00:00 2001 From: AlbertXXuu Date: Sat, 5 Sep 2026 01:57:26 +0800 Subject: [PATCH] fix: synchronize saved run controls with archiving readiness --- .../context/05-demo-saved-run-readiness.md | 28 +++++++++++++++ plans/05-demo-saved-run-readiness.md | 25 +++++++++++++ src/demo/public/app.js | 22 +++++++++--- src/demo/server.ts | 3 +- tests/playwright/demo.spec.ts | 36 +++++++++++++++++++ 5 files changed, 108 insertions(+), 6 deletions(-) create mode 100644 harness/context/05-demo-saved-run-readiness.md create mode 100644 plans/05-demo-saved-run-readiness.md diff --git a/harness/context/05-demo-saved-run-readiness.md b/harness/context/05-demo-saved-run-readiness.md new file mode 100644 index 0000000..b03a8f1 --- /dev/null +++ b/harness/context/05-demo-saved-run-readiness.md @@ -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. diff --git a/plans/05-demo-saved-run-readiness.md b/plans/05-demo-saved-run-readiness.md new file mode 100644 index 0000000..ea33279 --- /dev/null +++ b/plans/05-demo-saved-run-readiness.md @@ -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. diff --git a/src/demo/public/app.js b/src/demo/public/app.js index c94bcdf..f981754 100644 --- a/src/demo/public/app.js +++ b/src/demo/public/app.js @@ -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; @@ -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" @@ -145,6 +156,7 @@ async function refresh() { }), ); } + updateControls(); } catch (error) { byId("phase").textContent = `${error.message}. Run state is unknown; success has not been established.`; @@ -155,7 +167,7 @@ async function refresh() { byId("start").addEventListener("click", async () => { pendingControl = true; - byId("start").disabled = true; + updateControls(); showingStored = false; viewGeneration += 1; try { @@ -163,9 +175,9 @@ byId("start").addEventListener("click", async () => { await refresh(); } catch (error) { byId("phase").textContent = error.message; - byId("start").disabled = false; } finally { pendingControl = false; + updateControls(); } }); byId("cancel").addEventListener("click", async () => { diff --git a/src/demo/server.ts b/src/demo/server.ts index 9ff8958..274961a 100644 --- a/src/demo/server.ts +++ b/src/demo/server.ts @@ -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, }); diff --git a/tests/playwright/demo.spec.ts b/tests/playwright/demo.spec.ts index 57bc114..c71ca58 100644 --- a/tests/playwright/demo.spec.ts +++ b/tests/playwright/demo.spec.ts @@ -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(); + const received = Promise.withResolvers(); + 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, }) => {