2026-07-21 09:16:38 +02:00
|
|
|
|
import { execSync } from "node:child_process";
|
|
|
|
|
|
import path from "node:path";
|
|
|
|
|
|
import { test, expect } from "./fixtures";
|
|
|
|
|
|
import {
|
|
|
|
|
|
TRIO_PL,
|
|
|
|
|
|
TRIO_PCB,
|
|
|
|
|
|
TRIO_SCH,
|
|
|
|
|
|
type ToolCfg,
|
|
|
|
|
|
SYM1,
|
2026-07-21 10:37:37 +02:00
|
|
|
|
WIRE1,
|
2026-07-21 09:16:38 +02:00
|
|
|
|
WIRE2,
|
|
|
|
|
|
FP1,
|
|
|
|
|
|
PAD1,
|
|
|
|
|
|
VIA1,
|
|
|
|
|
|
SEG1,
|
2026-07-21 10:37:37 +02:00
|
|
|
|
SEG2,
|
2026-07-21 09:16:38 +02:00
|
|
|
|
callHook,
|
|
|
|
|
|
closeTrio,
|
|
|
|
|
|
drift,
|
|
|
|
|
|
getPos,
|
|
|
|
|
|
hasAbort,
|
|
|
|
|
|
modelText,
|
|
|
|
|
|
openTrio,
|
|
|
|
|
|
oracleSweep,
|
|
|
|
|
|
settleConverged,
|
|
|
|
|
|
undoDepth,
|
|
|
|
|
|
} from "./utils/trio";
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* Drift trio harness — S1 baseline (standalone-hardening 0008, phase A).
|
|
|
|
|
|
*
|
|
|
|
|
|
* Three tabs as three users in one room: A seeds + edits, B edits, C only
|
|
|
|
|
|
* observes. After every action group the full oracle sweep runs: per-tab drift
|
|
|
|
|
|
* silence (editor ≡ Y.Doc, production comparator), byte-identical silent saves
|
|
|
|
|
|
* across all three, and identical room materialization. C is the pure
|
|
|
|
|
|
* remote-apply oracle — everything it holds arrived over the wire.
|
|
|
|
|
|
*
|
|
|
|
|
|
* S1 drives only the EXISTING embind test hooks (move/rotate/set-field/
|
|
|
|
|
|
* set-pad-size/remove); the action-catalog hooks land in phase B.
|
|
|
|
|
|
*
|
|
|
|
|
|
* Repro convention (ysync-review): known-open gaps assert the CORRECT behavior
|
|
|
|
|
|
* and are marked test.fail() naming the tracking doc — fixing the bug flips
|
|
|
|
|
|
* them to "unexpected pass", forcing the marker's removal.
|
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
|
|
test.beforeAll(() => {
|
|
|
|
|
|
// Rebuild the v2 bundle so the harness always exercises the current stack.
|
|
|
|
|
|
execSync("node collab/build.mjs", { cwd: path.resolve(__dirname, ".."), stdio: "inherit" });
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
/** Heavy editors exceed Firefox's per-content-process wasm budget at 2+
|
|
|
|
|
|
* instances (see ysync-two-tab.spec.ts) — at 3 it is strictly worse. */
|
|
|
|
|
|
function skipFirefox(): void {
|
2026-08-13 18:43:36 +02:00
|
|
|
|
// 2026-08-13: guard retired — the JSPI build (~half the asyncify size) fits
|
|
|
|
|
|
// three editor tabs inside Firefox 153's per-process wasm budget; the
|
|
|
|
|
|
// whole file passes on kicad-firefox. Kept as a hook in case the wall
|
|
|
|
|
|
// returns with future growth.
|
2026-07-21 09:16:38 +02:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── pl_editor: green baseline validating the trio plumbing itself ────────────
|
|
|
|
|
|
|
|
|
|
|
|
test.describe("drift trio — pl_editor (plumbing baseline)", () => {
|
|
|
|
|
|
test.describe.configure({ timeout: 420000 });
|
|
|
|
|
|
|
|
|
|
|
|
test("A and B add text, C observes: all three drift-silent and converged", async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
const room = `drift-trio-pl-${test.info().workerIndex}`;
|
|
|
|
|
|
const trio = await openTrio(context, TRIO_PL, room);
|
|
|
|
|
|
|
|
|
|
|
|
const uuidA = await callHook<string>(trio.A, "kicadCollabTestAddText", "From A", 40, 40);
|
|
|
|
|
|
expect(uuidA).toMatch(/[0-9a-f-]{36}/);
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(async () => await modelText(trio.C, TRIO_PL), { timeout: 20000, intervals: [300] })
|
|
|
|
|
|
.toContain("From A");
|
|
|
|
|
|
await settleConverged(trio, TRIO_PL);
|
|
|
|
|
|
await oracleSweep(trio, TRIO_PL);
|
|
|
|
|
|
|
|
|
|
|
|
const uuidB = await callHook<string>(trio.B, "kicadCollabTestAddText", "From B", 60, 60);
|
|
|
|
|
|
expect(uuidB).toMatch(/[0-9a-f-]{36}/);
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(async () => await modelText(trio.A, TRIO_PL), { timeout: 20000, intervals: [300] })
|
|
|
|
|
|
.toContain("From B");
|
|
|
|
|
|
await settleConverged(trio, TRIO_PL);
|
|
|
|
|
|
await oracleSweep(trio, TRIO_PL);
|
|
|
|
|
|
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
await closeTrio(trio);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// ── The heavy editors: eeschema + pcbnew S1 ──────────────────────────────────
|
|
|
|
|
|
|
|
|
|
|
|
interface S1Actions {
|
|
|
|
|
|
/** [hookName, ...args] for B's rotate target and A's property edit. */
|
|
|
|
|
|
rotateTarget: string;
|
|
|
|
|
|
propertyEdit: { fn: string; args: (string | number)[]; expectInSave: string };
|
|
|
|
|
|
removeTarget: string;
|
|
|
|
|
|
moveDx: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const S1: ReadonlyArray<readonly [ToolCfg, string, S1Actions]> = [
|
|
|
|
|
|
[
|
|
|
|
|
|
TRIO_SCH,
|
|
|
|
|
|
"eeschema",
|
|
|
|
|
|
{
|
|
|
|
|
|
rotateTarget: SYM1,
|
|
|
|
|
|
// Assign-footprint-shaped edit: symbol field text (Value).
|
|
|
|
|
|
propertyEdit: {
|
|
|
|
|
|
fn: "kicadCollabTestSetFieldText",
|
|
|
|
|
|
args: [SYM1, "trio-10k"],
|
|
|
|
|
|
expectInSave: '"trio-10k"',
|
|
|
|
|
|
},
|
|
|
|
|
|
removeTarget: WIRE2,
|
|
|
|
|
|
moveDx: 40000, // eeschema IU = 1e4/mm → 4mm
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
[
|
|
|
|
|
|
TRIO_PCB,
|
|
|
|
|
|
"pcbnew",
|
|
|
|
|
|
{
|
|
|
|
|
|
rotateTarget: FP1,
|
|
|
|
|
|
propertyEdit: {
|
|
|
|
|
|
fn: "kicadCollabTestSetPadSize",
|
|
|
|
|
|
args: [PAD1, 1500000, 1000000], // pcbnew IU = 1e6/mm → 1.5 × 1 mm
|
|
|
|
|
|
expectInSave: "(size 1.5 1)",
|
|
|
|
|
|
},
|
|
|
|
|
|
removeTarget: VIA1,
|
|
|
|
|
|
moveDx: 2000000, // 2mm
|
|
|
|
|
|
},
|
|
|
|
|
|
],
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const [cfg, label, act] of S1) {
|
|
|
|
|
|
test.describe(`drift trio — ${label} S1 baseline`, () => {
|
|
|
|
|
|
test.describe.configure({ timeout: 900000 });
|
|
|
|
|
|
|
|
|
|
|
|
test(`${label}: A+B interleave move/rotate/property/delete, C observes`, async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
skipFirefox();
|
|
|
|
|
|
const room = `drift-trio-${label}-${test.info().workerIndex}`;
|
|
|
|
|
|
const trio = await openTrio(context, cfg, room);
|
|
|
|
|
|
|
|
|
|
|
|
// 1. A moves the first item (the seeder's emit half — bug 01 regression
|
|
|
|
|
|
// surface: seed()'s snapshotItems registered A's listener). The hooks
|
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
|
|
|
|
// run on a coroutine, so first poll A's OWN pos until the move landed
|
2026-07-21 09:16:38 +02:00
|
|
|
|
// (two-tab's green precondition), then compare the peers against it.
|
|
|
|
|
|
const uuids = [...cfg.fixture.matchAll(/\(uuid "([0-9a-f-]{36})"\)/g)].map((m) => m[1]!);
|
|
|
|
|
|
const before: Record<string, string> = {};
|
|
|
|
|
|
for (const u of uuids) before[u] = await getPos(trio.A, u);
|
|
|
|
|
|
const movedId = await callHook<string>(trio.A, "kicadCollabTestMoveFirst", act.moveDx, 0);
|
|
|
|
|
|
expect(movedId).toMatch(/[0-9a-f-]{36}/);
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(() => getPos(trio.A, movedId), { timeout: 15000, intervals: [300] })
|
|
|
|
|
|
.not.toBe(before[movedId]);
|
|
|
|
|
|
const posA = await getPos(trio.A, movedId);
|
|
|
|
|
|
for (const peer of [trio.B, trio.C]) {
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(() => getPos(peer, movedId), { timeout: 20000, intervals: [300] })
|
|
|
|
|
|
.toBe(posA);
|
|
|
|
|
|
}
|
|
|
|
|
|
await settleConverged(trio, cfg);
|
|
|
|
|
|
await oracleSweep(trio, cfg);
|
|
|
|
|
|
|
|
|
|
|
|
// 2. B rotates the symbol/footprint (joiner's emit half).
|
|
|
|
|
|
expect(await callHook<boolean>(trio.B, "kicadCollabTestRotateItem", act.rotateTarget, 90)).toBe(
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
await settleConverged(trio, cfg);
|
|
|
|
|
|
await oracleSweep(trio, cfg);
|
|
|
|
|
|
|
|
|
|
|
|
// 3. A edits a property (field text / pad size — the "assign footprint,
|
|
|
|
|
|
// change value" family).
|
|
|
|
|
|
expect(
|
|
|
|
|
|
await callHook<boolean>(trio.A, act.propertyEdit.fn, ...act.propertyEdit.args),
|
|
|
|
|
|
).toBe(true);
|
|
|
|
|
|
for (const peer of [trio.B, trio.C]) {
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(async () => await modelText(peer, cfg), { timeout: 20000, intervals: [300] })
|
|
|
|
|
|
.toContain(act.propertyEdit.expectInSave);
|
|
|
|
|
|
}
|
|
|
|
|
|
await settleConverged(trio, cfg);
|
|
|
|
|
|
await oracleSweep(trio, cfg);
|
|
|
|
|
|
|
|
|
|
|
|
// 4. B deletes a top-level item.
|
|
|
|
|
|
expect(await callHook<boolean>(trio.B, "kicadCollabTestRemoveItem", act.removeTarget)).toBe(
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
for (const peer of [trio.A, trio.C]) {
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(async () => await modelText(peer, cfg), { timeout: 20000, intervals: [300] })
|
|
|
|
|
|
.not.toContain(act.removeTarget);
|
|
|
|
|
|
}
|
|
|
|
|
|
await settleConverged(trio, cfg);
|
|
|
|
|
|
await oracleSweep(trio, cfg);
|
|
|
|
|
|
|
|
|
|
|
|
// 5. Undo isolation: remote applies ride SKIP_UNDO — the observer's undo
|
|
|
|
|
|
// stack must still be empty; the editors accumulated only their own ops.
|
|
|
|
|
|
expect(await undoDepth(trio.C), "observer undo stack").toBe(0);
|
|
|
|
|
|
expect(await undoDepth(trio.A), "A undo stack").toBeGreaterThan(0);
|
|
|
|
|
|
expect(await undoDepth(trio.B), "B undo stack").toBeGreaterThan(0);
|
|
|
|
|
|
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
await closeTrio(trio);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── pcbnew segment CHANGE apply (the envelope-parse path) ────────────────────
|
|
|
|
|
|
// A changed track rides wrapInBoardEnvelope → the `(kicad_pcb …)` envelope
|
|
|
|
|
|
// parse on the receiver. ysync 0008 "known limit 1" recorded that path as
|
|
|
|
|
|
// dying silently in the commit — this trio run shows the CHANGE path works
|
|
|
|
|
|
// under the current build (the still-fixme'd roundtrip full-board case is the
|
|
|
|
|
|
// bulk-ADD rebuild, a different path). Kept green as the regression guard.
|
|
|
|
|
|
|
|
|
|
|
|
test.describe("drift trio — pcbnew segment edit (envelope-parse change path)", () => {
|
|
|
|
|
|
test.describe.configure({ timeout: 900000 });
|
|
|
|
|
|
|
|
|
|
|
|
test("a segment endpoint edit reaches the peers' editors", async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
skipFirefox();
|
|
|
|
|
|
|
|
|
|
|
|
const room = `drift-trio-pcbseg-${test.info().workerIndex}`;
|
|
|
|
|
|
const trio = await openTrio(context, TRIO_PCB, room);
|
|
|
|
|
|
|
|
|
|
|
|
expect(await callHook<boolean>(trio.A, "kicadCollabTestMoveEndpoint", SEG1, 2000000, 0)).toBe(
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
|
|
// B applies the changed segment blob (envelope parse) and stays
|
|
|
|
|
|
// drift-silent; all three converge on the moved endpoint.
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(async () => (await drift(trio.B, TRIO_PCB))?.updated ?? [], {
|
|
|
|
|
|
timeout: 20000,
|
|
|
|
|
|
intervals: [400],
|
|
|
|
|
|
})
|
|
|
|
|
|
.toEqual([]);
|
|
|
|
|
|
await settleConverged(trio, TRIO_PCB);
|
|
|
|
|
|
await oracleSweep(trio, TRIO_PCB);
|
|
|
|
|
|
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
await closeTrio(trio);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
2026-07-21 10:37:37 +02:00
|
|
|
|
|
|
|
|
|
|
// ── S1 catalog: every phase-B action primitive, A/B alternating, sweep each ──
|
|
|
|
|
|
// One trio session per tool; each step runs on its actor, converges, and the
|
|
|
|
|
|
// full oracle sweep must stay silent. A step that returns a uuid parks it in
|
|
|
|
|
|
// `ctx` for later steps ("A adds a symbol → B moves THAT symbol").
|
|
|
|
|
|
|
|
|
|
|
|
type CatalogStep = {
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
actor: "A" | "B";
|
|
|
|
|
|
run(page: import("@playwright/test").Page, ctx: Record<string, string>): Promise<void>;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
async function hookUuid(
|
|
|
|
|
|
page: import("@playwright/test").Page,
|
|
|
|
|
|
ctx: Record<string, string>,
|
|
|
|
|
|
key: string,
|
|
|
|
|
|
fn: string,
|
|
|
|
|
|
...args: (string | number)[]
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
const uuid = await callHook<string>(page, fn, ...args);
|
|
|
|
|
|
expect(uuid, `${fn} must return a uuid`).toMatch(/[0-9a-f-]{36}/);
|
|
|
|
|
|
ctx[key] = uuid;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
async function hookOk(
|
|
|
|
|
|
page: import("@playwright/test").Page,
|
|
|
|
|
|
fn: string,
|
|
|
|
|
|
...args: (string | number | boolean)[]
|
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
|
expect(await callHook<boolean>(page, fn, ...(args as (string | number)[])), `${fn} must hit`).toBe(
|
|
|
|
|
|
true,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// eeschema IU = 1e4/mm; pcbnew IU = 1e6/mm.
|
|
|
|
|
|
const SCH_CATALOG: CatalogStep[] = [
|
|
|
|
|
|
{ name: "A adds a wire", actor: "A", run: (p, c) => hookUuid(p, c, "wire", "kicadCollabTestAddWire", 600000, 600000, 700000, 600000) },
|
|
|
|
|
|
// A junction dropped mid-wire with no branch is REDUNDANT — eeschema's
|
|
|
|
|
|
// connectivity cleanup deletes it in the same commit (net-zero, the landed
|
|
|
|
|
|
// gate would time out). A branch wire off A's wire is the realistic edit;
|
|
|
|
|
|
// the cleanup auto-inserts the junction at the T, which then syncs.
|
|
|
|
|
|
{ name: "B branches a wire off A's wire", actor: "B", run: (p, c) => hookUuid(p, c, "branch", "kicadCollabTestAddWire", 650000, 600000, 650000, 650000) },
|
|
|
|
|
|
{ name: "A adds a local label", actor: "A", run: (p, c) => hookUuid(p, c, "label", "kicadCollabTestAddLabel", "label", "NET_A", 620000, 600000) },
|
|
|
|
|
|
{ name: "B adds a global label", actor: "B", run: (p, c) => hookUuid(p, c, "glabel", "kicadCollabTestAddLabel", "global", "VCC", 700000, 600000) },
|
|
|
|
|
|
{ name: "A adds a hier label", actor: "A", run: (p, c) => hookUuid(p, c, "hlabel", "kicadCollabTestAddLabel", "hier", "H1", 640000, 600000) },
|
|
|
|
|
|
{ name: "B adds a no-connect", actor: "B", run: (p, c) => hookUuid(p, c, "nc", "kicadCollabTestAddNoConnect", 710000, 600000) },
|
|
|
|
|
|
{ name: "A places a symbol from the doc lib", actor: "A", run: (p, c) => hookUuid(p, c, "sym2", "kicadCollabTestAddSymbol", "Device:R", 800000, 800000, "R2") },
|
|
|
|
|
|
{ name: "B moves that symbol", actor: "B", run: (p, c) => hookOk(p, "kicadCollabTestMoveSchItem", c.sym2!, 40000, 0) },
|
|
|
|
|
|
{ name: "A mirrors it", actor: "A", run: (p, c) => hookOk(p, "kicadCollabTestMirrorSchItem", c.sym2!, true) },
|
|
|
|
|
|
{ name: "B duplicates it", actor: "B", run: (p, c) => hookUuid(p, c, "sym3", "kicadCollabTestDuplicateSchItem", c.sym2!, 100000, 0) },
|
|
|
|
|
|
{ name: "A renames the duplicate's value", actor: "A", run: (p, c) => hookOk(p, "kicadCollabTestSetFieldText", c.sym3!, "dup-R") },
|
|
|
|
|
|
{ name: "B deletes the fixture wire", actor: "B", run: (p) => hookOk(p, "kicadCollabTestRemoveItem", WIRE1) },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
const PCB_CATALOG: CatalogStep[] = [
|
|
|
|
|
|
{ name: "A adds a track", actor: "A", run: (p, c) => hookUuid(p, c, "track", "kicadCollabTestAddTrack", 50000000, 90000000, 60000000, 90000000, 300000, "F.Cu") },
|
|
|
|
|
|
{ name: "B adds a via", actor: "B", run: (p, c) => hookUuid(p, c, "via2", "kicadCollabTestAddVia", 55000000, 90000000, 800000, 400000) },
|
|
|
|
|
|
{ name: "A adds board text", actor: "A", run: (p, c) => hookUuid(p, c, "text", "kicadCollabTestAddBoardText", "T1", 70000000, 90000000, "F.SilkS") },
|
|
|
|
|
|
{ name: "B adds a zone", actor: "B", run: (p, c) => hookUuid(p, c, "zone", "kicadCollabTestAddZone", 30000000, 30000000, 40000000, 40000000, "F.Cu") },
|
|
|
|
|
|
{ name: "A flips the footprint", actor: "A", run: (p) => hookOk(p, "kicadCollabTestFlipBoardItem", FP1) },
|
|
|
|
|
|
{ name: "B edits the footprint value", actor: "B", run: (p) => hookOk(p, "kicadCollabTestSetFootprintField", FP1, "Value", "R-edited") },
|
|
|
|
|
|
{ name: "A locks a segment", actor: "A", run: (p) => hookOk(p, "kicadCollabTestSetBoardItemLocked", SEG2, true) },
|
|
|
|
|
|
{ name: "B moves the fixture via", actor: "B", run: (p) => hookOk(p, "kicadCollabTestMoveBoardItem", VIA1, 2000000, 0) },
|
|
|
|
|
|
{ name: "A duplicates the footprint", actor: "A", run: (p, c) => hookUuid(p, c, "fp2", "kicadCollabTestDuplicateBoardItem", FP1, 10000000, 0) },
|
|
|
|
|
|
{ name: "B deletes a fixture segment", actor: "B", run: (p) => hookOk(p, "kicadCollabTestRemoveItem", SEG1) },
|
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
|
|
for (const [cfg, label, catalog] of [
|
|
|
|
|
|
[TRIO_SCH, "eeschema", SCH_CATALOG],
|
|
|
|
|
|
[TRIO_PCB, "pcbnew", PCB_CATALOG],
|
|
|
|
|
|
] as const) {
|
|
|
|
|
|
test.describe(`drift trio — ${label} S1 action catalog`, () => {
|
|
|
|
|
|
test.describe.configure({ timeout: 900000 });
|
|
|
|
|
|
|
|
|
|
|
|
test(`${label}: full catalog, A/B alternating, sweep after every step`, async ({
|
|
|
|
|
|
context,
|
|
|
|
|
|
testLogger,
|
|
|
|
|
|
}) => {
|
|
|
|
|
|
skipFirefox();
|
|
|
|
|
|
const room = `drift-trio-cat-${label}-${test.info().workerIndex}`;
|
|
|
|
|
|
const trio = await openTrio(context, cfg, room);
|
|
|
|
|
|
const ctx: Record<string, string> = {};
|
|
|
|
|
|
|
|
|
|
|
|
for (const step of catalog) {
|
|
|
|
|
|
await test.step(step.name, async () => {
|
|
|
|
|
|
const actor = step.actor === "A" ? trio.A : trio.B;
|
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
|
|
|
|
// The hooks commit on a coroutine: settleConverged alone can pass on the
|
2026-07-21 10:37:37 +02:00
|
|
|
|
// PRE-action state (all tabs still equal) and the sweep then reads
|
|
|
|
|
|
// legitimate mid-propagation state as drift. Gate on the actor's own
|
|
|
|
|
|
// save changing first, so convergence is convergence ON the edit.
|
|
|
|
|
|
const before = await modelText(actor, cfg);
|
|
|
|
|
|
await step.run(actor, ctx);
|
|
|
|
|
|
await expect
|
|
|
|
|
|
.poll(() => modelText(actor, cfg), { timeout: 15000, intervals: [300] })
|
|
|
|
|
|
.not.toBe(before);
|
|
|
|
|
|
await settleConverged(trio, cfg);
|
|
|
|
|
|
await oracleSweep(trio, cfg);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
expect(await undoDepth(trio.C), "observer undo stack").toBe(0);
|
|
|
|
|
|
expect(hasAbort(testLogger), "no WASM abort").toBe(false);
|
|
|
|
|
|
await closeTrio(trio);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|