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 { createInitialState, reduce, PHASE, playableManifestIndices, playableCursor, } from "../../src/lib/preview/session.js"; const T0 = 1_700_000_000_000; const SID = "sess-1"; /** rows: a=available, x=not provided — e.g. "aaxa" */ function manifestOf(pattern) { return buildFixtureManifest({ rows: [...pattern].map((c, i) => ({ providerTrackId: String(i + 1), title: `T${i + 1}`, initialAvailability: c === "a" ? AVAILABILITY.AVAILABLE : AVAILABILITY.NOT_PROVIDED, })), createdAtMs: T0, }); } function opened(pattern) { const s0 = createInitialState(SID); return reduce(s0, { type: "MANIFEST_READY", sessionId: SID, manifest: manifestOf(pattern) }); } const key = (state, i) => state.manifest.items[i].sessionKey; // ------------------------------------------------------------ lifecycle --- test("manifest ready: first playable becomes active with listen intent, generation bumps", () => { const s = opened("xaa"); assert.equal(s.phase, PHASE.RESOLVING_MEDIA); assert.equal(s.activeManifestIndex, 1); // index 0 is not playable assert.equal(s.requestGeneration, 1); assert.equal(s.desiredPlaying, true); // take-audio-on-open decision }); test("empty manifest → empty phase, no active item", () => { const s = opened("xx"); assert.equal(s.phase, PHASE.EMPTY); assert.equal(s.activeItemKey, null); }); test("close is terminal: nothing lands afterwards", () => { let s = opened("aa"); s = reduce(s, { type: "CLOSE", sessionId: SID }); assert.equal(s.phase, PHASE.CLOSING); const after = reduce(s, { type: "MEDIA_PLAYING", sessionId: SID, generation: s.requestGeneration }); assert.equal(after.phase, PHASE.CLOSING); const stepped = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(stepped.activeManifestIndex, s.activeManifestIndex); }); test("session replacement: actions from the old session id are ignored", () => { const old = opened("aa"); const fresh = reduce(createInitialState("sess-2"), { type: "MANIFEST_READY", sessionId: "sess-2", manifest: manifestOf("aa"), }); const poked = reduce(fresh, { type: "STEP", sessionId: SID, dir: 1 }); // old session id assert.equal(poked, fresh); assert.notEqual(old.sessionId, fresh.sessionId); }); // ------------------------------------------------------------ navigation -- test("step skips unavailable ordinals and never wraps", () => { let s = opened("axa"); // playable indices [0, 2] assert.equal(s.activeManifestIndex, 0); s = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(s.activeManifestIndex, 2); // skipped 1 const end = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(end.activeManifestIndex, 2); // no wrap const back = reduce(s, { type: "STEP", sessionId: SID, dir: -1 }); assert.equal(back.activeManifestIndex, 0); const start = reduce(back, { type: "STEP", sessionId: SID, dir: -1 }); assert.equal(start.activeManifestIndex, 0); // no wrap at the front either }); test("every activation bumps the generation", () => { let s = opened("aaa"); const g1 = s.requestGeneration; s = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(s.requestGeneration, g1 + 1); s = reduce(s, { type: "ACTIVATE_KEY", sessionId: SID, sessionKey: key(s, 0) }); assert.equal(s.requestGeneration, g1 + 2); }); test("activating an unavailable or already-active card is refused", () => { const s = opened("ax"); assert.equal(reduce(s, { type: "ACTIVATE_KEY", sessionId: SID, sessionKey: key(s, 1) }), s); assert.equal(reduce(s, { type: "ACTIVATE_KEY", sessionId: SID, sessionKey: key(s, 0) }), s); }); // -------------------------------------------------- ordinal/cursor maths -- test("playable indices and cursor reflect manifest order and availability", () => { const s = opened("axaxa"); assert.deepEqual(playableManifestIndices(s), [0, 2, 4]); assert.equal(playableCursor(s), 0); const at4 = reduce(reduce(s, { type: "STEP", sessionId: SID, dir: 1 }), { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(at4.activeManifestIndex, 4); assert.equal(playableCursor(at4), 2); }); test("a newly available EARLIER item recomputes the cursor but never moves the active card", () => { let s = opened("xaa"); assert.equal(s.activeManifestIndex, 1); assert.equal(playableCursor(s), 0); s = reduce(s, { type: "AVAILABILITY_CHANGED", sessionId: SID, sessionKey: key(s, 0), availability: AVAILABILITY.AVAILABLE, }); assert.equal(s.activeManifestIndex, 1); // card did not move assert.equal(s.activeItemKey, key(s, 1)); assert.equal(playableCursor(s), 1); // cursor recomputed around it assert.deepEqual(playableManifestIndices(s), [0, 1, 2]); }); test("a failed ACTIVE item stays visible in its error state; stepping leaves it", () => { let s = opened("aaa"); const activeKey = s.activeItemKey; s = reduce(s, { type: "ITEM_FAILED", sessionId: SID, generation: s.requestGeneration, sessionKey: activeKey, errorClass: "stale_lease", }); assert.equal(s.phase, PHASE.ITEM_FAILED); assert.equal(s.activeItemKey, activeKey); // still the visible card assert.equal(playableCursor(s), null); // not currently eligible // stepping forward finds the next eligible MANIFEST ordinal const stepped = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); assert.equal(stepped.activeManifestIndex, 1); }); // ------------------------------------------------------ stale rejection --- test("a lease from an older generation cannot touch the newer item", () => { let s = opened("aaa"); const staleGen = s.requestGeneration; const staleKey = s.activeItemKey; s = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); // gen bumped const poked = reduce(s, { type: "LEASE_READY", sessionId: SID, generation: staleGen, sessionKey: staleKey, clipDurationMs: 30_000, }); assert.equal(poked, s); // dropped entirely }); test("a stale MEDIA_PLAYING cannot flip the phase", () => { let s = opened("aa"); const staleGen = s.requestGeneration; s = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); const poked = reduce(s, { type: "MEDIA_PLAYING", sessionId: SID, generation: staleGen }); assert.equal(poked.phase, s.phase); }); test("current-generation media flow: lease → buffering → playing → position", () => { let s = opened("aa"); const g = s.requestGeneration; s = reduce(s, { type: "LEASE_READY", sessionId: SID, generation: g, sessionKey: s.activeItemKey, clipDurationMs: 30_000 }); assert.equal(s.phase, PHASE.BUFFERING); // desiredPlaying was true assert.equal(s.clipDurationMs, 30_000); s = reduce(s, { type: "MEDIA_PLAYING", sessionId: SID, generation: g }); assert.equal(s.phase, PHASE.PLAYING); s = reduce(s, { type: "MEDIA_POSITION", sessionId: SID, generation: g, positionMs: 12_000 }); assert.equal(s.positionMs, 12_000); }); // ---------------------------------------------------------- auto-advance -- test("clip end auto-advances to the next playable item under a fresh generation", () => { let s = opened("axa"); const g = s.requestGeneration; s = reduce(s, { type: "MEDIA_PLAYING", sessionId: SID, generation: g }); s = reduce(s, { type: "MEDIA_ENDED", sessionId: SID, generation: g }); assert.equal(s.activeManifestIndex, 2); // skipped the unavailable ordinal assert.equal(s.requestGeneration, g + 1); // stale events from the old clip die assert.equal(s.desiredPlaying, true); // keeps playing assert.equal(s.phase, PHASE.RESOLVING_MEDIA); }); test("clip end on the LAST playable item stops there (Replay state)", () => { let s = opened("aa"); s = reduce(s, { type: "STEP", sessionId: SID, dir: 1 }); const g = s.requestGeneration; s = reduce(s, { type: "MEDIA_ENDED", sessionId: SID, generation: g }); assert.equal(s.phase, PHASE.ENDED); assert.equal(s.desiredPlaying, false); assert.equal(s.activeManifestIndex, 1); // stays on the last card // Replay restarts the same item with a new generation const r = reduce(s, { type: "REPLAY", sessionId: SID }); assert.equal(r.activeManifestIndex, 1); assert.equal(r.requestGeneration, g + 1); assert.equal(r.desiredPlaying, true); });