Study: Artery Boot Detection & Readiness Telemetry#
Date: 2026-02-21
Context: The test-line.mjs brush test runs before AC finishes booting. The notepat tests use waitForBoot() but it may not work correctly. This study maps the full boot signal graph to build reliable piece-readiness detection.
The Problem#
When an artery test calls client.jump('line'), the browser navigates to a new URL. The entire AC runtime re-boots from scratch — boot.mjs loads bios.mjs loads the piece. Tests that send pointer events before this completes get silently swallowed.
Current tests handle this two ways:
- Hardcoded
sleep()—test-line.mjsusessleep(3000)after each jump waitForBoot()— pollswindow.currentPiecewhich is broken (see below)
Neither is reliable.
The Boot Signal Graph#
Here's the complete timeline of what happens when AC loads a piece, with every observable window.* signal annotated:
NAVIGATION (client.jump sets window.location.href)
│
├── boot.mjs starts
│ ├── window.acBOOT_START_TIME = performance.now()
│ ├── window._bootTimings = []
│ ├── window.acSTARTING_PIECE = "prompt" (or from URL)
│ ├── window.acHIDE_BOOT_LOG = hideBootLog ← [SIGNAL A] boot.mjs loaded
│ ├── window.acBOOT_LOG = bootLog
│ ├── window.acAuthTiming = { bootStart, ... }
│ │
│ └── bios.mjs boot() begins
│ ├── let currentPiece = null ← LOCAL variable, NOT on window
│ ├── window.acDISABLE_HUD_LABEL_CACHE = true
│ ├── window.acDISK_SEND = function(...)
│ ├── window.acPieceState = null
│ ├── window.acGetState = () => (...)
│ │
│ ├── Disk worker/module loads
│ │ └── window.acSEND = send ← [SIGNAL B] worker connected
│ │
│ ├── Piece module loads
│ │ └── currentPiece = content.path ← LOCAL, invisible to tests
│ │
│ ├── "disk-loaded-and-booted" message
│ │ ├── window.preloaded = true ← [SIGNAL C] piece loaded
│ │ ├── postMessage({ type: "ready" })
│ │ └── setTimeout(500ms) → acHIDE_BOOT_LOG()
│ │
│ └── "piece-paint-ready" message
│ └── window.acHIDE_BOOT_LOG() ← [SIGNAL D] first paint done
│
FOR NOPAINT/BRUSH PIECES:
│
├── Piece boot() calls nopaint_boot()
│ ├── system.painting must exist
│ ├── system.nopaint.buffer created
│ └── system.nopaint initialized ← [SIGNAL E] brush ready
│
FOR AUDIO PIECES:
│
└── User interaction triggers activateSound
└── window.audioWorkletReady = true ← [SIGNAL F] audio ready
The waitForBoot() Bug#
File: artery/artery.mjs:427-461
async waitForBoot(timeoutMs = 15000) {
while (Date.now() - startTime < timeoutMs) {
const bootStatus = await this.eval(`({
hasBios: typeof window.currentPiece !== 'undefined', // ← WRONG
currentPiece: window.currentPiece || null, // ← ALWAYS null
...
})`);
if (bootStatus.currentPiece) { // ← NEVER true
return true;
}
await new Promise(r => setTimeout(r, 100));
}
darkLog('⚠️ Boot timeout - proceeding anyway'); // ← ALWAYS hits this
return false;
}
Why it's broken: currentPiece is declared as let currentPiece = null inside the boot() function closure in bios.mjs:804. It's never assigned to window.currentPiece. The check typeof window.currentPiece !== 'undefined' evaluates to false (it IS undefined), and bootStatus.currentPiece is always null.
Result: waitForBoot() always times out after 15 seconds, then proceeds anyway. Tests that call it simply wait 15 seconds for nothing.
Reliable Readiness Signals#
Signal A: window.acHIDE_BOOT_LOG (boot.mjs loaded)#
- When: Very early — boot.mjs top-level code, before bios even starts
- Checks:
typeof window.acHIDE_BOOT_LOG === 'function' - Useful for: Confirming the page loaded at all
- Not sufficient for: Piece readiness
Signal B: window.acSEND (disk worker connected)#
- When: After bios creates the disk worker/module and connects
- Checks:
typeof window.acSEND === 'function' - Useful for: Confirming the runtime is operational
- Not sufficient for: Knowing WHICH piece loaded
Signal C: window.preloaded (piece loaded and booted)#
- When: After
"disk-loaded-and-booted"message from the disk - Checks:
window.preloaded === true - Useful for: Confirming the current piece finished its
boot()function - Caveat: Reset to
falseon every piece navigation (bios.mjs:12767)
Signal D: Boot canvas hidden (first paint)#
- When: After
"piece-paint-ready"or 500ms fallback after disk-loaded - Checks: Boot canvas DOM element visibility (indirect)
- Useful for: Confirming the piece has painted at least one frame
Signal E: Nopaint ready (brush pieces)#
- When: After
nopaint_boot()completes in the piece'sboot()function - Checks:
window.acSYSTEM?.nopaint?.bufferexists,window.acSYSTEM?.painting?.pixels?.length > 0 - Caveat: Requires
window.acSYSTEMto be exposed — need to verify this exists
Signal F: window.audioWorkletReady (audio pieces)#
- When: After speaker AudioWorklet connects following user interaction
- Checks:
window.audioWorkletReady === true - Only relevant for: Audio-dependent tests
Full Cross-Examination: All 24 Artery Tests#
Audit Summary#
Every test-*.mjs file was examined for its boot/readiness strategy. Zero tests use window.preloaded. Most rely on hardcoded sleep() or the broken waitForBoot().
| Test | Wait Strategy | Timeout | Reconnects? | activateAudio? | Verdict |
|---|---|---|---|---|---|
test-line.mjs |
waitForPiece() |
10s poll | No | No | FIXED — now uses signal-based polling |
test-notepat-quick.mjs |
sleep(2000) |
2s fixed | No | Yes | FRAGILE — hardcoded sleep, then broken waitForBoot via activateAudio |
test-notepat.mjs |
sleep(2000) + reconnect + activateAudio() |
2s + 15s timeout | Yes | Yes | BROKEN — waitForBoot always times out (15s wasted) |
test-hiphop.mjs |
sleep(2000) + reconnect + activateAudio() |
2s + 15s timeout | Yes | Yes | BROKEN — same 15s waste |
test-trapwaltz.mjs |
sleep(2000) + reconnect + activateAudio() |
2s + 15s timeout | Yes | Yes | BROKEN — same 15s waste |
test-notepat-bach-prelude.mjs |
sleep(2000) + reconnect + activateAudio() |
2s + 15s timeout | Yes | Yes | BROKEN — same pattern |
test-notepat-stability.mjs |
sleep(2000) + activateAudio() |
2s + 15s timeout | No | Yes | BROKEN — same pattern |
test-notepat-latency.mjs |
sleep(2000) + reconnect + activateAudio() |
2s + 15s timeout | Yes | Yes | BROKEN — same pattern |
test-metronome.mjs |
sleep(1000) + click() |
1s fixed | No | No (manual click) | FRAGILE — short sleep, manual audio via click |
test-weather.mjs |
sleep(2000) |
2s fixed | No | No | FRAGILE — hardcoded |
test-prompt-curtain.mjs |
cdp.waitForReady(15000) |
15s poll | No | No | OK — polls readyState+bios, sufficient for prompt |
test-prompt-interaction.mjs |
None | N/A | No | No | RISKY — no wait at all, assumes already loaded |
test-jump.mjs |
sleep(1000) |
1s fixed | No | No | FRAGILE — short hardcoded sleep |
test-cards.mjs |
sleep(1000) |
1s fixed | No | No | FRAGILE — short hardcoded sleep |
test-1v1-split.mjs |
sleep(2000) |
2s fixed | No | No | FRAGILE — hardcoded |
test-1v1-multiplayer.mjs |
sleep(4000) |
4s fixed | No | No | FRAGILE — long but still arbitrary |
test-1v1-interactive.mjs |
Custom Camdoll.isReady() |
Variable | No | No | OK — has its own readiness check |
test-1v1-udp.mjs |
N/A (diagnostic) | N/A | No | No | N/A — doesn't navigate |
test-kidlisp.mjs |
Custom waitForReady() |
10s poll | No | No | OK — polls page readiness |
test-kidlisp-access.mjs |
None | N/A | No | No | N/A — Monaco editor diagnostic |
test-panel-open.mjs |
None | N/A | No | No | N/A — only tests panel open/close |
test-perf-report.mjs |
None | N/A | No | No | N/A — reads perf data, no navigation |
test-module-loader.mjs |
sleep(500) |
500ms fixed | No | No | FRAGILE — very short |
test-news-youtube.mjs |
None | N/A | No | No | N/A — YouTube embed diagnostic |
test-playlist-memory.mjs |
cdp.waitForReady(15000) |
15s poll | No | No | OK — polls readyState+bios |
Verdict Categories#
FIXED (1): test-line.mjs — now uses waitForPiece() with window.preloaded polling.
BROKEN (6): All notepat/music tests call activateAudio() which calls the broken waitForBoot(). This always times out at 15 seconds because window.currentPiece doesn't exist. The fix to waitForBoot() (now polling window.preloaded) will automatically fix all 6.
FRAGILE (7): Use hardcoded sleep() with arbitrary durations. Work on fast systems, fail on slow ones. Should migrate to waitForPiece().
OK (4): Use proper polling (waitForReady, custom readiness checks, or waitForPiece).
N/A (6): Diagnostic/non-navigation tests that don't need boot waiting.
The Reconnect Pattern#
5 tests do client.close() + client.connect() after jump(). This is because CDP's WebSocket connection can break when window.location.href triggers a full page reload. The CDP target changes — the old WebSocket is dead.
Tests that reconnect:
test-notepat.mjstest-hiphop.mjstest-trapwaltz.mjstest-notepat-bach-prelude.mjstest-notepat-latency.mjs
Tests that don't reconnect may work because Artery's connect() auto-finds the new target, or because the CDP connection survives SPA-style navigations in certain environments.
Impact of the waitForBoot() Fix#
The fix changes waitForBoot() to poll window.preloaded === true + window.acSEND instead of the nonexistent window.currentPiece. This means:
- 6 BROKEN tests will now boot-detect correctly (fast return instead of 15s timeout)
- Typical boot time should be ~1-3 seconds, saving 12-14 seconds per test run
activateAudio()callswaitForBoot()internally, so all audio tests benefit automatically
Applied Fix (2026-02-21)#
1. Fixed waitForBoot() in artery/artery.mjs#
Changed from polling the nonexistent window.currentPiece to polling real signals:
async waitForBoot(timeoutMs = 15000) {
while (Date.now() - startTime < timeoutMs) {
const bootStatus = await this.eval(`({
preloaded: window.preloaded === true, // ← Signal C: piece booted
hasSend: typeof window.acSEND === 'function', // ← Signal B: worker ready
hasActivateSound: typeof window.activateSound === 'function',
canvasExists: !!document.querySelector('canvas'),
locationPath: window.location?.pathname || '',
hasAudioContext: !!window.audioContext
})`);
if (bootStatus.preloaded && bootStatus.hasSend) { // ← Real signals
return true;
}
await new Promise(r => setTimeout(r, 100));
}
return false;
}
This automatically fixes the 6 BROKEN audio tests since activateAudio() calls waitForBoot() internally.
2. Added waitForPiece() to artery/artery.mjs#
New method for targeted piece-readiness detection:
async waitForPiece(expectedPath = null, timeoutMs = 10000) {
while (Date.now() - startTime < timeoutMs) {
const status = await this.eval(`({
preloaded: window.preloaded === true,
hasSend: typeof window.acSEND === 'function',
path: window.location?.pathname || '',
hasCanvas: !!document.querySelector('canvas'),
})`);
const pathOk = !expectedPath || status.path?.includes(expectedPath);
if (status.preloaded && status.hasSend && pathOk) {
return { ready: true, ...status };
}
await new Promise(r => setTimeout(r, 150));
}
return { ready: false, timedOut: true };
}
3. Updated test-line.mjs#
Replaced sleep(3000) + sleep(2000) with signal-based waiting:
await client.jump('new~128');
const newResult = await client.waitForPiece('new', 10000); // Polls, returns fast
await client.jump('line');
const lineResult = await client.waitForPiece('line', 10000); // Polls, returns fast
Future: waitForNopaint() (brush pieces)#
Not yet implemented — requires verifying window.acSYSTEM is exposed at runtime:
async waitForNopaint(timeoutMs = 8000) {
// Polls for system.painting + system.nopaint.buffer
// Would provide brush-specific readiness
}
The window.preloaded Lifecycle#
This is the most useful signal. Here's its full lifecycle:
1. boot() starts → window.preloaded is undefined (fresh page)
2. Piece navigated away → window.preloaded = false (bios.mjs:12767)
3. New piece loads + boots → window.preloaded = true (bios.mjs:16642)
4. Next navigation → window.preloaded = false (reset again)
Key insight: Since client.jump() sets window.location.href, the entire page reloads. So window.preloaded starts as undefined, then becomes true once the piece is fully loaded. Polling for window.preloaded === true is the correct general-purpose check.
Recommended Test Pattern for Brush Tests#
// 1. Navigate
await client.jump('new~128');
// 2. Wait for AC to fully boot with the new painting
const bootResult = await client.waitForPiece('new', 10000);
if (!bootResult.ready) throw new Error('Boot timeout on new~128');
// 3. Navigate to brush
await client.jump('line');
// 4. Wait for piece + nopaint to be ready
const pieceResult = await client.waitForPiece('line', 8000);
if (!pieceResult.ready) throw new Error('Piece timeout on line');
// 5. (Optional) Wait specifically for nopaint painting buffer
const nopaintResult = await client.waitForNopaint(5000);
if (!nopaintResult.ready) console.warn('Nopaint not fully initialized');
// 6. Now safe to draw
await drawStroke(client, points);
Open Questions#
-
Is
window.acSYSTEMexposed? Need to verify at runtime whether the system object is accessible fromwindowscope. If not, we may need to expose it for testing. -
Does
jump()(location.href change) cause a full page reload? If so,window.preloadedstarts fresh each time, which is clean. If it's a SPA-style navigation, the signal lifecycle may be different. -
Should we add a dedicated
window.acPIECE_READYsignal? A simple boolean set right aftercurrentPieceis assigned would solve the entire detection problem cleanly. -
Race condition on
window.preloaded: If the piece loads extremely fast (cached), couldpreloadedbecometruebefore our first poll? In theory yes, but that's fine — we'd catch it on the first poll iteration.
Files Referenced#
| File | Key Lines | Signal |
|---|---|---|
boot.mjs |
66, 172, 348, 351, 375 | acBOOT_START_TIME, _bootTimings, acHIDE_BOOT_LOG, acBOOT_LOG, acAuthTiming |
bios.mjs |
651-654, 700-711, 804, 4283-4294, 12717, 12767, 16637-16677 | acDISK_SEND, acSEND, acGetState, currentPiece (local), preloaded |
artery.mjs |
427-490 | waitForBoot() (FIXED), waitForPiece() (NEW) |
cdp.mjs |
237-252 | waitForReady() (DOM-only) |
nopaint.mjs |
67-91 | nopaint_boot() initialization |
Change Log#
- 2026-02-21: Initial study. Fixed
waitForBoot()to usewindow.preloaded. AddedwaitForPiece(). Updatedtest-line.mjs. Full 24-test audit.