Files
site/tools/harness-resume.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

41 lines
3.8 KiB
HTML

<!doctype html>
<meta charset="utf-8">
<title>harness: resume</title>
<p>This file is a fragment: <code>tools/harness.sh resume</code> appends everything from the marker below to index.html, serves
the folder over http (IndexedDB does not work from file:// in headless Chrome), drives it in real time over the
DevTools protocol (tools/harness-cdp.py) and runs it WITHOUT autoplay allowed. It seeds the library with one station
and one rendered track and resumes the station with no user gesture, the way an installed app comes back to a station
link. Expected: the track is loaded but autoplay is refused, so the player waits with the cover saying TAP TO PLAY,
and the next track cooks behind it. The other half — the tap starting it — needs a real user activation, which
headless Chrome cannot fake; it is documented in ARCHITECTURE.md, not asserted here.</p>
<!-- harness -->
<script>
try {
let n = 0;
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
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: "Resume FM", line: "hi" }) } }] };
if (path === "/render") { await sleep(50); 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 + " needTap=" + !!state.needTap + " tap=" + document.querySelector("#cover").classList.contains("tap") + " veil=" + JSON.stringify(document.querySelector("#cover .veil").textContent) + " paused=" + state.audio.paused + " playing=" + state.playing + " holding=" + !!state.holding + " cur=" + (state.current && state.current.spec.title) + " curblob=" + !!(state.current && state.current.blob) + " next=" + (state.nextJob && state.nextJob.spec.title) + " nextblob=" + !!(state.nextJob && state.nextJob.blob) + " crackle=" + crackle.on);
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)));
(async () => {
const sid = "s-1700000000000-1", made = 1700000000000, seed = "resume seed";
const spec = { title: "R1", tags: "lo-fi, piano, instrumental", lyrics: "", bpm: 80, key: "C major", energy: 0.4, seconds: 30, meter: "4/4", line: "hi", seed, stationId: sid, made, id: made + "-" + hash32("R1") };
await db.put({ id: spec.id, spec, blob: wavBlob(new Int16Array(8000 * 2), 8000), made, kept: false, plays: 0 });
await db.put({ id: sid, seed, name: "Resume FM", taste: { like: [], avoid: [], notes: [] }, history: [spec], made, last: made, played: 1 }, "stations");
await sleep(600); // past the 400 ms room-tone attempt, which autoplay refuses too
S("before resume expect phase=idle cur=null paused=true needTap=false");
await openStation(seed, { id: sid, name: "Resume FM", history: [spec], played: 1 }); // no gesture: a station link opening
await sleep(1500);
S("resume, no gesture expect phase=playing needTap=true tap=true veil=\"TAP TO PLAY\" paused=true cur=R1 curblob=true next=T1 nextblob=true");
L("(the tap that starts it needs a real user activation; not asserted here)");
L("END");
})();
} catch (e) { const p = document.createElement("pre"); p.id = "diag"; p.textContent = "TOPERR " + e.message; document.body.append(p); }
</script>