Files
linker/service/resolver.test.js
T
paulandClaude Opus 4.8 8496e55baf refactor: extract testable resolve-core + cover album routing; check.sh checks .cjs (closes task/resolver-orchestration-testable)
Source: task/resolver-orchestration-testable (plan/steward-linker-quality) — new album orchestration in resolver.js had zero node:test coverage.

- Extract the pure album/artist orchestration into service/resolve-core.js
  (resolveAll/resolveOne/pickDirect/cacheKey) taking injected search/classify
  fns; resolver.js becomes a thin server wiring the real Spotify search +
  ollama classify. No behavior change.
- Add service/resolver.test.js: self-titled artist+album tie-break, LLM-album
  -> Google fallback, kind-aware cache separation, plus gate/drop/error paths.
- scripts/check.sh: node --check now also matches *.cjs (extract.test.cjs).
- AGENTS.md Testing: note resolve-core coverage + the extract.test.cjs extractor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-23 00:29:59 +00:00

142 lines
7.3 KiB
JavaScript

// Unit tests for the resolve-core orchestration (run with `node --test`). Zero-dep, hermetic:
// the live Spotify/ollama clients are replaced by injected fake search/classify functions, so no
// network and no real timers. Covers the album routing the HTTP server can't unit-test directly.
import { test } from "node:test";
import assert from "node:assert/strict";
import { resolveAll, resolveOne, pickDirect, cacheKey } from "./resolve-core.js";
const ARTIST_URL = "https://open.spotify.com/artist/abc";
const ALBUM_URL = "https://open.spotify.com/album/xyz";
// A fake `search` from a { norm(name) -> { artist, album } } table (urls or null).
const fakeSearch = (table) => (name) => Promise.resolve(table[name.trim().toLowerCase()] ?? { artist: null, album: null });
// A fake `classify` returning a Map(norm(name) -> kind) from a plain { name: kind } object.
const fakeClassify = (verdicts) => (names) => {
void names;
return Promise.resolve(new Map(Object.entries(verdicts).map(([n, k]) => [n.toLowerCase(), k])));
};
test("pickDirect tie-break: kindHint='album' prefers the album, else artist-first", () => {
const both = { artist: ARTIST_URL, album: ALBUM_URL };
assert.deepEqual(pickDirect(both, "album"), { url: ALBUM_URL, kind: "album" });
assert.deepEqual(pickDirect(both, "artist"), { url: ARTIST_URL, kind: "artist" });
assert.deepEqual(pickDirect(both, undefined), { url: ARTIST_URL, kind: "artist" });
// single-sided hits ignore the hint
assert.deepEqual(pickDirect({ artist: ARTIST_URL, album: null }, "album"), { url: ARTIST_URL, kind: "artist" });
assert.equal(pickDirect({ artist: null, album: null }, "album"), null);
});
test("cacheKey separates the same name by kind hint, defaults to artist", () => {
assert.equal(cacheKey("Tool", "album"), "album:tool");
assert.equal(cacheKey("Tool", "artist"), "artist:tool");
assert.equal(cacheKey("Tool", undefined), "artist:tool");
});
test("self-titled artist+album tie-break routes to the kindHint'd URL", async () => {
// A self-titled name that is BOTH an exact artist hit and an exact album hit on Spotify.
const search = fakeSearch({ "bad religion": { artist: ARTIST_URL, album: ALBUM_URL } });
// kindHint='album' -> the album URL.
const asAlbum = await resolveOne("Bad Religion", { search, kindHint: "album", allowFallback: true });
assert.equal(asAlbum.kind, "album");
assert.equal(asAlbum.spotify, ALBUM_URL);
assert.deepEqual(asAlbum.primary, { platform: "spotify", url: ALBUM_URL });
// undefined and 'artist' both -> the artist URL (artist-first default).
for (const hint of [undefined, "artist"]) {
const asArtist = await resolveOne("Bad Religion", { search, kindHint: hint, allowFallback: true });
assert.equal(asArtist.kind, "artist", `hint=${hint}`);
assert.equal(asArtist.spotify, ARTIST_URL);
assert.deepEqual(asArtist.primary, { platform: "spotify", url: ARTIST_URL });
}
});
test("album confirmed by the LLM but absent from Spotify falls back to a Google search", async () => {
// The LLM gate confirms the name is an album; Spotify has no exact hit -> Google fallback.
const search = fakeSearch({}); // nothing on Spotify
const classify = fakeClassify({ "Some Local Release": "album" });
const out = await resolveAll(["Some Local Release"], { search, classify });
const r = out["Some Local Release"];
assert.ok(r, "confirmed name should resolve, not drop");
assert.equal(r.kind, "album");
assert.equal(r.primary.platform, "google");
assert.match(r.primary.url, /^https:\/\/www\.google\.com\/search\?q=/);
assert.ok(r.primary.url.includes("bandcamp"));
assert.equal(r.spotify, undefined);
});
test("LLM confirms artist+album for two names and routes each kind's Spotify lookup", async () => {
const search = fakeSearch({
devourment: { artist: ARTIST_URL, album: null },
"reek of putrefaction": { artist: null, album: ALBUM_URL },
});
const classify = fakeClassify({ Devourment: "artist", "Reek of Putrefaction": "album" });
const out = await resolveAll(["Devourment", "Reek of Putrefaction"], { search, classify });
assert.equal(out["Devourment"].kind, "artist");
assert.equal(out["Devourment"].spotify, ARTIST_URL);
assert.equal(out["Reek of Putrefaction"].kind, "album");
assert.equal(out["Reek of Putrefaction"].spotify, ALBUM_URL);
});
test("names the LLM did not confirm are dropped (null)", async () => {
const search = fakeSearch({ "real band": { artist: ARTIST_URL, album: null } });
const classify = fakeClassify({ "Real Band": "artist" }); // "the best part" not in the map
const out = await resolveAll(["Real Band", "the best part"], { search, classify });
assert.ok(out["Real Band"]);
assert.equal(out["the best part"], null);
});
test("cache-key kind separation: same name as artist vs album does not cross-contaminate", async () => {
const cache = new Map();
// One name exists on Spotify as BOTH; the kind hint must keep the two lookups distinct.
const search = fakeSearch({ "self titled": { artist: ARTIST_URL, album: ALBUM_URL } });
const asArtist = await resolveOne("Self Titled", { search, cache, kindHint: "artist", allowFallback: true });
const asAlbum = await resolveOne("Self Titled", { search, cache, kindHint: "album", allowFallback: true });
assert.equal(asArtist.spotify, ARTIST_URL);
assert.equal(asArtist.kind, "artist");
assert.equal(asAlbum.spotify, ALBUM_URL);
assert.equal(asAlbum.kind, "album");
// Two distinct cache entries, keyed by kind — no cross-contamination.
assert.equal(cache.size, 2);
assert.deepEqual(cache.get("artist:self titled"), { url: ARTIST_URL, kind: "artist" });
assert.deepEqual(cache.get("album:self titled"), { url: ALBUM_URL, kind: "album" });
});
test("with no LLM gate (classify null) the Spotify exact-match IS the gate; unconfirmed -> null", async () => {
// On Spotify -> links; not on Spotify and no confirmation -> null (no Google fallback).
const search = fakeSearch({ devourment: { artist: ARTIST_URL, album: null } });
const out = await resolveAll(["Devourment", "Not A Band"], { search, classify: null });
assert.equal(out["Devourment"].kind, "artist");
assert.equal(out["Devourment"].spotify, ARTIST_URL);
assert.equal(out["Not A Band"], null); // not confirmed, not on Spotify -> dropped
});
test("LLM-down (classify throws) falls back to the Spotify exact-match gate", async () => {
const search = fakeSearch({ devourment: { artist: ARTIST_URL, album: null } });
const classify = () => Promise.reject(new Error("ollama down"));
const out = await resolveAll(["Devourment", "Not A Band"], { search, classify });
assert.equal(out["Devourment"].spotify, ARTIST_URL); // Spotify hit still links
assert.equal(out["Not A Band"], null); // no fallback once the LLM gate is gone
});
test("resolveAll dedups, trims, drops blanks, and caps the candidate list", async () => {
const search = fakeSearch({ tool: { artist: ARTIST_URL, album: null } });
const out = await resolveAll(["Tool", " Tool ", "", " "], { search, classify: null });
assert.deepEqual(Object.keys(out), ["Tool"]); // " Tool " collapses to "Tool"; blanks gone
assert.equal(out["Tool"].spotify, ARTIST_URL);
});
test("resolveOne swallows a search error and yields null for that name", async () => {
const search = () => Promise.reject(new Error("spotify 503"));
const r = await resolveOne("Tool", { search, allowFallback: true });
assert.equal(r, null);
});