pcbjam/tests/asyncify/asyncify-races.spec.ts
Gergő Törcsvári d519ac89c0
design-b D-1 gate: battery green scheduler-only; startModal import scrub + work log
Gate results (single battery, scheduler-only): wx app battery +
asyncify + coroutine 363 passed / 3 skipped / 0 failed; full kicad
suite 138 passed / 30 skipped / 1 failed — the one failure is the
pre-existing local occ-probe glb case (predates D-1, unrelated).

Post-gate scrubs: vestigial 'startModal' removed from ASYNCIFY_IMPORTS
(tests/apps/Makefile.wasm) and env.startModal from
scripts/common/asyncify-imports.txt (the import no longer exists in any
wasm; both lists are boundary supersets so behavior is identical).
Stale comment pointers to the deleted legacy modal machinery updated.
Doc 20: D-1 work log added, status flipped to IN PROGRESS.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TEHGiiXMShNXbBr7gSJ7iz
2026-08-10 10:14:15 +02:00

179 lines
6.7 KiB
TypeScript

import { test, expect, tryLoadApp } from '../e2e/utils/fixtures';
// Red-green specs for the Asyncify race-condition harness
// (tests/apps/standalone/asyncify-races/races_test.cpp — see docs/features/async/).
//
// GREEN-target tests assert the desired end state (clean pass, clean console).
// While a fix is missing they FAIL — that failing run is the recorded "red".
// (The shim-ablation builds and their pins were retired at doc 20 D-1 together
// with the legacy runtime they exercised; the scheduler shim is the only
// runtime, and the battery below runs every scenario against it.)
const BATTERY = [
'post_park_fiber_swap',
'sleep_inside_fiber_inside_modal',
'out_of_order_sleep_resolution',
'long_parked_sleep_clobbered_by_swap',
];
const CRASH_SIGNATURES = [
'index out of bounds',
'indirect call to null',
'invalid state',
'unwind',
// assertion-free builds surface a clobbered doRewind as a TypeError
'is not a function',
];
function findSummary(logs: string[]) {
return logs.find((log) => log.includes('[ASYNCIFY_RACES] SUMMARY'));
}
function parseSummary(summary: string) {
const match = summary.match(/total=(\d+)\s+passed=(\d+)\s+failed=(\d+)/);
expect(match, 'summary line should be parseable').not.toBeNull();
return { total: Number(match![1]), passed: Number(match![2]), failed: Number(match![3]) };
}
function crashLines(testLogger: { consoleLogs: string[]; errors: string[] }) {
const all = [...testLogger.consoleLogs, ...testLogger.errors];
return all.filter(
(line) =>
CRASH_SIGNATURES.some((sig) => line.toLowerCase().includes(sig)) &&
// The harness's own meta-output mentions these words legitimately.
!line.includes('[ASYNCIFY_RACES]')
);
}
function realErrors(testLogger: { errors: string[] }) {
return testLogger.errors.filter((e) => !e.includes('favicon'));
}
test.describe('Asyncify races — green targets (full shims)', () => {
test('battery: all chained scenarios pass with a clean console', async ({
page,
testLogger,
}) => {
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 a final SUMMARY line (a missing one means a wedge/hang)',
})
.not.toBeNull();
const { total, passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
const failLogs = testLogger.consoleLogs.filter((l) => l.includes('[ASYNCIFY_RACES] FAIL '));
const passLogs = testLogger.consoleLogs.filter((l) => l.includes('[ASYNCIFY_RACES] PASS '));
expect(total).toBe(BATTERY.length);
expect(passed).toBe(BATTERY.length);
expect(failed).toBe(0);
expect(failLogs, `FAIL lines: ${failLogs.join(' || ')}`).toHaveLength(0);
expect(passLogs).toHaveLength(BATTERY.length);
for (const name of BATTERY) {
expect.soft(
testLogger.consoleLogs.some((l) => l.includes(`[ASYNCIFY_RACES] PASS ${name}`)),
`scenario ${name} should PASS`
).toBe(true);
}
expect(crashLines(testLogger), 'no crash signatures in console').toHaveLength(0);
expect(realErrors(testLogger), 'no page errors').toHaveLength(0);
});
test('modal_in_modal_in_modal: three nested ShowModals resolve LIFO', async ({
page,
testLogger,
}) => {
// Historical red: the pre-scheduler wx dialog.cpp kept the modal resolver
// in a single slot (Module._endModal), so with three nested modals the
// middle EndModal resolved nothing and its ShowModal parked forever.
// Green since the LIFO resolver semantics, now the scheduler wait
// registry's per-kind stacks (doc 17 S4; legacy machinery deleted at
// doc 20 D-1).
await page.goto('/standalone/asyncify-races/races_test.html#only=modal_in_modal_in_modal');
await tryLoadApp(page, 30000);
await expect
.poll(() => findSummary(testLogger.consoleLogs) ?? null, {
timeout: 45000,
message: 'triple modal should complete (middle EndModal must not be lost)',
})
.not.toBeNull();
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
expect(passed).toBe(1);
expect(failed).toBe(0);
expect(crashLines(testLogger), 'no crash signatures in console').toHaveLength(0);
});
test('wakeup_during_transition: modal teardown over parked sleeps stays clean', async ({
page,
testLogger,
}) => {
await page.goto('/standalone/asyncify-races/races_test.html#only=wakeup_during_transition');
await tryLoadApp(page, 30000);
await expect
.poll(() => findSummary(testLogger.consoleLogs) ?? null, { timeout: 45000 })
.not.toBeNull();
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
expect(passed).toBe(1);
expect(failed).toBe(0);
expect(crashLines(testLogger), 'no crash signatures in console').toHaveLength(0);
expect(realErrors(testLogger), 'no page errors').toHaveLength(0);
});
test('nested_quasi_modal_pump_error: pump rejection must not leak the parked DoRun', async ({
page,
testLogger,
}) => {
await page.goto(
'/standalone/asyncify-races/races_test.html#only=nested_quasi_modal_pump_error'
);
await tryLoadApp(page, 30000);
await expect
.poll(() => findSummary(testLogger.consoleLogs) ?? null, {
timeout: 45000,
message:
'nested loop must exit after a pump error (silent stall = the c27fe8bf bug, fixed in wx evtloop.cpp)',
})
.not.toBeNull();
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
expect(passed).toBe(1);
expect(failed).toBe(0);
});
test('sleep-park mode: park throw must not escape as an unhandled "unwind" rejection', async ({
page,
testLogger,
}) => {
await page.goto('/standalone/asyncify-races/races_test.html#mode=sleep-park');
await tryLoadApp(page, 30000);
await expect
.poll(() => findSummary(testLogger.consoleLogs) ?? null, { timeout: 45000 })
.not.toBeNull();
const { passed, failed } = parseSummary(findSummary(testLogger.consoleLogs)!);
expect(passed).toBe(1);
expect(failed).toBe(0);
const unwindLeaks = [...testLogger.errors, ...testLogger.consoleLogs].filter(
(l) =>
l.toLowerCase().includes('unwind') &&
!l.includes('[ASYNCIFY_RACES]') &&
// console *log* lines about unwind from our own shims are fine; errors are not
(testLogger.errors.includes(l) || l.toLowerCase().includes('uncaught'))
);
expect(unwindLeaks, `unwind escaped the park: ${unwindLeaks.join(' || ')}`).toHaveLength(0);
});
});