mailbox S0: dual-glue flag, beacon counters, N2 red spec
Doc 17 step S0 scaffolding: WX_SCHEDULER=1 injector path with an observation-only asyncify-scheduler.js skeleton (legacy shim stays authoritative until S2), guard-beacon extraction with occurrence recovery for rate-limited beacons, and the fixme'd N2 ordering spec (add-then-move probe; un-fixme at S1). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
parent
7713a6a1cf
commit
94ae4a8a41
5 changed files with 354 additions and 1 deletions
|
|
@ -127,6 +127,17 @@ rollback = the `WX_SCHEDULER=0` build + a per-step tag.
|
||||||
- **S0 · Baseline + scaffolding (2–3 d).** Tag current mains. CI matrix runs both EH models.
|
- **S0 · Baseline + scaffolding (2–3 d).** Tag current mains. CI matrix runs both EH models.
|
||||||
Add beacon-count extraction to the fuzz/e2e harness (guard-firing counters per run). Stand up
|
Add beacon-count extraction to the fuzz/e2e harness (guard-firing counters per run). Stand up
|
||||||
the `WX_SCHEDULER` dual-glue build. Write N2 (red), N8.
|
the `WX_SCHEDULER` dual-glue build. Write N2 (red), N8.
|
||||||
|
> **Work log 2026-08-05 — S0 mostly landed** on `feature/async-mailbox`:
|
||||||
|
> local `mailbox-s0-baseline` tags in all 6 repos; `WX_SCHEDULER=1` injector path +
|
||||||
|
> `scripts/common/shims/asyncify-scheduler.js` (observation-only skeleton, build marker
|
||||||
|
> `[wx-scheduler] scaffolding installed`; legacy shim stays authoritative until S2;
|
||||||
|
> remember the `.ci-cache-epoch` bump when shim behavior changes);
|
||||||
|
> `tests/kicad/utils/guard-beacons.ts` (per-family counts, occurrence-recovery for
|
||||||
|
> rate-limited beacons, `expectGuardsSilent`, `parseAsyncifyCounters`);
|
||||||
|
> `REPRO_ASSERT=1` in `apps/tests/tools/repro-board-load.ts` (N8: fails on unsettled load,
|
||||||
|
> missing recorder, `rootHotTotal>0`, trap signatures);
|
||||||
|
> `tests/kicad/mailbox-ordering.spec.ts` (N2, `test.fixme` red — add-then-move ordering
|
||||||
|
> probe; un-fixme at S1). **Still open in S0:** the CI workflow matrix for both EH models.
|
||||||
- **S1 · Mailbox front-end (≈1 wk).** One queue, drained by `wxWasmTopLevelTick`. Route into it:
|
- **S1 · Mailbox front-end (≈1 wk).** One queue, drained by `wxWasmTopLevelTick`. Route into it:
|
||||||
timer `Notify` (replacing the direct callback body; the 17 ms retry stays as tripwire), DOM
|
timer `Notify` (replacing the direct callback body; the 17 ms retry stays as tripwire), DOM
|
||||||
input (formalizing today's `wxPostEvent`/`CallAfter` deferrals), and a JS-side wrapper for
|
input (formalizing today's `wxPostEvent`/`CallAfter` deferrals), and a JS-side wrapper for
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@
|
||||||
# The actual JavaScript that gets injected lives in readable, standalone files in
|
# The actual JavaScript that gets injected lives in readable, standalone files in
|
||||||
# scripts/common/shims/ (not inline heredocs):
|
# scripts/common/shims/ (not inline heredocs):
|
||||||
# - handlesleep.js nested-Asyncify handleSleep currData save/restore (#9153)
|
# - handlesleep.js nested-Asyncify handleSleep currData save/restore (#9153)
|
||||||
|
# - asyncify-scheduler.js WX_SCHEDULER=1 builds only: the mailbox/scheduler
|
||||||
|
# (docs/features/async/17; S0 = observation-only skeleton)
|
||||||
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
# - diagnostics.js optional logging-only instrumentation (see SHIM_DIAGNOSTICS)
|
||||||
#
|
#
|
||||||
# Native wasm-EH is the only build mode, so the .js has no invoke_* wrappers / dynCall_<sig> call
|
# Native wasm-EH is the only build mode, so the .js has no invoke_* wrappers / dynCall_<sig> call
|
||||||
|
|
@ -17,6 +19,7 @@
|
||||||
# Usage:
|
# Usage:
|
||||||
# inject-dyncall-shims.sh <pcbnew.js>
|
# inject-dyncall-shims.sh <pcbnew.js>
|
||||||
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
# SHIM_DIAGNOSTICS=1 inject-dyncall-shims.sh <pcbnew.js> # also inject diagnostics.js
|
||||||
|
# WX_SCHEDULER=1 inject-dyncall-shims.sh <pcbnew.js> # also inject the scheduler (dual-glue variant)
|
||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
|
|
@ -32,7 +35,7 @@ if [ -z "$JS_FILE" ] || [ ! -f "$JS_FILE" ]; then
|
||||||
echo "Usage: $0 <path/to/pcbnew.js>"
|
echo "Usage: $0 <path/to/pcbnew.js>"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
for f in handlesleep.js diagnostics.js; do
|
for f in handlesleep.js asyncify-scheduler.js diagnostics.js; do
|
||||||
if [ ! -f "$SHIM_DIR/$f" ]; then
|
if [ ! -f "$SHIM_DIR/$f" ]; then
|
||||||
echo "Error: missing shim source $SHIM_DIR/$f"
|
echo "Error: missing shim source $SHIM_DIR/$f"
|
||||||
exit 1
|
exit 1
|
||||||
|
|
@ -99,6 +102,24 @@ else
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# --- 3e. Mailbox/scheduler (WX_SCHEDULER=1 dual-glue variant) ------------------
|
||||||
|
# docs/features/async/17-mailbox-scheduler-plan.md, step S0. Appended AFTER the
|
||||||
|
# handleSleep shim: the legacy shim stays authoritative until S2, when the
|
||||||
|
# scheduler takes ownership of currData and this ordering flips. Idempotent via
|
||||||
|
# the __wxSchedulerInstalled marker. Default OFF — the legacy build is the
|
||||||
|
# shippable fallback until S5.
|
||||||
|
if [ "${WX_SCHEDULER:-0}" = "1" ]; then
|
||||||
|
if grep -q '__wxSchedulerInstalled' "$JS_FILE"; then
|
||||||
|
echo "asyncify-scheduler already present - skipping"
|
||||||
|
else
|
||||||
|
echo "" >> "$JS_FILE"
|
||||||
|
cat "$SHIM_DIR/asyncify-scheduler.js" >> "$JS_FILE"
|
||||||
|
echo "Injected asyncify-scheduler (WX_SCHEDULER=1 dual-glue variant)"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "scheduler disabled (set WX_SCHEDULER=1 for the dual-glue variant)"
|
||||||
|
fi
|
||||||
|
|
||||||
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
|
# --- 3b. embind dynCall fallback (dynCallLegacy -> wasmExports) ----------------
|
||||||
# embind's generic caller (getDynCaller) routes through dynCallLegacy, which only
|
# embind's generic caller (getDynCaller) routes through dynCallLegacy, which only
|
||||||
# reads Module["dynCall_<sig>"]. But the DYNCALLS=1 trampolines are wasm EXPORTS,
|
# reads Module["dynCall_<sig>"]. But the DYNCALLS=1 trampolines are wasm EXPORTS,
|
||||||
|
|
|
||||||
42
scripts/common/shims/asyncify-scheduler.js
Normal file
42
scripts/common/shims/asyncify-scheduler.js
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
// === AsyncifyScheduler (S0 scaffolding — observation-only) ===
|
||||||
|
// docs/features/async/17-mailbox-scheduler-plan.md · injected only on WX_SCHEDULER=1 builds.
|
||||||
|
//
|
||||||
|
// S0 contract: this file changes NO runtime behavior. It claims the namespace, the
|
||||||
|
// registry data structures, and the build marker so (a) the dual-glue build variant
|
||||||
|
// exists and can run the full suite, and (b) tests can detect which runtime they're on.
|
||||||
|
// S2 turns this into the sole owner of Asyncify.currData/state (doc 13 §1): the four
|
||||||
|
// hooks (handleSleep wrap, fiber-swap tracking, trampoline ownership, deferred drain)
|
||||||
|
// land there, gated on the N1 single-writer tripwire. Until then the legacy
|
||||||
|
// handlesleep.js shim (injected just above) stays authoritative.
|
||||||
|
if (typeof Asyncify !== "undefined" && !globalThis.__wxSchedulerInstalled) {
|
||||||
|
globalThis.__wxSchedulerInstalled = true;
|
||||||
|
Asyncify.__schedulerBuild = 1;
|
||||||
|
|
||||||
|
var AsyncifyScheduler = {
|
||||||
|
// ctx = { id, kind: 'main'|'modal'|'nested'|'coroutine'|'sleep',
|
||||||
|
// buffer, status: 'running'|'parked'|'ready', wakeReason, result }
|
||||||
|
contexts: new Map(),
|
||||||
|
readyQueue: [],
|
||||||
|
running: null,
|
||||||
|
transitionRunning: false,
|
||||||
|
trampolineRunning: false,
|
||||||
|
|
||||||
|
// S2 fills these in. They throw today so a premature caller is loud, not silent —
|
||||||
|
// nothing in an S0 build calls them.
|
||||||
|
park: function () { throw new Error("[wx-scheduler] park(): not implemented until S2"); },
|
||||||
|
resume: function () { throw new Error("[wx-scheduler] resume(): not implemented until S2"); },
|
||||||
|
drain: function () { throw new Error("[wx-scheduler] drain(): not implemented until S2"); },
|
||||||
|
|
||||||
|
state: function () {
|
||||||
|
return "[wx-scheduler] build=1 impl=S0-observation-only"
|
||||||
|
+ " contexts=" + this.contexts.size
|
||||||
|
+ " ready=" + this.readyQueue.length
|
||||||
|
+ " running=" + this.running
|
||||||
|
+ " transition=" + this.transitionRunning;
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.__wxScheduler = AsyncifyScheduler;
|
||||||
|
console.log("[wx-scheduler] scaffolding installed (S0, observation-only)");
|
||||||
|
}
|
||||||
|
// === End AsyncifyScheduler ===
|
||||||
160
tests/kicad/mailbox-ordering.spec.ts
Normal file
160
tests/kicad/mailbox-ordering.spec.ts
Normal file
|
|
@ -0,0 +1,160 @@
|
||||||
|
import type { Page } from "@playwright/test";
|
||||||
|
import { test, expect } from "./fixtures";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* N2 — message ordering under a parked open (RED, 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.
|
||||||
|
*
|
||||||
|
* The mailbox flips drop→deliver: 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).
|
||||||
|
*
|
||||||
|
* 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
|
||||||
|
* both delivery and order (drop-A-deliver-B leaves B targetless).
|
||||||
|
*/
|
||||||
|
|
||||||
|
const NEW_SEG = "fa2c0000-0000-0000-0000-00000000beef";
|
||||||
|
const B_TARGET = "60000000,60000000"; // where apply B moves A's segment (IU)
|
||||||
|
|
||||||
|
function smallBoard(): string {
|
||||||
|
return `(kicad_pcb
|
||||||
|
\t(version 20241229)
|
||||||
|
\t(generator "pcbnew")
|
||||||
|
\t(generator_version "9.0")
|
||||||
|
\t(general (thickness 1.6))
|
||||||
|
\t(paper "A4")
|
||||||
|
\t(layers
|
||||||
|
\t\t(0 "F.Cu" signal)
|
||||||
|
\t\t(2 "B.Cu" signal)
|
||||||
|
\t\t(25 "Edge.Cuts" user)
|
||||||
|
\t)
|
||||||
|
\t(setup)
|
||||||
|
\t(net 0 "")
|
||||||
|
\t(segment (start 10 10) (end 15 10) (width 0.2) (layer "F.Cu") (net 0) (uuid "fa2b0000-0000-0000-0000-000000000001"))
|
||||||
|
)`;
|
||||||
|
}
|
||||||
|
|
||||||
|
type FS = { mkdirTree(p: string): void; writeFile(p: string, d: string): void };
|
||||||
|
type Mod = {
|
||||||
|
kicadOpenFile(p: string): unknown;
|
||||||
|
kicadOpenFileBusy(): boolean;
|
||||||
|
kicadTestSetOpenPark(ms: number): void;
|
||||||
|
kicadCollabApplyItems(j: string): unknown;
|
||||||
|
kicadCollabGetPos(id: string): string;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function bootHarness(page: Page): Promise<void> {
|
||||||
|
await page.goto("/kicad/pcbnew-collab.html");
|
||||||
|
await expect(page.locator("#canvas")).toBeVisible({ timeout: 90000 });
|
||||||
|
await page.waitForFunction(() => !!window.wxElementRegistry, null, { timeout: 90000 });
|
||||||
|
await page.waitForFunction(
|
||||||
|
() => {
|
||||||
|
const m = (window as unknown as { Module?: Partial<Mod> }).Module;
|
||||||
|
return (
|
||||||
|
typeof m?.kicadOpenFile === "function" &&
|
||||||
|
typeof m?.kicadCollabApplyItems === "function" &&
|
||||||
|
typeof m?.kicadTestSetOpenPark === "function"
|
||||||
|
);
|
||||||
|
},
|
||||||
|
null,
|
||||||
|
{ timeout: 90000 },
|
||||||
|
);
|
||||||
|
await page.waitForFunction(
|
||||||
|
() =>
|
||||||
|
!!window.wxElementRegistry &&
|
||||||
|
window.wxElementRegistry
|
||||||
|
.findAll({ visible: true })
|
||||||
|
.some((e) => /Frame$/.test(e.typeName) || (e.name || "").endsWith("Frame")),
|
||||||
|
null,
|
||||||
|
{ timeout: 90000 },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ({
|
||||||
|
page,
|
||||||
|
testLogger,
|
||||||
|
}) => {
|
||||||
|
test.setTimeout(180000);
|
||||||
|
void testLogger;
|
||||||
|
await bootHarness(page);
|
||||||
|
|
||||||
|
const issued = await page.evaluate(async ({ newSeg, board }) => {
|
||||||
|
const w = window as unknown as { FS: FS; Module: Mod };
|
||||||
|
const dir = "/home/kicad/documents";
|
||||||
|
try {
|
||||||
|
w.FS.mkdirTree(dir);
|
||||||
|
} catch {
|
||||||
|
/* exists */
|
||||||
|
}
|
||||||
|
const path = `${dir}/n2.kicad_pcb`;
|
||||||
|
w.FS.writeFile(path, board);
|
||||||
|
// Deterministic park window on entry AND post-load (open_gate.h test lever)
|
||||||
|
w.Module.kicadTestSetOpenPark(1500);
|
||||||
|
w.Module.kicadOpenFile(path);
|
||||||
|
|
||||||
|
// Wait until the busy window is observably open, then issue A and B once.
|
||||||
|
const t0 = performance.now();
|
||||||
|
while (!w.Module.kicadOpenFileBusy() && performance.now() - t0 < 30000) {
|
||||||
|
await new Promise((r) => setTimeout(r, 5));
|
||||||
|
}
|
||||||
|
if (!w.Module.kicadOpenFileBusy()) return { inWindow: false };
|
||||||
|
|
||||||
|
const applyA = JSON.stringify({
|
||||||
|
added: [
|
||||||
|
{
|
||||||
|
sexpr: `(segment (start 50 50) (end 55 50) (width 0.2) (layer "F.Cu") (net 0) (uuid "${newSeg}"))`,
|
||||||
|
parent: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
changed: [],
|
||||||
|
removed: [],
|
||||||
|
});
|
||||||
|
const applyB = JSON.stringify({
|
||||||
|
added: [],
|
||||||
|
changed: [
|
||||||
|
{
|
||||||
|
sexpr: `(segment (start 60 60) (end 65 60) (width 0.2) (layer "F.Cu") (net 0) (uuid "${newSeg}"))`,
|
||||||
|
parent: null,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
removed: [],
|
||||||
|
});
|
||||||
|
w.Module.kicadCollabApplyItems(applyA);
|
||||||
|
w.Module.kicadCollabApplyItems(applyB);
|
||||||
|
|
||||||
|
// Wait for the open chain to settle.
|
||||||
|
const t1 = performance.now();
|
||||||
|
while (w.Module.kicadOpenFileBusy() && performance.now() - t1 < 120000) {
|
||||||
|
await new Promise((r) => setTimeout(r, 50));
|
||||||
|
}
|
||||||
|
w.Module.kicadTestSetOpenPark(0);
|
||||||
|
return { inWindow: true, settled: !w.Module.kicadOpenFileBusy() };
|
||||||
|
}, { newSeg: NEW_SEG, board: smallBoard() });
|
||||||
|
|
||||||
|
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.
|
||||||
|
await expect
|
||||||
|
.poll(
|
||||||
|
() =>
|
||||||
|
page.evaluate((id) => (window.Module as unknown as Mod).kicadCollabGetPos(id), NEW_SEG),
|
||||||
|
{ timeout: 10000, intervals: [200] },
|
||||||
|
)
|
||||||
|
.toBe(B_TARGET);
|
||||||
|
});
|
||||||
|
});
|
||||||
119
tests/kicad/utils/guard-beacons.ts
Normal file
119
tests/kicad/utils/guard-beacons.ts
Normal file
|
|
@ -0,0 +1,119 @@
|
||||||
|
// Guard-beacon extraction for the mailbox/scheduler migration
|
||||||
|
// (docs/features/async/17-mailbox-scheduler-plan.md, step S0.3).
|
||||||
|
//
|
||||||
|
// Every legacy anti-collision guard announces itself on the console when it fires.
|
||||||
|
// During the migration each superseded guard is kept as a TRIPWIRE: the mailbox is
|
||||||
|
// only trusted once the guard it replaces is provably silent across the suite.
|
||||||
|
// This module turns a TestLogger's consoleLogs into per-family counts so specs can
|
||||||
|
// assert `expectGuardsSilent(...)` at the step that claims a family.
|
||||||
|
//
|
||||||
|
// Rate-limiting caveat: [wx-asyncify] and [collab-fcontext] beacons print the first
|
||||||
|
// 10 occurrences, then every 100th, embedding "(occurrence N)". `linesSeen` is what
|
||||||
|
// reached the console; `estimatedTotal` recovers the true count from the highest
|
||||||
|
// occurrence number when present (else it equals linesSeen). Assertions on SILENCE
|
||||||
|
// are exact either way: zero fires = zero lines.
|
||||||
|
|
||||||
|
export interface BeaconFamilyCount {
|
||||||
|
linesSeen: number;
|
||||||
|
estimatedTotal: number;
|
||||||
|
samples: string[]; // first few matching lines, for the failure message
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface GuardBeaconCounts {
|
||||||
|
// wx timer interlock (timer.cpp): parked-dispatch retries
|
||||||
|
timerRetry: BeaconFamilyCount;
|
||||||
|
// wx dispatch interlock bookkeeping anomalies (evtloop.cpp)
|
||||||
|
dispatchAnomaly: BeaconFamilyCount;
|
||||||
|
// handlesleep.js shim: nested-park / wake-aliasing / stale-fiber refusals
|
||||||
|
wxAsyncify: BeaconFamilyCount;
|
||||||
|
// libcontext swap-layer refusals + hot-main beacons ([collab-fcontext])
|
||||||
|
libcontext: BeaconFamilyCount;
|
||||||
|
// open-settle gate giving up (open-flow.ts)
|
||||||
|
openSettleFailed: BeaconFamilyCount;
|
||||||
|
// scheduler build marker — identifies the dual-glue variant, not a guard
|
||||||
|
schedulerBuild: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
const FAMILY_PATTERNS: Record<
|
||||||
|
Exclude<keyof GuardBeaconCounts, 'schedulerBuild'>,
|
||||||
|
RegExp
|
||||||
|
> = {
|
||||||
|
timerRetry: /\[wx-timer\] retry storm/,
|
||||||
|
dispatchAnomaly: /\[wx-dispatch\] (ERASED|NEGATIVE)/,
|
||||||
|
wxAsyncify:
|
||||||
|
/\[wx-asyncify\] (concurrent-park|reentrant-state|aliased-wake-live|overlapped-wake|fiber-resume-refused)/,
|
||||||
|
libcontext:
|
||||||
|
/\[collab-fcontext\] (jump-refused|jump-refused-hot-main|hot-main-swap-out|jump-hot-into-main|jump-ghost|entry-orphaned)/,
|
||||||
|
openSettleFailed: /\[open\] load chain never settled/,
|
||||||
|
};
|
||||||
|
|
||||||
|
const OCCURRENCE_RE = /\(occurrence (\d+)\)/;
|
||||||
|
const SAMPLE_LIMIT = 3;
|
||||||
|
|
||||||
|
function emptyFamily(): BeaconFamilyCount {
|
||||||
|
return { linesSeen: 0, estimatedTotal: 0, samples: [] };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function countGuardBeacons(consoleLines: string[]): GuardBeaconCounts {
|
||||||
|
const counts: GuardBeaconCounts = {
|
||||||
|
timerRetry: emptyFamily(),
|
||||||
|
dispatchAnomaly: emptyFamily(),
|
||||||
|
wxAsyncify: emptyFamily(),
|
||||||
|
libcontext: emptyFamily(),
|
||||||
|
openSettleFailed: emptyFamily(),
|
||||||
|
schedulerBuild: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const line of consoleLines) {
|
||||||
|
if (line.includes('[wx-scheduler] scaffolding installed')) {
|
||||||
|
counts.schedulerBuild = true;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (const family of Object.keys(FAMILY_PATTERNS) as Array<
|
||||||
|
keyof typeof FAMILY_PATTERNS
|
||||||
|
>) {
|
||||||
|
if (!FAMILY_PATTERNS[family].test(line)) continue;
|
||||||
|
const fam = counts[family];
|
||||||
|
fam.linesSeen += 1;
|
||||||
|
const occ = OCCURRENCE_RE.exec(line);
|
||||||
|
const occurrenceTotal = occ ? parseInt(occ[1], 10) : fam.linesSeen;
|
||||||
|
fam.estimatedTotal = Math.max(fam.estimatedTotal, occurrenceTotal, fam.linesSeen);
|
||||||
|
if (fam.samples.length < SAMPLE_LIMIT) fam.samples.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return counts;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Last-seen fcsTotal/rootHotTotal from a __wxAsyncifyDump()/STATE line, if any.
|
||||||
|
// rootHotTotal must stay 0 post-v0.1.28 — the standing N8 assertion.
|
||||||
|
export function parseAsyncifyCounters(
|
||||||
|
consoleLines: string[]
|
||||||
|
): { fcsTotal: number; rootHotTotal: number } | null {
|
||||||
|
let result: { fcsTotal: number; rootHotTotal: number } | null = null;
|
||||||
|
for (const line of consoleLines) {
|
||||||
|
const m = /fcsTotal=(\d+) rootHotTotal=(\d+)/.exec(line);
|
||||||
|
if (m) result = { fcsTotal: parseInt(m[1], 10), rootHotTotal: parseInt(m[2], 10) };
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the named guard families never fired. Throws with the offending sample
|
||||||
|
// lines so the log points straight at the collision the mailbox failed to absorb.
|
||||||
|
export function expectGuardsSilent(
|
||||||
|
consoleLines: string[],
|
||||||
|
families: Array<Exclude<keyof GuardBeaconCounts, 'schedulerBuild'>>
|
||||||
|
): void {
|
||||||
|
const counts = countGuardBeacons(consoleLines);
|
||||||
|
const noisy = families
|
||||||
|
.map((f) => ({ family: f, count: counts[f] }))
|
||||||
|
.filter(({ count }) => count.linesSeen > 0);
|
||||||
|
if (noisy.length > 0) {
|
||||||
|
const detail = noisy
|
||||||
|
.map(
|
||||||
|
({ family, count }) =>
|
||||||
|
`${family}: ${count.estimatedTotal} fire(s)\n ${count.samples.join('\n ')}`
|
||||||
|
)
|
||||||
|
.join('\n ');
|
||||||
|
throw new Error(`guard beacons fired (expected silent):\n ${detail}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue