mailbox S1 complete: embind lane, wheel lane, dual-contract specs

Shim embind lane wraps the doc-18 production mutators at the Module
boundary (busy-window calls queue + deliver post-settle; time-boxed
unkillable pump). N2 un-fixme'd and green; collab-load-fuzz carries the
variant contract (drop on legacy, deliver-in-order on scheduler, capped
hammer on the scheduler lane); timer-park's timerRetry silence tripwire
arms on shim+export and is green on the C-lane kicad build. Bump
wxwidgets for the wheel lane. CI both-EH matrix deliberately deferred.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
Gergő Törcsvári 2026-08-05 15:01:52 +02:00
commit af1eb5fc58
No known key found for this signature in database
GPG key ID: 8E75F2CDE64E5322
6 changed files with 223 additions and 21 deletions

View file

@ -25,6 +25,12 @@ import { test, expect } from "./fixtures";
* - mid-load applies are DROPPED (the probe segment must not move);
* - after settle the entries work normally (guard released).
*
* VARIANT CONTRACT (docs/features/async/17 §3b): on WX_SCHEDULER=1 glue the
* shim's embind lane queues busy-window mutators and delivers them after
* settle, so the "applies are DROPPED" assertions flip to "applies are
* DELIVERED in order" assertSettledContract branches on the lane's
* presence. The busy-window and release assertions hold for both variants.
*
* The second test is the scheduler-dependent stress fuzz (spinning-worker CPU
* starvation to force real futex-wait parks, hammering throughout the load).
* It is skipped unless PCBJAM_FUZZ_STRESS=1: engagement of the window is not
@ -229,12 +235,28 @@ async function openAndHammer(
let iterations = 0;
let maxBusySnapshotItems = 0;
const errors: string[] = [];
// Scheduler glue QUEUES busy-window entries for post-settle delivery
// (doc 17 §3b) — an unbounded hammer would replay hundreds of heavy
// applies/snapshots afterwards (each Push walks connectivity across the
// fixture's 800 vias; on a debug build every via prints an assert — a
// 170k-line console flood that drowns the drain). The deterministic
// contract needs delivery + order, not volume: cap the queued calls and
// keep observing the busy window. Legacy glue keeps the full hammer
// (drop semantics make it free). Volume lives in the STRESS test.
const lane =
((globalThis as unknown as { __wxScheduler?: { mutatorsWrapped: number } }).__wxScheduler
?.mutatorsWrapped ?? 0) > 0;
const maxEntryIters = lane ? 6 : Infinity;
// Every Asyncify park of the open chain hands the event loop to this
// timer — exactly how the prod shell's collab attach interleaved.
while (performance.now() - t0 < 120000) {
if (!w.Module.kicadOpenFileBusy()) break;
busySamples++;
iterations++;
if (iterations > maxEntryIters) {
await new Promise((r) => setTimeout(r, 10));
continue;
}
for (const [name, fn] of [
["snapshotItems", () => w.Module.kicadCollabSnapshotItems()],
["snapshot", () => w.Module.kicadCollabSnapshot()],
@ -271,18 +293,46 @@ async function openAndHammer(
async function assertSettledContract(page: Page, stats: FuzzStats): Promise<void> {
expect(stats.settled, "kicadOpenFileBusy cleared after the load").toBe(true);
expect(stats.errors, "no traps while hammering entries mid-load").toEqual([]);
// Guard held: no mid-load snapshot ever saw the model.
// Guard held: no mid-load snapshot ever saw the model. On scheduler glue a
// busy-window snapshot returns a Promise (typeof !== "string" — the hammer
// skips it), so this assertion holds for both variants.
expect(stats.maxBusySnapshotItems, "mid-load snapshots returned the empty delta").toBe(0);
await expect.poll(() => page.title(), { timeout: 30000 }).toMatch(/fuzz/i);
// Guard dropped the mid-load applies: the probe segment never moved.
// Variant contract (docs/features/async/17 §3b). Legacy glue: the gate
// DROPPED the mid-load applies — the probe never moved. Scheduler glue
// (WX_SCHEDULER=1 shim embind lane, doc 18): the same applies were QUEUED
// and DELIVERED after settle, in order — the probe sits where the hammer's
// deltas moved it. Same stimulus, the drop→deliver flip is the assertion.
const schedulerLane = await page.evaluate(
() =>
((globalThis as unknown as { __wxScheduler?: { mutatorsWrapped: number } }).__wxScheduler
?.mutatorsWrapped ?? 0) > 0,
);
if (schedulerLane) {
// The hammer queued hundreds of calls (each mid-load snapshot delivers as
// a FULL board walk now, not the gate's empty delta) — wait for the
// time-boxed pump to drain the backlog before asserting final state.
await expect
.poll(
() =>
page.evaluate(
() =>
(globalThis as unknown as { __wxScheduler: { mutatorQueue: unknown[] } })
.__wxScheduler.mutatorQueue.length,
),
{ timeout: 240000, intervals: [1000] },
)
.toBe(0);
}
const HAMMER_TARGET = "55000000,55000000"; // both hammer deltas move the probe here
await expect
.poll(
() =>
page.evaluate((id) => (window.Module as unknown as Mod).kicadCollabGetPos(id), SEG_TARGET),
{ timeout: 10000, intervals: [200] },
)
.toBe(PROBE_HOME);
.toBe(schedulerLane ? HAMMER_TARGET : PROBE_HOME);
// Guard released: the snapshot now walks the real, fully-loaded board…
const itemCount = await page.evaluate(
@ -326,7 +376,9 @@ test.describe("collab entries during a parked board load (open_gate)", () => {
page,
testLogger,
}) => {
test.setTimeout(180000);
// Scheduler-glue runs replay the whole hammer backlog after settle (the
// drain-wait in assertSettledContract) — budget for it on top of the load.
test.setTimeout(420000);
await bootHarness(page);
const hooks = await page.evaluate(() => {

View file

@ -2,21 +2,19 @@ import type { Page } from "@playwright/test";
import { test, expect } from "./fixtures";
/**
* N2 message ordering under a parked open (RED, target semantics).
* N2 message ordering under a parked open (scheduler-build target semantics).
* docs/features/async/17-mailbox-scheduler-plan.md §3d N2, §3b.
*
* Today (open_gate, doc 14): collab entries issued while `kicadOpenFile` is
* asyncify-parked are DROPPED collab-load-fuzz.spec.ts asserts exactly that
* contract, and it is correct for the guard architecture.
* Legacy glue (open_gate, doc 14): collab entries issued while `kicadOpenFile`
* is asyncify-parked are DROPPED collab-load-fuzz.spec.ts asserts that drop
* contract on legacy builds, and it is correct for the guard architecture.
*
* The mailbox flips dropdeliver: a mutating entry issued during the open
* becomes a queued message, applied IN ORDER after the open completes. This
* spec asserts those target semantics, so it is RED by design until:
* - S1 (JS wrapper enqueues mutating embind entries) makes basic delivery
* work, and
* - S4 (open runs on a scheduler fiber) removes the gate entirely.
* Un-fixme at S1; collab-load-fuzz's "entries no-op" assertions retire at S4
* (they flip per doc 17 §3b).
* becomes a queued message, applied IN ORDER after the open completes. GREEN
* since S1's embind lane the WX_SCHEDULER=1 shim wraps the audited mutators
* (doc 18) at the Module boundary, queueing busy-window calls and delivering
* after settle with promise-returned results. S4 moves queueing worker-side.
* Self-skips on legacy glue (the lane is a build variant until S5).
*
* Ordering probe: apply A ADDS a segment, apply B MOVES that same segment.
* B can only land if A landed first the single final-position check proves
@ -81,8 +79,7 @@ async function bootHarness(page: Page): Promise<void> {
}
test.describe("mailbox N2: entries during a parked open are delivered in order", () => {
// RED until doc 17 S1 — the open gate currently DROPS both applies.
test.fixme("apply A (add) then apply B (move) mid-park land in order after settle", async ({
test("apply A (add) then apply B (move) mid-park land in order after settle", async ({
page,
testLogger,
}) => {
@ -90,6 +87,15 @@ test.describe("mailbox N2: entries during a parked open are delivered in order",
void testLogger;
await bootHarness(page);
// The delivery contract under test is the scheduler build's embind lane;
// on legacy glue the open gate drops both applies by design.
const lane = await page.evaluate(() => {
const s = (globalThis as unknown as { __wxScheduler?: { mutatorsWrapped: number } })
.__wxScheduler;
return s ? s.mutatorsWrapped : 0;
});
test.skip(lane === 0, "legacy glue — embind lane absent (drop contract in collab-load-fuzz)");
const issued = await page.evaluate(async ({ newSeg, board }) => {
const w = window as unknown as { FS: FS; Module: Mod };
const dir = "/home/kicad/documents";
@ -146,9 +152,9 @@ test.describe("mailbox N2: entries during a parked open are delivered in order",
expect(issued.inWindow, "the busy window was observed").toBe(true);
expect(issued.settled, "the open settled").toBe(true);
// TARGET SEMANTICS (mailbox): both queued applies were delivered, in order —
// A's segment exists and sits where B moved it. Under the open gate both
// are dropped and GetPos finds nothing: deterministic red.
// Both queued applies were delivered, in order — A's segment exists and
// sits where B moved it. (Drop-A-deliver-B leaves B targetless; drop-both
// leaves GetPos empty — either failure mode misses B_TARGET.)
await expect
.poll(
() =>

View file

@ -1,5 +1,6 @@
import type { Page } from "@playwright/test";
import { test, expect } from "./fixtures";
import { expectGuardsSilent } from "./utils/guard-beacons";
/**
* Timer-park concurrent-Asyncify repro (gal-refresh-timer investigation).
@ -311,6 +312,22 @@ test.describe("timer Notify() Asyncify-park during main-loop yield (concurrent c
);
expect(trapLines, "no wasm trap signature anywhere in the run").toEqual([]);
// Doc 17 S1 tripwire (arms itself when the wasm ships the C mailbox lane,
// detected via the wxWasmMailboxTick export): timers are then delivered
// from the mailbox only when the interlock is free, so the legacy 17 ms
// parked-retry path must be SILENT. On legacy wasm this is skipped — the
// retry storm there is expected and covered by the assertions above.
// Both halves must be present: the wasm export (C lane compiled in) AND
// the shim (variant injected) — a C-lane wasm on legacy glue keeps legacy
// timer semantics, and its retry storms are expected.
const cLane = await page.evaluate(
() =>
typeof (window.Module as unknown as { _wxWasmMailboxTick?: unknown })
._wxWasmMailboxTick === "function" &&
!!(globalThis as unknown as { __wxScheduler?: unknown }).__wxScheduler,
);
if (cLane) expectGuardsSilent(testLogger.consoleLogs, ["timerRetry"]);
// Window-engagement proof, independent of survival: the handlesleep shim
// must have SEEN the concurrent parks (its reporting is new — silence here
// means the lever never created the overlap and the repro is vacuous).