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.
170 lines
6.9 KiB
JavaScript
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);
|
|
});
|