design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
import type { Page } from "@playwright/test";
|
|
|
|
|
|
import { test, expect } from "./fixtures";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Doc-19 strand red spec (docs/features/async/19, 20 §6 D0, 21 §4).
|
|
|
|
|
|
*
|
|
|
|
|
|
* The user-visible bug: Symbol Properties (any quasi-modal opened from a tool
|
|
|
|
|
|
* action) stops responding — OK/Cancel click, nothing happens, only the
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
* titlebar × closes it. Mechanism (doc 19 §4, asyncify-era vocabulary): the
|
|
|
|
|
|
* tool fiber that owned the dialog parked mid-body in the quasi-modal wait; a
|
|
|
|
|
|
* concurrent park's wake aliased over its live sleep buffer, the stale-fiber
|
|
|
|
|
|
* guard quarantined it, and the fiber's own legitimate resume was then
|
|
|
|
|
|
* REFUSED and dropped. The fiber never completed, the dispatch guard it held
|
|
|
|
|
|
* never released, every later click deferred forever.
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
*
|
|
|
|
|
|
* Staging: the strand needs concurrent parks over the dialog's parked fiber.
|
|
|
|
|
|
* The deterministic lever is the parking timer (wasm/bindings/timer_park.h):
|
|
|
|
|
|
* arm it so its Notify() parks and wakes while the Symbol Properties fiber is
|
|
|
|
|
|
* parked — the same overlap Leonardo's warm loads produce by volume dice
|
|
|
|
|
|
* (docs/features/async/19 §5, gal-refresh lineage).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Two tests, deliberately split so the red pin cannot rot into vacuity:
|
|
|
|
|
|
* - "staging" is a plain GREEN test: the dialog opens, the timer window
|
2026-08-08 23:11:51 +02:00
|
|
|
|
* engages (fired + parked + done). RE-PINNED AT THE FLIP (doc 22 §10,
|
|
|
|
|
|
* 2026-08-08): the overlap the shim used to observe is structurally
|
|
|
|
|
|
* impossible post-D5, so the assert now pins ZERO observable
|
|
|
|
|
|
* concurrent-park windows instead.
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
* - "OK closes" is the RED pin, marked test.fail(): its assertions are the
|
|
|
|
|
|
* desired end state (dialog closes, no refused-resume beacon, wait books
|
|
|
|
|
|
* balanced). It goes green at D3 (waits become context yields), at which
|
|
|
|
|
|
* point Playwright reports "expected to fail but passed" and the marker
|
|
|
|
|
|
* must be removed — the forced flip doc 20 §8 asks for.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
const SCH = `(kicad_sch
|
|
|
|
|
|
\t(version 20231120)
|
|
|
|
|
|
\t(generator "eeschema")
|
|
|
|
|
|
\t(uuid "cccc0000-0000-0000-0000-000000000001")
|
|
|
|
|
|
\t(paper "A4")
|
|
|
|
|
|
\t(lib_symbols
|
|
|
|
|
|
\t\t(symbol "Device:R"
|
|
|
|
|
|
\t\t\t(pin_numbers (hide yes))
|
|
|
|
|
|
\t\t\t(pin_names (offset 0))
|
|
|
|
|
|
\t\t\t(exclude_from_sim no) (in_bom yes) (on_board yes)
|
|
|
|
|
|
\t\t\t(property "Reference" "R" (at 2.032 0 90) (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
\t\t\t(property "Value" "R" (at 0 0 90) (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
\t\t\t(symbol "R_0_1"
|
|
|
|
|
|
\t\t\t\t(rectangle (start -1.016 -2.54) (end 1.016 2.54)
|
|
|
|
|
|
\t\t\t\t\t(stroke (width 0.254) (type default)) (fill (type none)))
|
|
|
|
|
|
\t\t\t)
|
|
|
|
|
|
\t\t\t(symbol "R_1_1"
|
|
|
|
|
|
\t\t\t\t(pin passive line (at 0 3.81 270) (length 1.27)
|
|
|
|
|
|
\t\t\t\t\t(name "~" (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
\t\t\t\t\t(number "1" (effects (font (size 1.27 1.27)))))
|
|
|
|
|
|
\t\t\t\t(pin passive line (at 0 -3.81 90) (length 1.27)
|
|
|
|
|
|
\t\t\t\t\t(name "~" (effects (font (size 1.27 1.27))))
|
|
|
|
|
|
\t\t\t\t\t(number "2" (effects (font (size 1.27 1.27)))))
|
|
|
|
|
|
\t\t\t)
|
|
|
|
|
|
\t\t)
|
|
|
|
|
|
\t)
|
|
|
|
|
|
\t(symbol
|
|
|
|
|
|
\t\t(lib_id "Device:R")
|
|
|
|
|
|
\t\t(at 100 100 0)
|
|
|
|
|
|
\t\t(unit 1)
|
|
|
|
|
|
\t\t(exclude_from_sim no) (in_bom yes) (on_board yes) (dnp no)
|
|
|
|
|
|
\t\t(uuid "dddd0000-0000-0000-0000-000000000002")
|
|
|
|
|
|
\t\t(property "Reference" "R1" (at 102 98 0) (effects (font (size 1.27 1.27)) (justify left)))
|
|
|
|
|
|
\t\t(property "Value" "10k" (at 102 101 0) (effects (font (size 1.27 1.27)) (justify left)))
|
|
|
|
|
|
\t\t(instances (project "strand" (path "/cccc0000-0000-0000-0000-000000000001" (reference "R1") (unit 1))))
|
|
|
|
|
|
\t)
|
|
|
|
|
|
)`;
|
|
|
|
|
|
|
|
|
|
|
|
const SYMBOL_UUID = "dddd0000-0000-0000-0000-000000000002";
|
|
|
|
|
|
|
|
|
|
|
|
type Mod = {
|
|
|
|
|
|
kicadOpenFile(p: string): unknown;
|
|
|
|
|
|
kicadOpenFileBusy(): boolean;
|
|
|
|
|
|
kicadCollabGetPos(id: string): string;
|
|
|
|
|
|
kicadCollabGetViewport(): string;
|
|
|
|
|
|
kicadTestArmTimerPark(delayMs: number, parkMs: number): boolean;
|
|
|
|
|
|
kicadTestTimerParkState(): string;
|
|
|
|
|
|
};
|
|
|
|
|
|
type FS = { mkdirTree(p: string): void; writeFile(p: string, d: string): void };
|
|
|
|
|
|
|
|
|
|
|
|
type SchedulerBooks = {
|
|
|
|
|
|
waitsBegun: number;
|
|
|
|
|
|
waitsResolved: number;
|
|
|
|
|
|
pendingWaits(kind: string): number;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
async function bootAndOpen(page: Page): Promise<void> {
|
|
|
|
|
|
await page.goto("/kicad/eeschema.html");
|
|
|
|
|
|
await expect(page.locator("#canvas")).toBeVisible({ timeout: 120000 });
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
() => {
|
|
|
|
|
|
const m = (window as unknown as { Module?: Partial<Mod> }).Module;
|
|
|
|
|
|
return (
|
|
|
|
|
|
typeof m?.kicadOpenFile === "function" &&
|
|
|
|
|
|
typeof m?.kicadCollabGetPos === "function" &&
|
|
|
|
|
|
// The parking-timer lever exists in the merged kicad_editor bundle
|
|
|
|
|
|
// (which eeschema.html serves via --frame=sch) — hard requirement,
|
|
|
|
|
|
// the staging is built on it.
|
|
|
|
|
|
typeof m?.kicadTestArmTimerPark === "function"
|
|
|
|
|
|
);
|
|
|
|
|
|
},
|
|
|
|
|
|
null,
|
|
|
|
|
|
{ timeout: 120000 },
|
|
|
|
|
|
);
|
|
|
|
|
|
await page.waitForFunction(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
!!window.wxElementRegistry &&
|
|
|
|
|
|
window.wxElementRegistry
|
|
|
|
|
|
.findAll({ visible: true })
|
|
|
|
|
|
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
|
|
|
|
|
|
null,
|
|
|
|
|
|
{ timeout: 120000 },
|
|
|
|
|
|
);
|
|
|
|
|
|
await page.evaluate((sch) => {
|
|
|
|
|
|
const w = window as unknown as { FS: FS; Module: Mod };
|
|
|
|
|
|
const dir = "/home/kicad/documents";
|
|
|
|
|
|
try {
|
|
|
|
|
|
w.FS.mkdirTree(dir);
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* exists */
|
|
|
|
|
|
}
|
|
|
|
|
|
w.FS.writeFile(`${dir}/strand.kicad_sch`, sch);
|
|
|
|
|
|
w.Module.kicadOpenFile(`${dir}/strand.kicad_sch`);
|
|
|
|
|
|
}, SCH);
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(() => page.evaluate(() => (window.Module as unknown as Mod).kicadOpenFileBusy()), {
|
|
|
|
|
|
timeout: 120000,
|
|
|
|
|
|
intervals: [250],
|
|
|
|
|
|
})
|
|
|
|
|
|
.toBe(false);
|
|
|
|
|
|
// The symbol landed and is queryable — the open truly settled.
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
page.evaluate(
|
|
|
|
|
|
(id) => (window.Module as unknown as Mod).kicadCollabGetPos(id),
|
|
|
|
|
|
SYMBOL_UUID,
|
|
|
|
|
|
),
|
|
|
|
|
|
{ timeout: 30000, intervals: [250] },
|
|
|
|
|
|
)
|
|
|
|
|
|
.toMatch(/^-?[\d.]+,-?[\d.]+$/);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Screen-space center of the fixture symbol (viewport transform math as
|
|
|
|
|
|
* presence-locks-pcbnew.spec.ts). */
|
|
|
|
|
|
async function symbolScreenPos(page: Page): Promise<{ x: number; y: number }> {
|
|
|
|
|
|
const glId = await page.evaluate(() => {
|
|
|
|
|
|
const visible = Array.from(document.querySelectorAll('[id^="glcanvas-"]'))
|
|
|
|
|
|
.map((c) => c as HTMLCanvasElement)
|
|
|
|
|
|
.find(
|
|
|
|
|
|
(c) =>
|
|
|
|
|
|
window.getComputedStyle(c).display !== "none" &&
|
|
|
|
|
|
c.getBoundingClientRect().width > 0,
|
|
|
|
|
|
);
|
|
|
|
|
|
return visible?.id ?? null;
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(glId, "a visible GAL canvas (glcanvas-*)").toBeTruthy();
|
|
|
|
|
|
const box = await page.locator(`#${glId}`).boundingBox();
|
|
|
|
|
|
expect(box, "canvas bounding box").not.toBeNull();
|
|
|
|
|
|
|
|
|
|
|
|
const { vp, pos } = await page.evaluate((id) => {
|
|
|
|
|
|
const m = window.Module as unknown as Mod;
|
|
|
|
|
|
return {
|
|
|
|
|
|
vp: JSON.parse(m.kicadCollabGetViewport()) as {
|
|
|
|
|
|
cx: number;
|
|
|
|
|
|
cy: number;
|
|
|
|
|
|
scale: number;
|
|
|
|
|
|
w: number;
|
|
|
|
|
|
h: number;
|
|
|
|
|
|
},
|
|
|
|
|
|
pos: m.kicadCollabGetPos(id),
|
|
|
|
|
|
};
|
|
|
|
|
|
}, SYMBOL_UUID);
|
|
|
|
|
|
const [wx, wy] = pos.split(",").map(Number);
|
|
|
|
|
|
const x = box!.x + (wx - vp.cx) * vp.scale + vp.w / 2;
|
|
|
|
|
|
const y = box!.y + (wy - vp.cy) * vp.scale + vp.h / 2;
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[STRAND] canvas=${glId} box=${JSON.stringify(box)} vp=${JSON.stringify(vp)} ` +
|
|
|
|
|
|
`pos=${pos} -> screen=(${x.toFixed(1)}, ${y.toFixed(1)})`,
|
|
|
|
|
|
);
|
|
|
|
|
|
// The point must be inside the canvas or the double-click is aimed at air.
|
|
|
|
|
|
expect(x, "symbol x inside canvas").toBeGreaterThan(box!.x);
|
|
|
|
|
|
expect(x, "symbol x inside canvas").toBeLessThan(box!.x + box!.width);
|
|
|
|
|
|
expect(y, "symbol y inside canvas").toBeGreaterThan(box!.y);
|
|
|
|
|
|
expect(y, "symbol y inside canvas").toBeLessThan(box!.y + box!.height);
|
|
|
|
|
|
return { x, y };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Visible non-file dialogs in the wx element registry (the pre-created
|
|
|
|
|
|
* wxFileDialog reads visible at boot — exclude it). */
|
|
|
|
|
|
const dialogCount = (page: Page) =>
|
|
|
|
|
|
page.evaluate(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
window.wxElementRegistry
|
|
|
|
|
|
.findAll({ visible: true })
|
|
|
|
|
|
.filter((e) => /Dialog/i.test(e.typeName) && e.typeName !== "wxFileDialog").length,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
/** Center of the dialog's OK button (a real DOM button inside the dialog's
|
|
|
|
|
|
* window div — import-settings-modal-stack.spec.ts pattern). */
|
|
|
|
|
|
async function okButtonCenter(page: Page): Promise<{ x: number; y: number }> {
|
|
|
|
|
|
const rect = await page.evaluate(() => {
|
|
|
|
|
|
const btn = Array.from(
|
|
|
|
|
|
document.querySelectorAll("#window-container button"),
|
|
|
|
|
|
).find((b) => (b.textContent ?? "").trim() === "OK");
|
|
|
|
|
|
if (!btn) return null;
|
|
|
|
|
|
const r = (btn as HTMLElement).getBoundingClientRect();
|
|
|
|
|
|
return { x: r.x + r.width / 2, y: r.y + r.height / 2, w: r.width, h: r.height };
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(rect, "the dialog has an OK button").not.toBeNull();
|
|
|
|
|
|
expect(rect!.w, "OK button has extent").toBeGreaterThan(0);
|
|
|
|
|
|
return { x: rect!.x, y: rect!.y };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** Open Symbol Properties via the real UI path (double-click the symbol) and
|
|
|
|
|
|
* run the parking-timer window across the dialog's parked fiber.
|
|
|
|
|
|
*
|
|
|
|
|
|
* NOTE on what this returns: `done` (the timer's park completed) is NOT a
|
|
|
|
|
|
* staging invariant — whether a park survives the aliasing is part of the
|
|
|
|
|
|
* disease under test, so asserting it would make the harness fail for the
|
|
|
|
|
|
* bug's own reason. Only `fired` + `sawParked` prove the window existed. */
|
|
|
|
|
|
async function openDialogAndEngageWindow(
|
|
|
|
|
|
page: Page,
|
|
|
|
|
|
): Promise<{ timer: { fired: boolean; sawParked: boolean; done: boolean } }> {
|
|
|
|
|
|
const { x, y } = await symbolScreenPos(page);
|
|
|
|
|
|
|
|
|
|
|
|
await page.mouse.dblclick(x, y);
|
|
|
|
|
|
|
|
|
|
|
|
// Symbol Properties opens: the edit tool's fiber is now parked in the
|
|
|
|
|
|
// quasi-modal wait and STAYS parked for as long as the dialog is up.
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(() => dialogCount(page), { timeout: 20000, intervals: [250] })
|
|
|
|
|
|
.toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
// Arm only NOW. Because the fiber's park is open-ended (it ends when the
|
|
|
|
|
|
// dialog closes, which is what the red pin is about), a timer that fires
|
|
|
|
|
|
// while the dialog is up necessarily parks ON TOP of it — the overlap is
|
|
|
|
|
|
// deterministic by construction rather than a race won by luck. (The
|
|
|
|
|
|
// opener zeroes its interlock slot for the park's duration, so the mailbox
|
|
|
|
|
|
// delivers the timer instead of deferring it.)
|
|
|
|
|
|
const armed = await page.evaluate(() =>
|
|
|
|
|
|
(window.Module as unknown as Mod).kicadTestArmTimerPark(30, 1500),
|
|
|
|
|
|
);
|
|
|
|
|
|
expect(armed, "parking timer armed while the dialog is open").toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
// Ride the window: fired → (parked) → done. `sawParked` is best-effort —
|
|
|
|
|
|
// a sample may simply miss a short park — so it is reported, never asserted.
|
|
|
|
|
|
const timer = await page.evaluate(async () => {
|
|
|
|
|
|
const m = window.Module as unknown as Mod;
|
|
|
|
|
|
const stats = { fired: false, sawParked: false, done: false };
|
|
|
|
|
|
const t0 = performance.now();
|
|
|
|
|
|
// Bounded ride: exits as soon as the park completes, and stops after the
|
|
|
|
|
|
// budget if it never does (a park that never completes is a legitimate
|
|
|
|
|
|
// outcome here — see the note on the caller).
|
|
|
|
|
|
while (performance.now() - t0 < 12000) {
|
|
|
|
|
|
const st = JSON.parse(m.kicadTestTimerParkState()) as {
|
|
|
|
|
|
fired: number;
|
|
|
|
|
|
done: number;
|
|
|
|
|
|
parked: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
if (st.fired > 0) stats.fired = true;
|
|
|
|
|
|
if (st.parked) stats.sawParked = true;
|
|
|
|
|
|
if (st.done > 0) {
|
|
|
|
|
|
stats.done = true;
|
|
|
|
|
|
break;
|
|
|
|
|
|
}
|
|
|
|
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
|
|
|
|
}
|
|
|
|
|
|
return stats;
|
|
|
|
|
|
});
|
|
|
|
|
|
return { timer };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
test.describe("quasi-modal strand (doc 19)", () => {
|
|
|
|
|
|
test.describe.configure({ mode: "serial" });
|
|
|
|
|
|
|
|
|
|
|
|
test("staging: Symbol Properties opens and the parked-timer window engages", async ({
|
|
|
|
|
|
page,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
test.setTimeout(240000);
|
|
|
|
|
|
await bootAndOpen(page);
|
|
|
|
|
|
const { timer } = await openDialogAndEngageWindow(page);
|
|
|
|
|
|
console.log(`[STRAND] staging timer: ${JSON.stringify(timer)}`);
|
|
|
|
|
|
|
|
|
|
|
|
// Engagement proofs — without these the red pin below is vacuous.
|
|
|
|
|
|
// Asserted: the timer FIRED inside the window (monotonic counter, not a
|
|
|
|
|
|
// sampled state). Not asserted: `done` (whether a park survives is the
|
|
|
|
|
|
// disease under test) and `sawParked` (a 100 ms sampler can miss a short
|
|
|
|
|
|
// park); the beacon check below is the sampling-independent overlap proof.
|
|
|
|
|
|
expect(timer.fired, "parking timer fired while the dialog was open").toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
// The dialog is up and its OK button is a real, hittable DOM button.
|
|
|
|
|
|
expect(await dialogCount(page)).toBeGreaterThan(0);
|
|
|
|
|
|
await okButtonCenter(page);
|
|
|
|
|
|
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
|
// RE-PINNED AT THE FLIP (docs/features/async/22 §10, 2026-08-08), and
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
// RE-KEYED for JSPI (2026-08-13): the asyncify scheduler's concurrent-park
|
|
|
|
|
|
// beacon family retired with it, which made the old filter
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
|
// vacuous. The post-migration invariant is the same — the dialog opens,
|
|
|
|
|
|
// the timer fires and its park survives (asserted above) — and the
|
|
|
|
|
|
// observable JSPI failure modes of an overlap are ghost/refused
|
|
|
|
|
|
// transitions, a stuck-window force-clear, or a job-tick trap.
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
const overlapBeacons = testLogger.consoleLogs.filter((l) =>
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
|
/\[libctx-jspi\] ghost\/refused transition|\[wx-scheduler\] (force-clearing stuck window|job tick error)/.test(
|
|
|
|
|
|
l,
|
|
|
|
|
|
),
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
);
|
|
|
|
|
|
console.log(`[STRAND] staging overlap beacons: ${overlapBeacons.length}`);
|
|
|
|
|
|
expect(
|
|
|
|
|
|
overlapBeacons.length,
|
jspi: fix the dead-tools ownership bug, emscripten-6 fallout, and green the full suite on Playwright 1.62
Live-app fix (Place Footprints / routing dead in Chrome): submodule
bumps carry the coroutine ownership fix (kicad 012d95ecb4) and the
handler-exception survival fix (wxwidgets 1b5f0e31f4).
Emscripten-6 fallout:
- occ/ngspice worker wrappers: mainScriptUrlOrBlob was removed
upstream; pthread children re-run the wrapper blob, so an em-pthread
realm now importScripts the glue and gets out of the way (before:
recursive service boots, pool never fills, silent 180s boot hangs —
every occ spec and ngspice bg_run).
- Makefile.wasm: -sASYNCIFY frankenlinks on the no-wx coroutine repro
targets ported to -sJSPI (the JSPI-only libcontext crashed at first
yield under them); mainloop/gl repro pages drive their tick through a
promising export (emscripten_set_main_loop callbacks cannot suspend);
retired inject-dyncall-shims lines removed (targets were unbuildable
since Phase 8); $stringToNewUTF8 force-included (the EM_ASM value
bridge aborted the runtime on the first decoded exception).
- fiber-park levers: neither embind shape can drive suspending levers
(plain throws on strict-JSPI Firefox; emscripten::async() re-executes
its invoker on settle) — kept sync for manual Chromium probing, spec
coverage moved to the jspi-coroutine harness (18 cases).
Suite work:
- Playwright 1.61.1 -> 1.62.1 (Firefox 153: JSPI on by default).
- fiber-resume-park.spec retired -> coroutine-lifecycle.spec: census
gate over boot / board load / chooser open / cancel (deterministically
red on the pre-fix build).
- Blind asyncify-era pins re-keyed: quasimodal-strand + wait-beacons
beacon regexes, footprint-chooser-close liveness -> wx parking-timer
heartbeat (scheduler counters idle flat on Firefox).
- occ/ngspice test providers: 60s boot timeout + worker error
surfacing (a worker death used to be a silent 180s timeout).
- Harness pages: stale 9.99 config dir -> 10.0 (library_manager wxCHECK
noise, chooser had no libraries).
- gal-webgl harness: missing artifacts rebuilt (boost/glm extracted to
the host sysroot), PgmOrNull stub added for the rebased GAL.
- jspi-scheduler: clean-shutdown console line restored (app-quit
contract), quarantine never yanks SP from a live window.
Gates: test:e2e 699 passed / 0 failed (wx-chromium, kicad-firefox,
kicad-chromium, jspi-firefox, coroutine-firefox); web ff/cr/mobile 71
passed; lint:ci-coverage 166, lint:determinism 163, screenshots
manifest 492 current, corpus 7/7, tools:contract green. Offline
screenshot baselines show expected mass drift from the engine bump —
re-baseline (screenshots:noise -> promote) is a follow-up.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-13 17:41:28 +02:00
|
|
|
|
"no ghost/stuck-window/job-tick anomaly is observable post-flip",
|
2026-08-08 23:11:51 +02:00
|
|
|
|
).toBe(0);
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
2026-08-06 12:22:21 +02:00
|
|
|
|
test("doc-19: OK resolves the quasi-modal wait and the dialog closes", async ({
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
page,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
2026-08-06 12:22:21 +02:00
|
|
|
|
// WAS RED (aliased wake → quarantined fiber → refused resume → the dialog
|
|
|
|
|
|
// could never be closed by a click). GREEN since the quasi-modal's nested
|
|
|
|
|
|
// event loop stopped running on the tool coroutine's stack: it is bounced
|
|
|
|
|
|
// onto the main stack, so the coroutine is suspended the legitimate way —
|
|
|
|
|
|
// a recorded fiber swap — instead of parking its body where the fiber
|
|
|
|
|
|
// layer cannot see it. This test is now the regression pin for that.
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
test.setTimeout(240000);
|
|
|
|
|
|
await bootAndOpen(page);
|
|
|
|
|
|
const { timer } = await openDialogAndEngageWindow(page);
|
|
|
|
|
|
console.log(`[STRAND] red timer: ${JSON.stringify(timer)}`);
|
|
|
|
|
|
expect(timer.fired, "the doc-19 window was staged").toBe(true);
|
|
|
|
|
|
|
|
|
|
|
|
const ok = await okButtonCenter(page);
|
|
|
|
|
|
await page.mouse.click(ok.x, ok.y);
|
|
|
|
|
|
|
|
|
|
|
|
// Give the close its full budget, then REPORT the outcome before
|
|
|
|
|
|
// asserting — the log should say which end state failed, not merely that
|
|
|
|
|
|
// one did. (Polling to a boolean never throws; the assertions below are
|
|
|
|
|
|
// the verdict.)
|
|
|
|
|
|
const closed = await page
|
|
|
|
|
|
.waitForFunction(
|
|
|
|
|
|
() =>
|
|
|
|
|
|
window.wxElementRegistry
|
|
|
|
|
|
.findAll({ visible: true })
|
|
|
|
|
|
.filter((e) => /Dialog/i.test(e.typeName) && e.typeName !== "wxFileDialog")
|
|
|
|
|
|
.length === 0,
|
|
|
|
|
|
null,
|
|
|
|
|
|
{ timeout: 15000 },
|
|
|
|
|
|
)
|
|
|
|
|
|
.then(() => true, () => false);
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
const anomaliesSoFar = testLogger.consoleLogs.filter((l) =>
|
|
|
|
|
|
/\[libctx-jspi\] ghost\/refused transition|\[wx-scheduler\] (force-clearing stuck window|job tick error)|entry REJECTED/.test(
|
|
|
|
|
|
l,
|
|
|
|
|
|
),
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
).length;
|
|
|
|
|
|
console.log(
|
|
|
|
|
|
`[STRAND] red outcome: closed=${closed} dialogs=${await dialogCount(page)} ` +
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
`anomalies=${anomaliesSoFar}`,
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// Desired end state 1: the dialog closes.
|
|
|
|
|
|
expect(closed, "OK closed the quasi-modal dialog").toBe(true);
|
|
|
|
|
|
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
// Desired end state 2: no ghost/refused transition, scheduler anomaly or
|
|
|
|
|
|
// rejected coroutine entry anywhere in the run (the old refused-resume
|
|
|
|
|
|
// beacon retired with the asyncify scheduler; these are the JSPI
|
|
|
|
|
|
// equivalents of a dropped resume).
|
|
|
|
|
|
const anomalies = testLogger.consoleLogs.filter((l) =>
|
|
|
|
|
|
/\[libctx-jspi\] ghost\/refused transition|\[wx-scheduler\] (force-clearing stuck window|job tick error)|entry REJECTED/.test(
|
|
|
|
|
|
l,
|
|
|
|
|
|
),
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
);
|
jspi cleanup: remove the asyncify-era residue — dead code, conditionals, pipeline scaffolding, stale prose
The runtime is JSPI-only; this removes everything that still pretended
otherwise. Three exhaustive sweeps (C++/JS+build+CI/tests+docs) drove
the inventory; every deletion verified by grep closure + full gates.
Broken-right-now fixes:
- deploy-staging.yml passed the retired opt_level input — the workflow
could not even start. Removed.
- env.sh carried dead exports with a live -sASYNCIFY=1 inside
(WASM_LDFLAGS/PTHREAD_LDFLAGS, zero consumers). Removed; the
WASM_LEGACY_EXCEPTIONS rationale rewritten to the real reason.
- docker/build.sh exported PCBJAM_ASYNC_BACKEND (read nowhere). Gone.
Dead weight removed:
- binaryen submodule (nothing builds or invokes it), wasm-opt-bench
workflow + scripts/bench/, get-wasm-opt.sh, diagnostics.js (242 lines
of Asyncify-API-only code), the KICAD_PIPELINE background-postprocess
scaffolding (existed to parallelize the deleted wasm-opt phase; the
postprocess is a seconds-long node script and now runs inline),
build-monitor's dead asyncify rows, sched-context orphan build
output, dead .gitignore entries, the .jspi-assets spike dir (the two
wf-result research JSONs moved to docs/features/async/migration-evidence/).
- bindings: fiber_park.h + its 12 embind registrations (broken-if-
called under JSPI), the kicadOpenFileStart/OPEN_JOB starter route,
main_stack_runner.h + 5 includes, the always-null context-sleep weak
hook in nanosleep_yield.c.
- shim: the backend field (installed-flag idempotency instead),
noteContextWait (dead both sides), the __wxAsyncifyDump alias (+ the
WasmTool fallback and string-dump normalize branch).
- web: the emscripten-6-ignored mainScriptUrlOrBlob option in boot.ts
(gerber-demo keeps it: it loads the deployed CDN release, which
predates emscripten 6 — noted inline).
Conditionals: all 'backend === jspi' checks reduced to scheduler-
presence checks; races_quiescent re-keyed from Asyncify.state (vacuous)
to real backlog quiescence (resumeReady/mutatorQueue — NOT _windowLive,
which is the probing activation's own window by definition).
Renames (identifiers only, no file renames): ASYNC_LINK_FLAGS→
JSPI_LINK_FLAGS and Makefile ASYNC_LDFLAGS→JSPI_LDFLAGS,
kicadCollabFiberBusy→kicadCollabBusy (embind + web + tests),
collab_common.h fiber*→apply*/coroutine naming, asyncifySignatures→
wasmTrapSignatures (lists byte-identical).
Tests: the two remaining vacuous [wx-asyncify]/fiber-resume-refused
asserts re-keyed to live JSPI beacons; eeschema-load's failure message
no longer sends the developer to a deleted script; wait-beacons' dead
families/parser deleted; lane-0 legacy-glue guards removed (lane 0 is
unconstructible); the embind test.fail re-gated with the JSPI reason
(plain embind invokers cannot suspend — verified still failing);
lint-determinism now scans tests/jspi (166 files clean);
eeschema-collab local-move gated to chromium (~50% flaky on FF even
solo; pcbnew twin covers both engines).
Docs: DEBUG.md rewritten as the JSPI debugging guide; build.md
describes the single-phase build; docs/features/async/README.md
banner-marked historical and repointed at the NEW
23-jspi-runtime.md (current architecture: export census, turnstile,
libcontext ownership + refusal contract, embind call shapes, the
em-pthread service-wrapper trick, exception policy, known gaps).
Gates on the cleaned tree: test:e2e 725 passed / 0 failed (after the
quiescence-probe fix; the 3 other reds were verified contention flakes
solo-green or the documented FF gate), web 76/0, jspi 18/18 both
engines, vitest 295/295 + 17/17, all lints green, live-app census
clean.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016X9eh1s5sTx1o9Em9KBuwR
2026-08-14 09:25:32 +02:00
|
|
|
|
expect(anomalies, `anomalies: ${anomalies.join(" || ")}`).toHaveLength(0);
|
design-b D0: land the doc-19 strand as a deterministic red spec
Doc 20 D0 second deliverable (doc 21 §4). tests/kicad/quasimodal-strand.spec.ts
reproduces the Symbol Properties hang on demand, in two parts:
- "staging" (GREEN): double-click the fixture symbol → Symbol Properties
opens → arm the parking timer (wasm/bindings/timer_park.h) WHILE the
dialog is up. Because the opener's fiber park is open-ended, a timer
firing now necessarily parks on top of it — the overlap is structural,
not a won race. Asserts the dialog opened, the timer fired, its OK
button is hittable, and the shim beaconed concurrent contexts. Keeps
the red pin from rotting into vacuity, and fails loudly on its own.
- "doc-19 red" (test.fail()): clicks OK and asserts the desired end state
— dialog closes, zero fiber-resume-refused beacons, wait books balanced
(no unresolved nested/modal wait). Goes green at D3, when Playwright
will report "expected to fail but passed" and the marker comes off.
Verified 6/6 consecutive full-file runs, identical outcome each time:
closed=false dialogs=1 refused-resumes=1 — the doc-19 mechanism exactly
(quarantined fiber's legitimate resume refused, dialog never closes).
Deliberately NOT asserted in staging: the timer park COMPLETING (whether
a park survives the aliasing is the disease under test) and sawParked (a
100ms sampler can miss a short park) — both are reported, not gated.
Retires tests/kicad/dialog-deadlock-probe.spec.ts: the 8/4 throwaway probe
that established the mechanism. This spec supersedes it and, unlike it, is
deterministic (the probe's 3 blind waitForTimeouts were the only
determinism-lint violations in the tree; the guard is now clean).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-05 21:15:01 +02:00
|
|
|
|
|
|
|
|
|
|
// Desired end state 3: the wait books balance — the quasi-modal's
|
|
|
|
|
|
// "nested" wait was resolved and consumed, nothing left parked.
|
|
|
|
|
|
const books = await page.evaluate(() => {
|
|
|
|
|
|
const s = (globalThis as unknown as { __wxScheduler: SchedulerBooks }).__wxScheduler;
|
|
|
|
|
|
return {
|
|
|
|
|
|
begun: s.waitsBegun,
|
|
|
|
|
|
resolved: s.waitsResolved,
|
|
|
|
|
|
pendingNested: s.pendingWaits("nested"),
|
|
|
|
|
|
pendingModal: s.pendingWaits("modal"),
|
|
|
|
|
|
};
|
|
|
|
|
|
});
|
|
|
|
|
|
expect(books.pendingNested, "no unresolved nested wait").toBe(0);
|
|
|
|
|
|
expect(books.pendingModal, "no unresolved modal wait").toBe(0);
|
|
|
|
|
|
expect(books.resolved, "every begun wait resolved").toBe(books.begun);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|