Files
linker/service/lib.test.js
T
paulandClaude Opus 4.8 2b4babf955 feat: detect & link album mentions, not just artists (closes task/album-detection)
Source: task/album-detection (objective/linker-improvements) — project brief asks for artist OR album linking; MVP linked artists only.

- service/lib.js: pickAlbum (the exact-name gate, named for the album path) + linkResult now carries kind:"artist"|"album" (additive; defaults to artist so older clients keep working).
- service/resolver.js: one Spotify type=artist,album search per candidate, exact-match against either; returns {url,kind}; cache key includes the kind hint. resolveOne/resolveAll thread the LLM kind hint through.
- service/llm.js: parseClassification now returns Map(name -> "artist"|"album") so album-kind candidates route to the album path (.has() still works like the old Set).
- extension/extract.js: albumMatchesIn over two precise signals (quoted strings + album cue phrases "the album X", "X LP/EP"), folded into allMatchesIn.
- extension/content.js + styles.css: album links render italicised (.rsl-album) with an album tooltip; artist behavior unchanged.
- Tests: new pure-logic coverage in lib.test.js / llm.test.js / extract.test.cjs (album shaping, Map-kind routing, album candidate extraction). scripts/check.sh green.
- README.md + AGENTS.md: document album linking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-22 22:33:32 +00:00

91 lines
4.2 KiB
JavaScript

// Unit tests for the resolver's pure logic (run with `node --test`). Zero-dep.
import { test } from "node:test";
import assert from "node:assert/strict";
import { norm, googleSearch, pickArtist, pickAlbum, linkResult, isSearchUrl, isHttpUrl } from "./lib.js";
test("norm lowercases, trims, NFKC-normalizes", () => {
assert.equal(norm(" Cerebral Bore "), "cerebral bore");
assert.equal(norm("DEVOURMENT"), "devourment");
});
test("pickArtist matches case-insensitively, rejects non-matches", () => {
const items = [{ name: "Cattle Decapitation" }, { name: "Cerebral Bore" }];
assert.equal(pickArtist("cerebral bore", items)?.name, "Cerebral Bore");
assert.equal(pickArtist("CATTLE DECAPITATION", items)?.name, "Cattle Decapitation");
assert.equal(pickArtist("Notarealband", items), null);
});
test("pickArtist tolerates empty / garbage input", () => {
assert.equal(pickArtist("x", null), null);
assert.equal(pickArtist("x", []), null);
assert.equal(pickArtist("x", [null, { name: "y" }]), null);
});
test("googleSearch points at a Google query for '<name> bandcamp', url-encoded", () => {
const u = googleSearch("Cerebral Bore");
assert.match(u, /^https:\/\/www\.google\.com\/search\?q=/);
assert.ok(u.includes("Cerebral%20Bore%20bandcamp"));
assert.ok(googleSearch("A&B").includes("A%26B"));
});
test("pickAlbum is the same exact-name gate as pickArtist (album items)", () => {
const items = [{ name: "Scum" }, { name: "Reek of Putrefaction" }];
assert.equal(pickAlbum("reek of putrefaction", items)?.name, "Reek of Putrefaction");
assert.equal(pickAlbum("SCUM", items)?.name, "Scum");
assert.equal(pickAlbum("Not an album", items), null);
assert.equal(pickAlbum, pickArtist); // intentionally the same function, named for readability
});
test("linkResult prefers a direct Spotify url, primary = spotify", () => {
const r = linkResult("Devourment", "https://open.spotify.com/artist/abc");
assert.equal(r.spotify, "https://open.spotify.com/artist/abc");
assert.deepEqual(r.primary, { platform: "spotify", url: "https://open.spotify.com/artist/abc" });
assert.equal(r.google, undefined);
});
test("linkResult defaults kind to artist, carries kind=album when asked", () => {
assert.equal(linkResult("Devourment", "https://open.spotify.com/artist/abc").kind, "artist");
const al = linkResult("Reek of Putrefaction", "https://open.spotify.com/album/xyz", "album");
assert.equal(al.kind, "album");
assert.equal(al.spotify, "https://open.spotify.com/album/xyz");
assert.deepEqual(al.primary, { platform: "spotify", url: "https://open.spotify.com/album/xyz" });
// an unknown kind string degrades to "artist" rather than passing through
assert.equal(linkResult("X", "https://open.spotify.com/artist/abc", "bogus").kind, "artist");
});
test("linkResult keeps kind on the Google fallback path too", () => {
const r = linkResult("Some Local Album", null, "album");
assert.equal(r.kind, "album");
assert.equal(r.primary.platform, "google");
assert.match(r.primary.url, /^https:\/\/www\.google\.com\/search\?q=/);
});
test("linkResult falls back to Google->Bandcamp when there is no direct Spotify url", () => {
const r = linkResult("Some Local Band", null);
assert.equal(r.spotify, undefined);
assert.equal(r.primary.platform, "google");
assert.match(r.primary.url, /^https:\/\/www\.google\.com\/search\?q=/);
assert.equal(r.google, r.primary.url);
});
test("linkResult rejects non-http(s) / search Spotify urls and falls back to Google", () => {
for (const bad of ["javascript:alert(1)", "ftp://x/y", "https://open.spotify.com/search/X", "not a url"]) {
const r = linkResult("X", bad);
assert.equal(r.primary.platform, "google", `expected fallback for ${bad}`);
}
});
test("isSearchUrl detects /search urls", () => {
assert.equal(isSearchUrl("https://open.spotify.com/search/X"), true);
assert.equal(isSearchUrl("https://open.spotify.com/artist/abc"), false);
});
test("isHttpUrl accepts http/https only", () => {
assert.equal(isHttpUrl("https://x.com"), true);
assert.equal(isHttpUrl("http://x.com"), true);
assert.equal(isHttpUrl("javascript:alert(1)"), false);
assert.equal(isHttpUrl("ftp://x/y"), false);
assert.equal(isHttpUrl("not a url"), false);
assert.equal(isHttpUrl(null), false);
});