diff --git a/.changes/planning/perf-8757-journey-apply-cached-module-4c1e9a7d.md b/.changes/planning/perf-8757-journey-apply-cached-module-4c1e9a7d.md new file mode 100644 index 00000000000..96cc88cc74c --- /dev/null +++ b/.changes/planning/perf-8757-journey-apply-cached-module-4c1e9a7d.md @@ -0,0 +1,7 @@ +--- +bump: none +--- + +### Changed + +- surface-journey test: the shell's applies and handles between page phases run `round` as a module, which reuses its cached bytecode instead of recompiling `round.py` on each call, and the pause before the last Notes reply waits only for the next whole second. diff --git a/plugins/planning/surface/surface-checks.sh b/plugins/planning/surface/surface-checks.sh index 9c1b23f13c8..69381b88b9f 100755 --- a/plugins/planning/surface/surface-checks.sh +++ b/plugins/planning/surface/surface-checks.sh @@ -363,14 +363,16 @@ print(json.dumps({"live": len(live), "answered": sum(r[1] == "answered" for r in for n in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do sed "s/__PORT__/$jport/; s/__PHASE__/$n/" tests/ui_journey.js >"$tmp/uj$n.js" done + # The repeated writes run round as a module from this directory: Python then reuses its cached + # bytecode, where round.sh runs round.py as a script that recompiles all of it on every call. jhandle() { local s read -r -a s <<<"$(unhandled "$j")" - [[ "${#s[@]}" -eq 0 ]] || jtime handle bash "$here/round.sh" --dir "$j" handle --seq "${s[@]}" >/dev/null + [[ "${#s[@]}" -eq 0 ]] || jtime handle "$py" -m round --dir "$j" handle --seq "${s[@]}" >/dev/null } japply() { # name ops-json printf '%s' "$2" >"$j/ops/$1.json" - jtime apply bash "$here/round.sh" --dir "$j" apply --file "$j/ops/$1.json" >/dev/null || bad "journey: ops $1 refused" + jtime apply "$py" -m round --dir "$j" apply --file "$j/ops/$1.json" >/dev/null || bad "journey: ops $1 refused" } jrun() { run_code "$tmp/uj$1.js" "$tmp/uj$1.out"; } jrun 1 @@ -460,8 +462,12 @@ print(json.dumps({"live": len(live), "answered": sum(r[1] == "answered" for r in "constraints": "Builds stop at ten minutes and share one cache."}}]}' # A Notes reply after the restatement: the restate notice outranks it in phase 16. japply n2 '{"ops": [{"op": "note-reply", "needsAnswer": true, "text": "Anything else before the Brief?"}]}' - # An ordinary Claude note after it must not unpin the question. - jtime sleep sleep 1 + # An ordinary Claude note after it must not unpin the question. Notes carry whole-second times, + # so waiting for the next second to start gives it a later time than n2, as a full second would. + local now=${EPOCHREALTIME:-} us wait=1 # bash 4 has no EPOCHREALTIME: keep the full second + us=${now#*.} + [[ "$us" =~ ^[0-9]{6}$ && "$us" != 000000 ]] && wait=$(printf '0.%06d' "$((1000000 - 10#$us))") + jtime sleep sleep "$wait" japply n3 '{"ops": [{"op": "note-reply", "text": "The restatement is posted."}]}' jrun 16 # Phase 17 confirms the third restatement. The shell then posts a status and the finish op, diff --git a/plugins/planning/surface/tests/ui_journey.js b/plugins/planning/surface/tests/ui_journey.js index e616592f8be..07b7ff06b78 100644 --- a/plugins/planning/surface/tests/ui_journey.js +++ b/plugins/planning/surface/tests/ui_journey.js @@ -190,6 +190,8 @@ async page => { // the user journey in order on one page, no reload after phase const h2 = await lineIs(status.text) ? await head() : {}; ok("a status set after the newest unhandled event is not dimmed", h2.line === "Checked the cache key" && h2.old === false && h2.work === "Claude is working on Q99, Q1", JSON.stringify(h2)); status = null; + // h2 came from the 3 s poll; a visible tab's visibilitychange reads /api/state at once, so h3 need not wait for the next poll + await tab.evaluate(() => document.dispatchEvent(new Event("visibilitychange"))); await tab.waitForFunction(() => document.getElementById("claudeLine").hidden || document.getElementById("clText").textContent !== "Checked the cache key", null, {timeout: 8000}).catch(() => {}); const h3 = await head(); ok("clearing the status leaves the working line unchanged", h3.line !== "Checked the cache key" && h3.work === "Claude is working on Q99, Q1", JSON.stringify(h3)); @@ -713,10 +715,14 @@ async page => { // the user journey in order on one page, no reload after phase await page.request.get((await page.evaluate(() => location.origin)) + "/api/wait?after=0&timeout=1", {headers: {"X-Interview-Token": await token()}}); // delivers it: a wake is in flight await until(() => /^Claude is working on /.test(document.getElementById("workLine").textContent), 5000); // the page has seen the delivery, the state queued() reads ok("the first note posted", sent.ok(), sent.status()); - await page.click("#title"); await page.keyboard.press("l"); await page.click("#flyClose", {timeout: 2000}).catch(() => {}); + // l toggles Activity, left open since phase 10: close it only when this press opened it + await page.click("#title"); await page.keyboard.press("l"); if (await page.$eval("#fly", el => el.classList.contains("open"))) await page.click("#flyClose"); await page.evaluate(() => document.querySelector('[data-panel="notes"]').click()); await settle(300); + // The next state frame re-renders the Notes panel, emptying the toast, so record every text it shows. + await page.evaluate(() => { const seen = window.__noteToasts = []; const mo = window.__noteToastObserver = new MutationObserver(() => { const t = (document.getElementById("noteToast") || {}).textContent; if (t && seen[seen.length - 1] !== t) seen.push(t); }); mo.observe(document.body, {subtree: true, childList: true, characterData: true}); }); await page.fill("#noteText", "second note"); await page.click("[data-sendnote]"); - ok("a save while a wake is in flight says queued, Claude is busy", await until(() => /Queued, Claude is busy/.test((document.getElementById("noteToast") || {}).textContent || ""), 4000), await text("#noteToast")); + ok("a save while a wake is in flight says queued, Claude is busy", await until(() => window.__noteToasts.some(t => /Queued, Claude is busy/.test(t)), 4000), JSON.stringify(await page.evaluate(() => window.__noteToasts))); + await page.evaluate(() => window.__noteToastObserver.disconnect()); // the page lives on through phase 20 await page.click("#flyClose", {timeout: 2000}).catch(() => {}); } if (PHASE === 17) { // after the wrap-up the user confirms the third restatement; the shell has written nothing since phase 16