Files
site/tools/harness.html
T
teahubxyzandClaude Fable 5.1 1aeebac6f2 harness: a resume mode (autoplay blocked); docs for the state machine and hooks
tools/harness.sh resume seeds IndexedDB with a station and one rendered
track, resumes it with no user gesture and expects the TAP TO PLAY wait.
IndexedDB needs http, so the page is served from a temp folder; headless
Chrome's virtual time doesn't wait for IndexedDB, so this mode is driven in
real time over the DevTools protocol (tools/harness-cdp.py, stdlib only).
The loop harness now prints state.phase too.

ARCHITECTURE.md describes the machine as implemented; HANDOFF.md lists what
was found and left alone; STORAGE.md gains db.get and the tapes store (the
code already had both); README points at the new files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K6897PWcUEwHiqZMcVnNF6
2026-10-21 06:01:48 +00:00

36 lines
3.0 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<title>harness</title>
<p>This file is a fragment: tools/harness.sh appends everything from the marker below to index.html and runs it.</p>
<!-- harness -->
<script>
try {
let renderDelay = 50, n = 0;
venice = async function (path, body, { raw = false } = {}) {
if (path.includes("chat")) return { choices: [{ message: { content: JSON.stringify({ title: "T" + (++n), tags: "lo-fi, piano, instrumental", lyrics: "[verse]\nla\n[chorus]\nlo", bpm: 80, key: "C major", energy: 0.4, seconds: 30, meter: "4/4", station: "Test FM", line: "hi" }) } }] };
if (path === "/render") { await new Promise((r) => setTimeout(r, renderDelay)); const b = wavBlob(new Int16Array(8000 * 2), 8000); return new Response(b, { headers: { "content-type": "audio/wav", "x-cost": "0.001", "x-renderer": "test" } }); }
throw new Error("unexpected " + path);
};
const log = []; const dd = document.createElement("pre"); dd.id = "diag"; document.body.append(dd);
const L = (m) => { log.push(m); dd.textContent = log.join("\n"); };
const S = (tag) => L(tag + "\n phase=" + state.phase + " src=" + String(state.audio.src).slice(5, 14) + " paused=" + state.audio.paused + " playing=" + state.playing + " holding=" + !!state.holding + " cur=" + (state.current && state.current.spec.title) + " next=" + (state.nextJob && state.nextJob.spec.title) + " nextblob=" + !!(state.nextJob && state.nextJob.blob) + " crackle=" + crackle.on + " loop=" + state.audio.loop);
window.addEventListener("error", (e) => L("ERR " + e.message + " @" + e.lineno));
window.addEventListener("unhandledrejection", (e) => L("REJ " + (e.reason && (e.reason.stack || e.reason)).toString().slice(0, 200)));
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const ended = () => { state.audio.dispatchEvent(new Event("ended")); };
(async () => {
document.dispatchEvent(new Event("keydown")); await sleep(300);
openStation("test seed"); await sleep(1500); S("after first expect phase=playing cur=T1 playing, next=T2 nextblob=true");
ended(); await sleep(600); S("after ended expect cur=T2 playing, holding=false");
renderDelay = 3000; state.nextJob = null; cookNext(); await sleep(300); S("slow cook started expect next cooking, nextblob=false");
ended(); await sleep(600); S("ended, nothing ready expect phase=holding holding=true loop=true");
await sleep(3500); S("track landed expect holding=true loop=false (queued behind the crackle)");
ended(); await sleep(600); S("crackle pass ended expect phase=playing cur=that track playing, holding=false");
renderDelay = 50; document.querySelector("#play").click(); await sleep(600); S("after pause expect paused=true crackle=true");
document.querySelector("#play").click(); await sleep(300); S("after resume expect paused=false crackle=false");
document.querySelector("#skip").click(); await sleep(1500); S("after skip expect a new cur");
L("END");
})();
} catch (e) { const p = document.createElement("pre"); p.id = "diag"; p.textContent = "TOPERR " + e.message; document.body.append(p); }
</script>