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
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