mailbox S2: scheduler core — deferred wakes + N1 single-writer tripwire
asyncify-scheduler.js REPLACES handlesleep.js on WX_SCHEDULER=1 builds (injector either-or): ports capture/restore, fiber consume-once/ quarantine guard, wake-window flags, recorder, trampoline heal — and adds deferred wakes (a wake mid-transition queues and drains from a clean macrotask) plus the N1 currData accessor (pure-JS writes need scheduler authorization; strict mode throws; meta-tested). Gates: races 9/9 with NO legacy shim (subsumption), coroutine 39/39, wx-chromium 30/30, kicad trio 3/3 on the C-lane build. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TfxKn5utcntBSnxz4ZnYKs
This commit is contained in:
parent
af1eb5fc58
commit
603e260885
4 changed files with 584 additions and 97 deletions
122
tests/asyncify/asyncify-races-scheduler.spec.ts
Normal file
122
tests/asyncify/asyncify-races-scheduler.spec.ts
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
import { test, expect, tryLoadApp } from '../e2e/utils/fixtures';
|
||||
|
||||
/**
|
||||
* S2 scheduler-core gates (docs/features/async/17 §3d N1/N4, §4 S2).
|
||||
* Runs against the races harness built with WX_SCHEDULER=1 (scheduler-only
|
||||
* glue — the legacy handlesleep shim is NOT injected on scheduler builds).
|
||||
* Both tests self-skip on legacy glue, so the file is safe in either variant.
|
||||
*
|
||||
* N1 — single-writer tripwire: Asyncify.currData is an accessor; a pure-JS
|
||||
* write without scheduler authorization beacons (and throws in strict mode).
|
||||
* The meta-test INTRODUCES a stray writer and expects the tripwire to fire —
|
||||
* proving the alarm works, not merely that nobody tripped it.
|
||||
*
|
||||
* N4 — wake-never-rewinds-mid-transition: across the races battery (which
|
||||
* stages overlapping parks, nested modals, out-of-order wakes) the
|
||||
* scheduler's books must be coherent at settle: no queued wake left, every
|
||||
* deferral drained, zero unplanned strays, battery green.
|
||||
*/
|
||||
|
||||
type SchedulerState = {
|
||||
state(): string;
|
||||
strayWrites: number;
|
||||
strictStrays: boolean;
|
||||
readyWakes: unknown[];
|
||||
deferredWakes: number;
|
||||
drainedWakes: number;
|
||||
};
|
||||
|
||||
function findSummary(logs: string[]) {
|
||||
return logs.find((log) => log.includes('[ASYNCIFY_RACES] SUMMARY'));
|
||||
}
|
||||
|
||||
async function bootAndSettle(
|
||||
page: import('@playwright/test').Page,
|
||||
testLogger: { consoleLogs: string[] },
|
||||
): Promise<boolean> {
|
||||
await page.goto('/standalone/asyncify-races/races_test.html');
|
||||
const loaded = await tryLoadApp(page, 30000);
|
||||
expect(loaded, 'races harness should load').toBe(true);
|
||||
await expect
|
||||
.poll(() => findSummary(testLogger.consoleLogs) ?? null, {
|
||||
timeout: 60000,
|
||||
message: 'battery should emit its SUMMARY line',
|
||||
})
|
||||
.not.toBeNull();
|
||||
return page.evaluate(
|
||||
() => !!(globalThis as unknown as { __wxScheduler?: unknown }).__wxScheduler,
|
||||
);
|
||||
}
|
||||
|
||||
test.describe('S2 scheduler core (WX_SCHEDULER=1 glue)', () => {
|
||||
test('N4: battery leaves coherent books — wakes drained, no strays, battery green', async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
test.setTimeout(180000);
|
||||
const scheduler = await bootAndSettle(page, testLogger);
|
||||
test.skip(!scheduler, 'legacy glue — scheduler core absent');
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
() =>
|
||||
page.evaluate(
|
||||
() =>
|
||||
(globalThis as unknown as { __wxScheduler: SchedulerState }).__wxScheduler
|
||||
.readyWakes.length,
|
||||
),
|
||||
{ timeout: 30000, intervals: [250] },
|
||||
)
|
||||
.toBe(0);
|
||||
|
||||
const books = await page.evaluate(() => {
|
||||
const S = (globalThis as unknown as { __wxScheduler: SchedulerState }).__wxScheduler;
|
||||
return {
|
||||
ready: S.readyWakes.length,
|
||||
deferred: S.deferredWakes,
|
||||
drained: S.drainedWakes,
|
||||
strays: S.strayWrites,
|
||||
state: S.state(),
|
||||
};
|
||||
});
|
||||
console.log(`[TEST] scheduler books: ${books.state}`);
|
||||
|
||||
expect(books.ready, 'no wake left queued after settle').toBe(0);
|
||||
expect(books.drained, 'every deferred wake was drained').toBe(books.deferred);
|
||||
expect(books.strays, 'no stray currData writes during the battery').toBe(0);
|
||||
const fails = testLogger.consoleLogs.filter((l) => l.includes('[ASYNCIFY_RACES] FAIL '));
|
||||
expect(fails, 'battery green under the scheduler core').toEqual([]);
|
||||
});
|
||||
|
||||
test('N1 meta: an introduced stray currData write trips the alarm', async ({
|
||||
page,
|
||||
testLogger,
|
||||
}) => {
|
||||
test.setTimeout(180000);
|
||||
const scheduler = await bootAndSettle(page, testLogger);
|
||||
test.skip(!scheduler, 'legacy glue — scheduler core absent');
|
||||
|
||||
const result = await page.evaluate(() => {
|
||||
const S = (globalThis as unknown as { __wxScheduler: SchedulerState }).__wxScheduler;
|
||||
const A = (globalThis as unknown as { Asyncify: { currData: number | null } }).Asyncify;
|
||||
const before = S.strayWrites;
|
||||
const saved = A.currData;
|
||||
A.currData = saved; // value-preserving, still a stray WRITE
|
||||
const counted = S.strayWrites === before + 1;
|
||||
S.strictStrays = true;
|
||||
let threw = false;
|
||||
try {
|
||||
A.currData = saved;
|
||||
} catch {
|
||||
threw = true;
|
||||
}
|
||||
S.strictStrays = false;
|
||||
return { counted, threw };
|
||||
});
|
||||
|
||||
expect(result.counted, 'stray write was counted').toBe(true);
|
||||
expect(result.threw, 'strict mode throws on stray write').toBe(true);
|
||||
const beacons = testLogger.consoleLogs.filter((l) => l.includes('stray-currdata-write'));
|
||||
expect(beacons.length, 'stray write beaconed to the console').toBeGreaterThan(0);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue