Files
SoulStream/tests/preview/focus.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

170 lines
6.9 KiB
JavaScript

import { test } from "node:test";
import assert from "node:assert/strict";
import { createFocusCoordinator, createRevisionGateway } from "../../src/lib/preview/focus.js";
import { createFixtureResolver } from "../../src/lib/preview/resolver.js";
function harness({ audible = true, ackGeneration, ackApplied = true, ackDelayMs = 0, ackHangs = false } = {}) {
const gateway = createRevisionGateway();
const resumed = [];
const status = {
status: audible ? "playing" : "idle",
trackKey: "track-A",
positionMs: 102_000,
audible,
playbackGeneration: 7,
};
const coord = createFocusCoordinator({
mainStatus: () => ({ ...status }),
requestSilence: (gen) =>
ackHangs
? new Promise(() => {})
: new Promise((res) =>
setTimeout(() => res({ generation: ackGeneration ?? gen, applied: ackApplied }), ackDelayMs)
),
resumeMain: (snapshot, opts) => resumed.push({ snapshot, opts }),
gateway,
ackTimeoutMs: 50,
});
return { coord, gateway, resumed, status };
}
// ------------------------------------------------------------ acquisition --
test("main idle: focus granted without any silence negotiation, nothing to restore", async () => {
const { coord, resumed } = harness({ audible: false });
const { granted, token } = await coord.acquirePreview();
assert.ok(granted);
assert.equal(token.pausedByPreview, false);
const rel = coord.release(token);
assert.equal(rel.restored, false);
assert.equal(rel.reason, "main_was_not_audible");
assert.equal(resumed.length, 0);
});
test("main audible: granted only through a generation-bound acknowledgement", async () => {
const { coord } = harness({ audible: true });
const { granted, token } = await coord.acquirePreview();
assert.ok(granted);
assert.equal(token.pausedByPreview, true);
assert.equal(token.snapshot.trackKey, "track-A");
assert.equal(token.snapshot.positionMs, 102_000);
});
test("stale/wrong-generation ack leaves preview parked", async () => {
const { coord } = harness({ audible: true, ackGeneration: 3 }); // main is at 7
const { granted, token } = await coord.acquirePreview();
assert.equal(granted, false);
assert.equal(token, null);
});
test("ack that never arrives (timeout) leaves preview parked — renderer status is not proof", async () => {
const { coord } = harness({ audible: true, ackHangs: true });
const { granted } = await coord.acquirePreview();
assert.equal(granted, false);
});
test("ack applied=false is not silence", async () => {
const { coord } = harness({ audible: true, ackApplied: false });
const { granted } = await coord.acquirePreview();
assert.equal(granted, false);
});
test("user command DURING negotiation invalidates the grant", async () => {
const { coord, gateway } = harness({ audible: true, ackDelayMs: 20 });
const p = coord.acquirePreview();
gateway.userSeekOrScratch(); // user scrubbed while we negotiated
const { granted } = await p;
assert.equal(granted, false);
});
// -------------------------------------------------------------- release ----
test("clean release restores main at the snapshot, tagged focus-internal", async () => {
const { coord, gateway, resumed } = harness({ audible: true });
const { token } = await coord.acquirePreview();
const rel = coord.release(token);
assert.ok(rel.restored);
assert.equal(resumed.length, 1);
assert.equal(resumed[0].snapshot.trackKey, "track-A");
assert.equal(resumed[0].snapshot.positionMs, 102_000);
assert.equal(resumed[0].opts.focusInternal, true);
// and the focus-internal resume did not invalidate anything
assert.equal(gateway.changedSince(token.revisions), false);
});
for (const [name, bump] of [
["user pause/resume", (g) => g.userPauseResume()],
["user starts another track", (g) => g.userPlay()],
["queue jump / next / previous", (g) => g.queueJumpOrNextPrev()],
["queue mutation", (g) => g.queueMutation()],
["seek/scratch", (g) => g.userSeekOrScratch()],
["stop", (g) => g.userStop()],
["auto-advance", (g) => g.autoAdvance()],
["native source change", (g) => g.nativeSourceChange()],
]) {
test(`no restoration after ${name}`, async () => {
const { coord, gateway, resumed } = harness({ audible: true });
const { token } = await coord.acquirePreview();
bump(gateway);
const rel = coord.release(token);
assert.equal(rel.restored, false);
assert.equal(rel.reason, "revisions_changed");
assert.equal(resumed.length, 0);
});
}
test("focus-internal commands do not invalidate their own token", async () => {
const { coord, gateway } = harness({ audible: true });
const { token } = await coord.acquirePreview();
gateway.userPauseResume({ focusInternal: true }); // the coordinator's own pause
gateway.userPlay({ focusInternal: true });
assert.equal(gateway.changedSince(token.revisions), false);
assert.ok(coord.release(token).restored);
});
test("play-full handoff never restores the old track", async () => {
const { coord, resumed } = harness({ audible: true });
await coord.acquirePreview();
const h = coord.handoffToMain();
assert.equal(h.restored, false);
assert.equal(h.reason, "handoff");
assert.equal(resumed.length, 0);
assert.equal(coord.owner(), "main");
});
test("a stale token cannot release anything", async () => {
const { coord } = harness({ audible: true });
const { token } = await coord.acquirePreview();
coord.release(token);
const again = coord.release(token);
assert.equal(again.restored, false);
assert.equal(again.reason, "stale_token");
});
// ------------------------------------------------------------- resolver ----
test("fixture resolver: scripted fail-once-then-succeed with bypassCache", async () => {
const src = { provider: "deezer", providerTrackId: "42", matchConfidence: "native" };
const resolver = createFixtureResolver({
42: (n) => (n === 1 ? { failure: "stale_lease", retryable: true } : { opaqueLeaseId: "op-fresh" }),
});
const first = await resolver.resolve(src, { generation: 5 });
assert.equal(first.ok, false);
assert.equal(first.failure, "stale_lease");
assert.ok(first.retryable);
const second = await resolver.resolve(src, { generation: 5, bypassCache: true });
assert.ok(second.ok);
assert.equal(second.lease.transport, "native");
assert.equal(second.lease.opaqueLeaseId, "op-fresh");
assert.equal(resolver.callCount("42"), 2);
});
test("fixture resolver: unknown id classifies as not_found, not an exception", async () => {
const resolver = createFixtureResolver({});
const r = await resolver.resolve({ provider: "deezer", providerTrackId: "999", matchConfidence: "native" }, { generation: 1 });
assert.equal(r.ok, false);
assert.equal(r.failure, "not_found");
assert.equal(r.retryable, false);
});