forked from Keelrleek/SoulStream
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.
86 lines
3.7 KiB
JavaScript
86 lines
3.7 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 } from "../../src/lib/preview/session.js";
|
|
import { createFixtureTransport } from "../../src/lib/preview/transport.js";
|
|
import { makeNativeLease } from "../../src/lib/preview/lease.js";
|
|
|
|
const SID = "sess-t";
|
|
const T0 = 1_700_000_000_000;
|
|
const SRC = { provider: "deezer", providerTrackId: "1", matchConfidence: "native" };
|
|
|
|
function openSession() {
|
|
const manifest = buildFixtureManifest({
|
|
rows: [
|
|
{ providerTrackId: "1", title: "One", initialAvailability: AVAILABILITY.AVAILABLE },
|
|
{ providerTrackId: "2", title: "Two", initialAvailability: AVAILABILITY.AVAILABLE },
|
|
],
|
|
createdAtMs: T0,
|
|
});
|
|
return reduce(createInitialState(SID), { type: "MANIFEST_READY", sessionId: SID, manifest });
|
|
}
|
|
|
|
test("end-to-end against the fake transport: load → play → ticks → auto-advance", () => {
|
|
let state = openSession();
|
|
const transport = createFixtureTransport((action) => {
|
|
state = reduce(state, action);
|
|
});
|
|
|
|
const lease = makeNativeLease({ source: SRC, opaqueLeaseId: "op-1", safeForMs: 300_000, clipDurationMs: 30_000 });
|
|
state = reduce(state, {
|
|
type: "LEASE_READY", sessionId: SID, generation: state.requestGeneration,
|
|
sessionKey: state.activeItemKey, clipDurationMs: 30_000,
|
|
});
|
|
transport.load(lease, { sessionId: SID, generation: state.requestGeneration, sessionKey: state.activeItemKey });
|
|
transport.play();
|
|
assert.equal(state.phase, PHASE.PLAYING);
|
|
|
|
transport.tick(12_000);
|
|
assert.equal(state.positionMs, 12_000);
|
|
|
|
transport.tick(18_000); // reaches clip end → MEDIA_ENDED → auto-advance
|
|
assert.equal(state.activeManifestIndex, 1);
|
|
assert.equal(state.phase, PHASE.RESOLVING_MEDIA);
|
|
assert.equal(state.desiredPlaying, true);
|
|
});
|
|
|
|
test("events after a step carry the old generation and are inert", () => {
|
|
let state = openSession();
|
|
const transport = createFixtureTransport((action) => {
|
|
state = reduce(state, action);
|
|
});
|
|
const lease = makeNativeLease({ source: SRC, opaqueLeaseId: "op-1", safeForMs: 300_000 });
|
|
const genAtLoad = state.requestGeneration;
|
|
transport.load(lease, { sessionId: SID, generation: genAtLoad, sessionKey: state.activeItemKey });
|
|
transport.play();
|
|
assert.equal(state.phase, PHASE.PLAYING);
|
|
|
|
// user steps: the reducer bumps the generation…
|
|
state = reduce(state, { type: "STEP", sessionId: SID, dir: 1 });
|
|
const after = state;
|
|
// …but the un-stopped transport keeps ticking with the OLD generation
|
|
transport.tick(5_000);
|
|
assert.equal(state.positionMs, after.positionMs, "stale position must not land");
|
|
assert.equal(state.phase, PHASE.RESOLVING_MEDIA);
|
|
});
|
|
|
|
test("invalid lease surfaces ITEM_FAILED(invalid_media) instead of playing", () => {
|
|
let state = openSession();
|
|
const transport = createFixtureTransport((action) => {
|
|
state = reduce(state, action);
|
|
});
|
|
const bogus = { transport: "native", safeForMs: 100 }; // no opaqueLeaseId
|
|
transport.load(bogus, { sessionId: SID, generation: state.requestGeneration, sessionKey: state.activeItemKey });
|
|
assert.equal(state.phase, PHASE.ITEM_FAILED);
|
|
assert.equal(state.errorClass, "invalid_media");
|
|
});
|
|
|
|
test("dispose is terminal and idempotent", () => {
|
|
const transport = createFixtureTransport(() => {});
|
|
transport.dispose();
|
|
transport.dispose();
|
|
assert.throws(() => transport.load({}, { sessionId: SID, generation: 1, sessionKey: "k" }));
|
|
});
|