pcbjam/tests/web/symbol-write-remote.spec.ts

124 lines
6 KiB
TypeScript
Raw Normal View History

test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
import { test, expect } from '@playwright/test';
import { waitForWxApp, focusCanvas, clickByTooltip, stableShot } from '../e2e/utils/element-tracker';
// Mirrors @pcbjam/shared USER_HEADER (tests/ doesn't depend on the shared pkg).
const USER_HEADER = 'x-pcbjam-user';
// The reference backend serves its single project/libs under any scope.
const SCOPE = 'default';
/**
* 0004-D: the FULL remote write round-trip no in-memory spike. Boot ensures a
* "My Symbols" user lib via the backend's createLib; New Symbol Ctrl+S routes
* the save through window.kicadLibs.request("save") remoteLibsSource.saveItemBody
* PUT /api/libs/:lib/items/... on the example backend. Verify the backend
* persisted a valid fork-native body, then reload and confirm it enumerates.
*
* Requires the example backend (:3060) running with USER_LIBS_DIR set.
*/
const BACKEND = process.env.BACKEND_URL ?? 'http://localhost:3060';
e2e/CI: dual-engine suites, per-engine screenshots, SwiftShader retired, prod web suite, CI-coverage gate Squash of experiment/ff-big-modules vs main. Big-module routing removed: native-EH shrank kicad_editor below SpiderMonkey's x86-64 code budget (runs 29355049705/29356152413 green on stock Firefox), so BIG_MODULE_SPECS routing and the baseline-only-JIT crutch are gone — kicad-firefox and kicad-chromium both run the full suite, with the module compiled the way real users' browsers compile it. Per-engine screenshots end to end: stableShot/shotPath write test-results/<engine>/<name>.png; baselines move to baseline-screenshots/{chromium,firefox}/ and the whole tools/screenshots pipeline (compare/promote/manifest/spec-map/changelog/Discord) keys on <engine>/<name>. Previously Firefox and Chromium renders of one spec overwrote each other and Firefox renders were never actually gated. Seeded from CI run 29421380806 (92 new firefox baselines, +24 chromium web-suite shots); manifest generated from the baseline tree. One merged playwright.config.ts (kicad/asyncify/coroutine/perf as projects); ~25 dead npm scripts dropped. The web suite is gated in CI for the first time ever (4 rotted specs fixed, 5 broken lib-bridge specs triaged as fixme in docs/features/web-e2e-rot/); cheap lint step after npm ci; last 26 blind-sleep violations fixed. SwiftShader retired: CI Chromium renders WebGL on ANGLE → Mesa llvmpipe (--use-gl=angle --use-angle=gl --ignore-gpu-blocklist; the blocklist flag is mandatory — llvmpipe is blocklisted and WebGL is silently unavailable without it) in BOTH configs. Under WORKERS=4 congestion SwiftShader transiently failed the first post-board-load draw and the recovery cascade ended in a silent permanent Cairo fallback — that engine flip was the "~1.2% changedRatio both directions" occ-export baseline flake. Validated 160/160 across two 80-repeat rigs; full analysis in docs/features/wx-parity-bugs/occ-export-context-eviction.md. Chromium baselines shift slightly on llvmpipe — promote once from the first green run. Deflakes the new coverage exposed: presence baselines settle before capture; presence fixtures declare current file formats; perf gets its own outputDir so CI evidence survives; occ-export settles the board paint before the export dialog; menu-item waits (waitForRenderedByLabel before clickMenuItem) in 4 specs + the TESTING.md rule. Web suite runs the PROD build, in parallel: webServer becomes backend `start` + the standalone's e2e:preview (build-preview.mjs: link-wasm → stash the public/wasm symlink aside during vite build, build-demo.mjs's move — then vite preview as the persistent server). The wasm middleware serves /wasm/* in preview and emits COOP/COEP/CORP itself (a pthread worker script's own response must carry COEP or Chrome kills it with ERR_BLOCKED_BY_RESPONSE). VITE_* flags bake at build time; VITE_ALLOW_USER_OVERRIDE joins turbo globalEnv. fullyParallel + default workers: 5.2m → 1.4m. Determinism fixes the parallel run exposed: shared-page specs become serial groups; locks.spec grabs alice's exact item via the new kicadCollabTestSelectByUuid hook (cross-tab "first footprint" order is not a ysync invariant); quit specs poll page.url() (quit supersedes its own navigation — NS_BINDING_ABORTED on Firefox). Suite: 51 passed / 12 skipped / 0 failed in 1.6m. CI-coverage gate (lint:ci-coverage): every tests/**/*.spec.ts must be reachable from the npm scripts the workflows invoke — scraped from .github/workflows/, resolved through package.json, coverage asked from playwright --list itself. Rules: uncovered-spec + orphan-project (with a documented LOCAL_ONLY_PROJECTS allowlist). Gating next to lint:determinism; 138 spec files / 13 projects accounted for. Product fixes kept from the investigations (reachable on real GPUs too): wx 7799fd1be5 — paint flags clear before dispatch + Invalidate always propagates; kicad 3dcfea5e45 — SwiftShader pass-boundary flush + per-instance font texture + first-frame GL-error drain (GAL recovery recovers instead of falling back to Cairo) + the user-facing eeschema switch navigates again under __EMSCRIPTEN__ (project-sync's FaceRegistered gate had rerouted it into the hidden sync player; caught by the newly-gated web suite). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_018eUxiPApHgGiu9NFyQfhAq
2026-07-17 12:10:40 +02:00
// KNOWN-BROKEN: the fp/sym editor kicadLibs enumerate/save flows are dead
// (docs/features/web-e2e-rot/01-editor-lib-bridge-flows.md). fixme, not fail:
// on CI this dies in a 180s boot timeout per engine — running it buys no signal.
tests: un-skip sweep — 26 tests revived on the JSPI build, failures re-gated with fresh evidence Empirical pass over every skip/fixme whose premise the JSPI migration could have changed. Revived (verified green): - Firefox wasm-budget guards RETIRED (drift-trio, drift-trio-fuzz, drift-trio-scenarios, ysync-two-tab, ysync-libsymbols): the JSPI build (~half the asyncify size) fits three editor tabs inside Firefox 153's per-process budget — +20 firefox collab tests. - roundtrip 'pcbnew preserves items through a yjs round trip': the asyncify-fragile envelope parse it waited on is gone — both engines. - drift-trio-scenarios S4/S4b: converge now (was KNOWN ~5-8%). - pcbnew-collab + eeschema-collab 'a local move propagates A→B'. - web eeschema-fp-selector, read-only-editor's fixme'd writer-stream test, footprint-browse-remote read path (chromium; firefox gated: FootprintEnumerate rows never appear in 60s — slow wasm tier suspected). Still broken, re-gated with re-verified reasons: - 3d-viewer raytracer engine toggle: still inert, both engines. - maximize display geometry: wxDisplay reports 0x0 in the harness. - web editor WRITE bridge (symbol/footprint × remote/spike): wedges at the New Symbol/Footprint dialog step on both engines — the web-e2e-rot 01 gap stands for writes. Verify runs: kicad+wx touched files 93 passed / 0 failed / 7 skipped (intended gates); web touched files 7 passed / 0 failed / 9 skipped. lint:determinism + lint:ci-coverage green. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 18:43:36 +02:00
// Probed 2026-08-13 on the JSPI build: still dead — wedges at the New Symbol/
// Footprint dialog step on BOTH engines (web-e2e-rot 01 stands for writes).
test.fixme(
'symbol editor save persists to the backend (remote write round-trip)', async ({ page }) => {
// Unique owner per run so the test is isolated from prior runs.
const owner = `e2e-${Date.now()}`;
const logs: string[] = [];
page.on('console', (m) => logs.push(`[${m.type()}] ${m.text()}`));
page.on('pageerror', (e) => logs.push(`[pageerror] ${e.message}`));
await page.goto(`/default/projects/demo/-/symbol_editor?libowner=${owner}`);
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await waitForWxApp(page, { timeout: 150000 });
await page.waitForFunction(
() => !!window.wxElementRegistry && window.wxElementRegistry.findAll({}).length > 5,
null,
{ timeout: 150000 },
);
await page.waitForFunction(() => !!(window as any).kicadLibs, null, { timeout: 60000 });
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await stableShot(page, 'symremote-01-boot.png');
// Boot's ensure-user-lib created "My Symbols" (slug my-symbols) for this owner.
const ownerHeaders = { [USER_HEADER]: owner };
const libsRes = await fetch(`${BACKEND}/api/scopes/${SCOPE}/libs`, { headers: ownerHeaders });
const libsBody = (await libsRes.json()) as { id: string; type: string }[];
expect(libsBody.some((l) => l.type === 'user' && l.id === 'my-symbols'), 'user lib created at boot').toBe(true);
// Select the (only) library row in the tree, then New Symbol.
const hdr = await page.evaluate(() => {
const rd = window.wxElementRegistry.findAllRendered({});
const h = rd.find((e: any) => e.elementType === 'columnheader' && e.label === 'Item');
return h ? { cx: h.centerX, cy: h.centerY, hgt: h.height } : null;
});
expect(hdr, 'Item column header found').not.toBeNull();
await page.mouse.click(hdr!.cx, hdr!.cy + hdr!.hgt + 8);
findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work A review of the group-E fixes found 13 further defects; ten were introduced by those fixes, two pre-existed and were merely relocated, one is deferred. Services / transport E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning mirror stayed latched true after a mid-run worker death: Run stayed disabled and the promised fresh-worker restart was unreachable for the whole session. Retirement now dispatches a synthetic controlled-exit straight to the installed handler (never through dispatchEvt — a fabricated frame must not touch the credit ledger). Driving the repro exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit that uninitialized engine before KiCad's validate() re-init (the native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker). Reads now answer their empty shapes pre-init, writes lazy-init, and init is idempotent per worker engine. E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked one unit of the 64-frame credit window until the stream died with a misattributed "transport exceeded". The ack moves to a finally in both service copies; the throw still propagates (the trap machinery needs it). E-20 the oversize-line path promises to transfer the accepted prefix, but with the window full that flush only DEFERS, and stopEventStream wiped the deferred queue — losing the diagnostics that explain the failure. The terminal notice now carries them as pendingEvents; both hosts deliver them in order, unacked (the fatal frame is outside the credit protocol). E-21 the 30s prefetch deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. (Awaiting the aborted collection was rejected: an in-flight source fetch is not abortable — E-4's original disease.) Plus a serving-candidate memo, so a .wrl ref served by its .step fallback stops re-probing the miss on every export. Scheduler E-14 _terminalizeNativeTrap classified by message substring, so any plain JS error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a healthy instance. Now structural only: instanceof RuntimeError plus a duck-typed name check (verified in this build's glue that abort() throws a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort now latches the gate — the authoritative notification, previously ignored. E-15 the shim half: _pumpResume gates on terminal (catching wakes already queued at latch time) and resolveWait refuses on terminal WITHOUT consuming the entry, so a frame stays visibly parked rather than resuming inside a trapped module. E-16 the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path. E-11 get_vec trusted the worker's res.length over the transferred arrays. Observed death shape: a 4 GiB std::vector threw an unhandled std::length_error that exited the editor's main loop. Now clamped, with the buffers freed on every failure path. Guardrails (replacing two deferred refactors: e2e→production-code injection and collapsing the four copies of the worker-lifecycle machinery) E-18 the source contract asserted comment-string counts — rewording failed CI while moving a guard outside its #ifdef passed. It now parses the #ifdef regions and asserts on code. service-stub-parity.ts pins what the four lifecycle copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. The transport numbers are now single-sourced from the worker. CI actually runs the gates: the web/standalone vitest suites (which had NEVER run in CI), the reducer, the source contract and the parity tool — with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint. E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in the ngspice-stub shape with a wedgeNextBoot() repro hook. Every behavioral fix has red-then-green evidence (the reds were captured first). E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral) is DEFERRED with its analysis recorded — a real fix needs run identity on the bg frames. Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare markers carry their reason; three export-report dwells became modal-lease polls; exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch, an unused fault hook and unused receipt plumbing are gone; abort scans, wx dialog drivers, the sim harness and the vitest FakeWorker are each one copy now. Bumps kicad and wxwidgets to their findings-group-e tips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:27:12 +02:00
await page.waitForTimeout(200); // eslint-disable-line -- documented interaction dwell: tree focus commit
await page.keyboard.press('Home');
findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work A review of the group-E fixes found 13 further defects; ten were introduced by those fixes, two pre-existed and were merely relocated, one is deferred. Services / transport E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning mirror stayed latched true after a mid-run worker death: Run stayed disabled and the promised fresh-worker restart was unreachable for the whole session. Retirement now dispatches a synthetic controlled-exit straight to the installed handler (never through dispatchEvt — a fabricated frame must not touch the credit ledger). Driving the repro exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit that uninitialized engine before KiCad's validate() re-init (the native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker). Reads now answer their empty shapes pre-init, writes lazy-init, and init is idempotent per worker engine. E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked one unit of the 64-frame credit window until the stream died with a misattributed "transport exceeded". The ack moves to a finally in both service copies; the throw still propagates (the trap machinery needs it). E-20 the oversize-line path promises to transfer the accepted prefix, but with the window full that flush only DEFERS, and stopEventStream wiped the deferred queue — losing the diagnostics that explain the failure. The terminal notice now carries them as pendingEvents; both hosts deliver them in order, unacked (the fatal frame is outside the credit protocol). E-21 the 30s prefetch deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. (Awaiting the aborted collection was rejected: an in-flight source fetch is not abortable — E-4's original disease.) Plus a serving-candidate memo, so a .wrl ref served by its .step fallback stops re-probing the miss on every export. Scheduler E-14 _terminalizeNativeTrap classified by message substring, so any plain JS error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a healthy instance. Now structural only: instanceof RuntimeError plus a duck-typed name check (verified in this build's glue that abort() throws a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort now latches the gate — the authoritative notification, previously ignored. E-15 the shim half: _pumpResume gates on terminal (catching wakes already queued at latch time) and resolveWait refuses on terminal WITHOUT consuming the entry, so a frame stays visibly parked rather than resuming inside a trapped module. E-16 the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path. E-11 get_vec trusted the worker's res.length over the transferred arrays. Observed death shape: a 4 GiB std::vector threw an unhandled std::length_error that exited the editor's main loop. Now clamped, with the buffers freed on every failure path. Guardrails (replacing two deferred refactors: e2e→production-code injection and collapsing the four copies of the worker-lifecycle machinery) E-18 the source contract asserted comment-string counts — rewording failed CI while moving a guard outside its #ifdef passed. It now parses the #ifdef regions and asserts on code. service-stub-parity.ts pins what the four lifecycle copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. The transport numbers are now single-sourced from the worker. CI actually runs the gates: the web/standalone vitest suites (which had NEVER run in CI), the reducer, the source contract and the parity tool — with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint. E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in the ngspice-stub shape with a wedgeNextBoot() repro hook. Every behavioral fix has red-then-green evidence (the reds were captured first). E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral) is DEFERRED with its analysis recorded — a real fix needs run identity on the bg frames. Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare markers carry their reason; three export-report dwells became modal-lease polls; exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch, an unused fault hook and unused receipt plumbing are gone; abort scans, wx dialog drivers, the sim harness and the vitest FakeWorker are each one copy now. Bumps kicad and wxwidgets to their findings-group-e tips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:27:12 +02:00
await page.waitForTimeout(150); // eslint-disable-line -- documented interaction dwell: tree-nav keystroke commit
await page.keyboard.press('ArrowDown');
findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work A review of the group-E fixes found 13 further defects; ten were introduced by those fixes, two pre-existed and were merely relocated, one is deferred. Services / transport E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning mirror stayed latched true after a mid-run worker death: Run stayed disabled and the promised fresh-worker restart was unreachable for the whole session. Retirement now dispatches a synthetic controlled-exit straight to the installed handler (never through dispatchEvt — a fabricated frame must not touch the credit ledger). Driving the repro exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit that uninitialized engine before KiCad's validate() re-init (the native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker). Reads now answer their empty shapes pre-init, writes lazy-init, and init is idempotent per worker engine. E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked one unit of the 64-frame credit window until the stream died with a misattributed "transport exceeded". The ack moves to a finally in both service copies; the throw still propagates (the trap machinery needs it). E-20 the oversize-line path promises to transfer the accepted prefix, but with the window full that flush only DEFERS, and stopEventStream wiped the deferred queue — losing the diagnostics that explain the failure. The terminal notice now carries them as pendingEvents; both hosts deliver them in order, unacked (the fatal frame is outside the credit protocol). E-21 the 30s prefetch deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. (Awaiting the aborted collection was rejected: an in-flight source fetch is not abortable — E-4's original disease.) Plus a serving-candidate memo, so a .wrl ref served by its .step fallback stops re-probing the miss on every export. Scheduler E-14 _terminalizeNativeTrap classified by message substring, so any plain JS error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a healthy instance. Now structural only: instanceof RuntimeError plus a duck-typed name check (verified in this build's glue that abort() throws a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort now latches the gate — the authoritative notification, previously ignored. E-15 the shim half: _pumpResume gates on terminal (catching wakes already queued at latch time) and resolveWait refuses on terminal WITHOUT consuming the entry, so a frame stays visibly parked rather than resuming inside a trapped module. E-16 the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path. E-11 get_vec trusted the worker's res.length over the transferred arrays. Observed death shape: a 4 GiB std::vector threw an unhandled std::length_error that exited the editor's main loop. Now clamped, with the buffers freed on every failure path. Guardrails (replacing two deferred refactors: e2e→production-code injection and collapsing the four copies of the worker-lifecycle machinery) E-18 the source contract asserted comment-string counts — rewording failed CI while moving a guard outside its #ifdef passed. It now parses the #ifdef regions and asserts on code. service-stub-parity.ts pins what the four lifecycle copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. The transport numbers are now single-sourced from the worker. CI actually runs the gates: the web/standalone vitest suites (which had NEVER run in CI), the reducer, the source contract and the parity tool — with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint. E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in the ngspice-stub shape with a wedgeNextBoot() repro hook. Every behavioral fix has red-then-green evidence (the reds were captured first). E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral) is DEFERRED with its analysis recorded — a real fix needs run identity on the bg frames. Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare markers carry their reason; three export-report dwells became modal-lease polls; exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch, an unused fault hook and unused receipt plumbing are gone; abort scans, wx dialog drivers, the sim harness and the vitest FakeWorker are each one copy now. Bumps kicad and wxwidgets to their findings-group-e tips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:27:12 +02:00
await page.waitForTimeout(150); // eslint-disable-line -- documented interaction dwell: tree-nav keystroke commit
await page.keyboard.press('ArrowUp');
findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work A review of the group-E fixes found 13 further defects; ten were introduced by those fixes, two pre-existed and were merely relocated, one is deferred. Services / transport E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning mirror stayed latched true after a mid-run worker death: Run stayed disabled and the promised fresh-worker restart was unreachable for the whole session. Retirement now dispatches a synthetic controlled-exit straight to the installed handler (never through dispatchEvt — a fabricated frame must not touch the credit ledger). Driving the repro exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit that uninitialized engine before KiCad's validate() re-init (the native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker). Reads now answer their empty shapes pre-init, writes lazy-init, and init is idempotent per worker engine. E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked one unit of the 64-frame credit window until the stream died with a misattributed "transport exceeded". The ack moves to a finally in both service copies; the throw still propagates (the trap machinery needs it). E-20 the oversize-line path promises to transfer the accepted prefix, but with the window full that flush only DEFERS, and stopEventStream wiped the deferred queue — losing the diagnostics that explain the failure. The terminal notice now carries them as pendingEvents; both hosts deliver them in order, unacked (the fatal frame is outside the credit protocol). E-21 the 30s prefetch deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. (Awaiting the aborted collection was rejected: an in-flight source fetch is not abortable — E-4's original disease.) Plus a serving-candidate memo, so a .wrl ref served by its .step fallback stops re-probing the miss on every export. Scheduler E-14 _terminalizeNativeTrap classified by message substring, so any plain JS error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a healthy instance. Now structural only: instanceof RuntimeError plus a duck-typed name check (verified in this build's glue that abort() throws a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort now latches the gate — the authoritative notification, previously ignored. E-15 the shim half: _pumpResume gates on terminal (catching wakes already queued at latch time) and resolveWait refuses on terminal WITHOUT consuming the entry, so a frame stays visibly parked rather than resuming inside a trapped module. E-16 the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path. E-11 get_vec trusted the worker's res.length over the transferred arrays. Observed death shape: a 4 GiB std::vector threw an unhandled std::length_error that exited the editor's main loop. Now clamped, with the buffers freed on every failure path. Guardrails (replacing two deferred refactors: e2e→production-code injection and collapsing the four copies of the worker-lifecycle machinery) E-18 the source contract asserted comment-string counts — rewording failed CI while moving a guard outside its #ifdef passed. It now parses the #ifdef regions and asserts on code. service-stub-parity.ts pins what the four lifecycle copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. The transport numbers are now single-sourced from the worker. CI actually runs the gates: the web/standalone vitest suites (which had NEVER run in CI), the reducer, the source contract and the parity tool — with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint. E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in the ngspice-stub shape with a wedgeNextBoot() repro hook. Every behavioral fix has red-then-green evidence (the reds were captured first). E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral) is DEFERRED with its analysis recorded — a real fix needs run identity on the bg frames. Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare markers carry their reason; three export-report dwells became modal-lease polls; exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch, an unused fault hook and unused receipt plumbing are gone; abort scans, wx dialog drivers, the sim harness and the vitest FakeWorker are each one copy now. Bumps kicad and wxwidgets to their findings-group-e tips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:27:12 +02:00
await page.waitForTimeout(400); // eslint-disable-line -- documented interaction dwell: tree selection commit
expect(await clickByTooltip(page, 'New Symbol...'), 'New Symbol clicked').toBe(true);
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await stableShot(page, 'symremote-02-newsym.png');
// Name field (the one that isn't the lib filter ~y65/87), then confirm.
const nameField = await page.evaluate(() => {
const all = window.wxElementRegistry.findAll({ visible: true });
const t = all
.filter((e: any) => /TextCtrl/i.test(e.typeName))
.map((e: any) => ({ cx: Math.round(e.centerX), cy: Math.round(e.centerY) }))
.find((e: any) => Math.abs(e.cy - 65) > 30 && Math.abs(e.cy - 87) > 30);
return t ?? null;
});
expect(nameField, 'New Symbol name field present').toBeTruthy();
await page.mouse.click(nameField!.cx, nameField!.cy);
findings(E-10..E-22): fix the defects a code review found in the E-1..E-9 work A review of the group-E fixes found 13 further defects; ten were introduced by those fixes, two pre-existed and were merely relocated, one is deferred. Services / transport E-10 retireWorker synthesized no bg/exit frame, so sharedspice's s_bgRunning mirror stayed latched true after a mid-run worker death: Run stayed disabled and the promised fresh-worker restart was unreachable for the whole session. Retirement now dispatches a synthetic controlled-exit straight to the installed handler (never through dispatchEvt — a fabricated frame must not touch the credit ledger). Driving the repro exposed two further defects, both fixed here: a replacement worker trapped on pre-init engine reads, and the rerun's cm_input_path/circ hit that uninitialized engine before KiCad's validate() re-init (the native flow assumes a crashed engine survives in-process — true for the dll, false for a dead worker). Reads now answer their empty shapes pre-init, writes lazy-init, and init is idempotent per worker engine. E-19 dispatchEvt acked only AFTER handler(evt) returned, and the sharedspice client deliberately rethrows non-trap errors — so each throw leaked one unit of the 64-frame credit window until the stream died with a misattributed "transport exceeded". The ack moves to a finally in both service copies; the throw still propagates (the trap machinery needs it). E-20 the oversize-line path promises to transfer the accepted prefix, but with the window full that flush only DEFERS, and stopEventStream wiped the deferred queue — losing the diagnostics that explain the failure. The terminal notice now carries them as pendingEvents; both hosts deliver them in order, unacked (the fatal frame is outside the credit protocol). E-21 the 30s prefetch deadline discarded every model already collected and reported nothing. A caller-owned progress sink ships the partials and the omission reaches the export report. (Awaiting the aborted collection was rejected: an in-flight source fetch is not abortable — E-4's original disease.) Plus a serving-candidate memo, so a .wrl ref served by its .step fallback stops re-probing the miss on every export. Scheduler E-14 _terminalizeNativeTrap classified by message substring, so any plain JS error QUOTING 'Aborted(' or 'out of bounds' permanently bricked a healthy instance. Now structural only: instanceof RuntimeError plus a duck-typed name check (verified in this build's glue that abort() throws a genuine RuntimeError both pre- and post-runtime-init). Module.onAbort now latches the gate — the authoritative notification, previously ignored. E-15 the shim half: _pumpResume gates on terminal (catching wakes already queued at latch time) and resolveWait refuses on terminal WITHOUT consuming the entry, so a frame stays visibly parked rather than resuming inside a trapped module. E-16 the E-5 handler read the realm-global scheduler at dispatch instead of its installing module's; also frees the per-line buffer on the non-trap rethrow path. E-11 get_vec trusted the worker's res.length over the transferred arrays. Observed death shape: a 4 GiB std::vector threw an unhandled std::length_error that exited the editor's main loop. Now clamped, with the buffers freed on every failure path. Guardrails (replacing two deferred refactors: e2e→production-code injection and collapsing the four copies of the worker-lifecycle machinery) E-18 the source contract asserted comment-string counts — rewording failed CI while moving a guard outside its #ifdef passed. It now parses the #ifdef regions and asserts on code. service-stub-parity.ts pins what the four lifecycle copies must share: credit-window equality parsed from source, the finally-ack, boot deadlines, terminal-notice consumption. The transport numbers are now single-sourced from the worker. CI actually runs the gates: the web/standalone vitest suites (which had NEVER run in CI), the reducer, the source contract and the parity tool — with a NON_PLAYWRIGHT_GATES check so deleting a step re-fails the lint. E-22 the e2e occ stub's 60s boot watchdog, deleted in a66e109, is restored in the ngspice-stub shape with a wedgeNextBoot() repro hook. Every behavioral fix has red-then-green evidence (the reds were captured first). E-17 (a stale RUNNING cross-stamping the next run's generation under E-6's transport deferral) is DEFERRED with its analysis recorded — a real fix needs run identity on the bg frames. Test hygiene: the dwell lint now requires the mandated ": <why>" and all 47 bare markers carry their reason; three export-report dwells became modal-lease polls; exact-ledger assertions became relative deltas; the dead data-wx-dom-id branch, an unused fault hook and unused receipt plumbing are gone; abort scans, wx dialog drivers, the sim harness and the vitest FakeWorker are each one copy now. Bumps kicad and wxwidgets to their findings-group-e tips. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-27 12:27:12 +02:00
await page.waitForTimeout(150); // eslint-disable-line -- documented interaction dwell: name field focus commit
await page.keyboard.press('Control+a');
await page.keyboard.press('Delete');
await page.keyboard.type('RemoteRes', { delay: 40 });
await page.keyboard.press('Enter');
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await stableShot(page, 'symremote-03-created.png');
// Save → PUT to the backend.
await focusCanvas(page);
await page.keyboard.press('Control+s');
// Poll the backend until the item lands.
let items: { kind: string; name: string }[] = [];
await expect
.poll(
async () => {
const r = await fetch(`${BACKEND}/api/scopes/${SCOPE}/libs/my-symbols/items`, { headers: ownerHeaders });
items = r.ok ? ((await r.json()) as { kind: string; name: string }[]) : [];
return items.length;
},
{ timeout: 30000, intervals: [500] },
)
.toBeGreaterThan(0);
test(determinism): deterministic waits + stableShot screenshots; drop blind sleeps/ifs/retries Make the Playwright e2e + kicad suites deterministic so screenshot flake stops tracing to timing races. - Blind page.waitForTimeout -> condition waits (expect.poll, web-first assertions, waitUntil) + readiness helpers (waitForWxApp, waitForCanvasApp). Remaining sleeps are documented interaction dwells (annotated). - Defensive "if element exists" branches -> loud asserts; label-fallback chains -> normalized clickMenuItemByText. First-run wizard for/if loops removed by seeding calculator/gerbview/pcbnew HTMLs. - Screenshots: new stableShot(page, name) settles the render in-page (canvas hash over rAF) then writes a raw PNG to test-results/ for the existing offline gate (tools/screenshots vs baseline-screenshots). Replaces toHaveScreenshot, which did inline compare + its own baselines and had decoupled the specs from the real gate. scale:'css' pinned. - retries: 0 in both configs. - Guard: tests/tools/lint-determinism.ts (npm run lint:determinism) bans blind sleeps / toHaveScreenshot / inline retries / swallowed catches in specs; documented exceptions carry a marker. Rules in tests/TESTING.md. Assertions, coverage, and renders unchanged (semantic-equivalence reviewed; captures pixel-identical modulo inherent timer/timestamp/3d-raytrace variance). Both suites green at retries:0 (e2e 340, kicad 92); ~35-61% faster. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BVX1pHMvRPYHdp6ZfEawrk
2026-07-07 10:50:24 +02:00
await stableShot(page, 'symremote-04-saved.png');
const saved = items[0];
logs.push(`[spec] backend item: ${JSON.stringify(saved)}`);
// The persisted body is a well-formed fork-native kicad_symbol_lib.
const bodyRes = await fetch(
`${BACKEND}/api/scopes/${SCOPE}/libs/my-symbols/items/symbol/${encodeURIComponent(saved.name)}`,
{ headers: ownerHeaders },
);
const body = await bodyRes.text();
logs.push(`[spec] persisted body (${body.length} bytes):\n${body}`);
expect(body).toContain('(kicad_symbol_lib');
expect(body).toContain('(version 20250925)');
expect(body).toContain(`(symbol "${saved.name}"`);
// App stayed live.
expect(logs.some((l) => l.includes('Aborted(')), 'no WASM abort').toBe(false);
expect(new URL(page.url()).searchParams.get('oomRetry'), 'no OOM respawn').toBeNull();
console.log('--- spec log ---\n' + logs.join('\n'));
});