Files
KeelrLeek 8ede6a8adf Phase 1: preview contracts and pure session core, 61 tests
src/lib/preview/ — framework-free ESM modules, per design §12–14:
- contracts.js: versioned manifest/item/source-ref factories; signed
  URLs structurally excluded from manifests; Apple identity first-class.
- lease.js: discriminated MediaLease union; computeSafeForMs derives
  lifetimes provider-clock-first with fallback/contradiction handling
  (margins are ADRs backed by Phase 0 soak evidence).
- redact.js: URL redaction (host+hash+exp only) and the HTTPS + exact
  host allowlist gate applied to initial URLs and every redirect.
- manifest.js: artist (rank order, exact-id dedupe), album (disc/track
  sort, context inheritance) and fixture builders — pure, no fetching.
- session.js: reducer with session/generation guards on every async
  action; keyed active item with derived playable indices/cursor;
  auto-advance under fresh generations; close is terminal.
- resolver.js: PreviewResolver contract + scripted fixture resolver
  with classified failures (stale_lease/not_found/network/…).
- transport.js: transport seam + deterministic fake (manual ticks).
- focus.js: revision gateway (all main-player entry points; focus-
  internal tag) and the coordinator — acquisition only via a
  generation-bound silence ack, restore only with revisions unchanged.

tests/preview/*.test.mjs run via `npm test` (node --test): 61 passing,
covering the full Phase 1 list in IMPLEMENTATION-PLAN §6 including the
exit gate: stale lease/play events are inert, no restoration after any
simulated main command, and no component renders anywhere.
2026-07-22 18:29:03 -05:00

213 lines
8.6 KiB
JavaScript

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);
});