Files
SoulStream/tests/preview/transport.test.mjs
T
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

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