import { test } from "node:test"; import assert from "node:assert/strict"; import { AVAILABILITY } from "../../src/lib/preview/contracts.js"; import { buildFixtureManifest } from "../../src/lib/preview/manifest.js"; import { createFixtureResolver } from "../../src/lib/preview/resolver.js"; import { createPreviewService } from "../../src/lib/preview/service.js"; import { PHASE } from "../../src/lib/preview/session.js"; const T0 = 1_700_000_000_000; const SID = "svc-1"; function manifest(n = 3) { return buildFixtureManifest({ rows: Array.from({ length: n }, (_, i) => ({ providerTrackId: String(i + 1), title: `T${i + 1}`, initialAvailability: AVAILABILITY.AVAILABLE, })), createdAtMs: T0, }); } /** Manual scheduler: settle windows fire only when the test says so. */ function makeScheduler() { let queue = new Map(); let nextId = 1; return { set(fn, ms) { const id = nextId++; queue.set(id, fn); return id; }, clear(id) { queue.delete(id); }, async flush() { const fns = [...queue.values()]; queue.clear(); for (const fn of fns) await fn(); await drainMicrotasks(); }, pending: () => queue.size, }; } const drainMicrotasks = () => new Promise((r) => setTimeout(r, 0)); /** Transport double: records every command AND behaves like the real one — * play/pause dispatch their media events stamped with the load's context, * which is exactly the wiring the service's transportFactory provides. */ function makeTransport() { const log = []; let dispatch = null; let ctx = null; const t = { log, async load(lease, c, opts) { ctx = c; log.push(["load", lease.opaqueLeaseId, c.generation, !!opts?.startPaused]); }, async play() { log.push(["play"]); if (ctx) dispatch({ type: "MEDIA_PLAYING", ...ctx }); }, async pause() { log.push(["pause"]); if (ctx) dispatch({ type: "MEDIA_PAUSED", ...ctx }); }, async seek(ms) { log.push(["seek", ms]); }, async stop() { ctx = null; log.push(["stop"]); }, async dispose() { log.push(["dispose"]); }, end() { if (ctx) dispatch({ type: "MEDIA_ENDED", ...ctx }); }, }; return { transport: t, factory: (d) => { dispatch = d; return t; }, }; } function makeFocus({ grant = true, pausedByPreview = true } = {}) { const calls = { acquire: 0, release: 0, handoff: 0 }; let live = null; return { calls, released: [], coordinator: { async acquirePreview() { calls.acquire += 1; if (!grant) return { granted: false, token: null }; live = { id: `t${calls.acquire}`, pausedByPreview }; return { granted: true, token: live }; }, release(token) { calls.release += 1; const ok = token === live; live = null; return { restored: ok && pausedByPreview }; }, handoffToMain() { calls.handoff += 1; live = null; return { restored: false, reason: "handoff" }; }, }, }; } function makeService({ plan, grant = true, n = 3 } = {}) { const scheduler = makeScheduler(); const { transport, factory } = makeTransport(); const focus = makeFocus({ grant }); let clock = 0; const resolver = createFixtureResolver( plan ?? { 1: { opaqueLeaseId: "op-1" }, 2: { opaqueLeaseId: "op-2" }, 3: { opaqueLeaseId: "op-3" } } ); const service = createPreviewService({ resolver, transportFactory: factory, focus, scheduler, monotonicNow: () => clock, }); return { service, scheduler, transport, focus, resolver, advance: (ms) => (clock += ms), manifest: manifest(n) }; } // ------------------------------------------------------------------ open -- test("open: focus, settle, resolve, load audible, play", async () => { const { service, scheduler, transport, focus, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); assert.equal(focus.calls.acquire, 1); assert.equal(service.getState().phase, PHASE.RESOLVING_MEDIA); assert.equal(scheduler.pending(), 1); // settle armed await scheduler.flush(); const st = service.getState(); assert.equal(st.phase, PHASE.PLAYING); // lease landed, played, confirmed const load = transport.log.find((e) => e[0] === "load"); assert.deepEqual(load, ["load", "op-1", st.requestGeneration, false]); // audible → not parked assert.ok(transport.log.some((e) => e[0] === "play")); }); test("open with focus denied: session opens PARKED, wait state surfaced", async () => { const { service, scheduler, transport, manifest } = makeService({ grant: false }); await service.open({ sessionId: SID, manifest }); assert.equal(service.getState().desiredPlaying, false); await scheduler.flush(); const load = transport.log.find((e) => e[0] === "load"); assert.equal(load[3], true, "must load parked without focus"); assert.ok(!transport.log.some((e) => e[0] === "play"), "no audio without focus"); }); // ------------------------------------------------------------- stepping --- test("rapid steps: only the LAST generation ever reaches the transport", async () => { const { service, scheduler, transport, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); service.step(1); // supersedes the open's pipeline before its settle fires service.step(1); const finalGen = service.getState().requestGeneration; await scheduler.flush(); await scheduler.flush(); const loads = transport.log.filter((e) => e[0] === "load"); assert.equal(loads.length, 1, "superseded generations never loaded"); assert.equal(loads[0][2], finalGen); assert.equal(loads[0][1], "op-3"); // stepped 1 → 2 → 3 }); test("failed resolve retries exactly once, then fails the item in place", async () => { const { service, scheduler, resolver, manifest } = makeService({ plan: { 1: (n) => (n === 1 ? { failure: "stale_lease", retryable: true } : { opaqueLeaseId: "op-fresh" }), 2: { failure: "network", retryable: true }, // fails on retry too 3: { opaqueLeaseId: "op-3" }, }, }); await service.open({ sessionId: SID, manifest }); await scheduler.flush(); await drainMicrotasks(); // background neighbor prefetch (item 2) settles assert.equal(service.getState().phase, PHASE.PLAYING); // retry rescued item 1 assert.equal(resolver.callCount("1"), 2); assert.equal(resolver.callCount("2"), 1, "prefetch tried item 2 once — prefetch never retries"); service.step(1); await scheduler.flush(); const st = service.getState(); assert.equal(st.phase, PHASE.ITEM_FAILED); assert.equal(st.errorClass, "network"); // 1 prefetch + 1 pipeline resolve + exactly 1 capped retry = 3, then stop assert.equal(resolver.callCount("2"), 3, "one capped retry in the pipeline, never a loop"); // the failed card stays; stepping onward still works service.step(1); await scheduler.flush(); assert.equal(service.getState().phase, PHASE.PLAYING); assert.equal(resolver.callCount("2"), 3, "failure is settled — no more calls"); }); // ------------------------------------------------------------- prefetch --- test("neighbor lease prefetch: stepping forward skips the resolve", async () => { const { service, scheduler, resolver, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); await scheduler.flush(); // pipeline + prefetch of item 2 kicked await drainMicrotasks(); assert.equal(resolver.callCount("2"), 1, "prefetched in the background"); service.step(1); await scheduler.flush(); assert.equal(resolver.callCount("2"), 1, "no second resolve on activation"); assert.equal(service.getState().phase, PHASE.PLAYING); }); test("a prefetched lease past 80% of its safe window is not trusted", async () => { const { service, scheduler, resolver, advance, manifest } = makeService({ plan: { 1: { opaqueLeaseId: "op-1", safeForMs: 10_000 }, 2: { opaqueLeaseId: "op-2", safeForMs: 10_000 }, 3: { opaqueLeaseId: "op-3" }, }, }); await service.open({ sessionId: SID, manifest }); await scheduler.flush(); await drainMicrotasks(); advance(9_000); // beyond 80% of the 10s safe window service.step(1); await scheduler.flush(); assert.equal(resolver.callCount("2"), 2, "stale prefetch re-resolved"); }); // ------------------------------------------------------ close/handoff ----- test("close during resolve: terminal, transport torn down, focus released once", async () => { const { service, scheduler, transport, focus, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); // close BEFORE the settle window fires — mid-"resolving" phase const out = await service.close(); assert.equal(service.getState().phase, PHASE.CLOSING); assert.equal(scheduler.pending(), 0, "settle timer cancelled"); assert.ok(transport.log.some((e) => e[0] === "dispose")); assert.equal(focus.calls.release, 1); assert.ok(out.restored); // straggler flush does nothing await scheduler.flush(); assert.ok(!transport.log.some((e) => e[0] === "load")); }); test("play-full handoff: transport stopped, no restoration, main starts with the active item", async () => { const { service, scheduler, transport, focus, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); await scheduler.flush(); let started = null; await service.playFullTrack(async (item) => { started = item; }); assert.equal(focus.calls.handoff, 1); assert.equal(started.title, "T1"); assert.ok(transport.log.some((e) => e[0] === "stop")); // closing afterwards must NOT restore the pre-preview main track const out = await service.close(); assert.equal(out.restored, false); }); // -------------------------------------------------------------- controls -- test("togglePlay: pause from playing, resume from paused, replay from ended", async () => { const { service, scheduler, transport, manifest } = makeService(); await service.open({ sessionId: SID, manifest }); await scheduler.flush(); assert.equal(service.getState().phase, PHASE.PLAYING); await service.togglePlay(); assert.equal(service.getState().phase, PHASE.PAUSED); assert.equal(service.getState().desiredPlaying, false); assert.ok(transport.log.some((e) => e[0] === "pause")); await service.togglePlay(); assert.equal(service.getState().phase, PHASE.PLAYING); assert.equal(service.getState().desiredPlaying, true); // ride to the LAST playable item, end its clip, and replay from ENDED service.step(1); await scheduler.flush(); service.step(1); await scheduler.flush(); assert.equal(service.getState().phase, PHASE.PLAYING); transport.end(); assert.equal(service.getState().phase, PHASE.ENDED); await service.togglePlay(); // replay assert.equal(service.getState().phase, PHASE.RESOLVING_MEDIA); assert.equal(service.getState().desiredPlaying, true); });