import { describe, test } from "node:test"; import assert from "node:assert/strict"; import { slugify, mintEntityId, normalizeProvenance } from "../src/id.js"; describe("id module (SC4)", () => { describe("slugify", () => { test("lowercases, trims, and dashes non-alphanumeric runs", () => { assert.equal(slugify("Current Role"), "current-role"); }); test("collapses repeated separators and strips edge dashes", () => { assert.equal(slugify(" Core Expertise Areas (5 topics) "), "core-expertise-areas-5-topics"); }); test("is stable across case + whitespace variation of the same label", () => { assert.equal(slugify("Current Role"), slugify(" current ROLE ")); }); // N24.6 — transliteration. Before this, every non-ASCII letter fell into the // `[^a-z0-9]+` run and became a dash, so a Norwegian label was mangled // ("Løkkene" → "l-kkene") and distinct labels collapsed onto one slug. test("transliterates the non-decomposable Nordic/German letters", () => { assert.equal(slugify("Løkkene"), "lokkene"); assert.equal(slugify("Æresmedlem"), "aeresmedlem"); assert.equal(slugify("Straße"), "strasse"); }); test("strips combining diacritics rather than dashing them", () => { assert.equal(slugify("Måte"), "mate"); assert.equal(slugify("Café résumé"), "cafe-resume"); assert.equal(slugify("Señor"), "senor"); }); test("keeps ø-labels and å-labels distinct (they both collapsed to `-` before)", () => { assert.notEqual(slugify("Møte"), slugify("Måte")); }); test("output stays whitelisted to [a-z0-9-] for any input", () => { // Load-bearing beyond this repo: the ingestion-guard exposure numbers reported // to the guard owner rest on this whitelist being STRUCTURAL (no %-escape and no // image URL can survive a slug). Transliteration runs BEFORE the collapse so the // collapse stays the final gate. for (const raw of ["%2Fetc%2Fpasswd", "https://x.tld/a b.png?q=1#f", "Løkke%C3%B8", "日本語", "ab"]) { assert.match(slugify(raw), /^[a-z0-9-]*$/, `slug of ${JSON.stringify(raw)} is whitelisted`); } }); }); describe("mintEntityId", () => { test("is deterministic — same seed yields the same id", () => { assert.equal( mintEntityId({ kind: "profile-field", key: "Current Role" }), mintEntityId({ kind: "profile-field", key: "Current Role" }), ); }); test("is stable across label case/whitespace (id keyed on the slug)", () => { assert.equal( mintEntityId({ kind: "profile-field", key: "Current Role" }), mintEntityId({ kind: "profile-field", key: " current ROLE " }), ); }); test("different key yields a different id", () => { assert.notEqual( mintEntityId({ kind: "profile-field", key: "Current Role" }), mintEntityId({ kind: "profile-field", key: "Organization" }), ); }); test("different kind yields a different id (kind is part of the seed)", () => { assert.notEqual( mintEntityId({ kind: "profile-field", key: "role" }), mintEntityId({ kind: "expertise-area", key: "role" }), ); }); // N24.6 id-stability pin. The transliteration change re-mints any id whose key // carries a non-ASCII letter. Every label the SHIPPED profile template mints is // ASCII (27/27, measured), so for the profile layer the change is a provable // no-op — these goldens were computed BEFORE the change and must not move. test("ASCII keys keep their pre-transliteration ids (profile-field goldens)", () => { const golden: Record = { Name: "4f321cef28b9", "Current Role": "25c03976c855", "expertise-area-1": "ad3cac5a825f", "Topics to AVOID": "36803b03c55f", "90-day growth goal": "e18da9c9e66a", }; for (const [key, id] of Object.entries(golden)) { assert.equal(mintEntityId({ kind: "profile-field", key }), id, `id for ${JSON.stringify(key)}`); } }); test("id is 12 lowercase hex characters", () => { assert.match(mintEntityId({ kind: "profile-field", key: "Name" }), /^[0-9a-f]{12}$/); }); }); describe("normalizeProvenance", () => { test("returns each accepted value unchanged", () => { assert.equal(normalizeProvenance("human"), "human"); assert.equal(normalizeProvenance("published"), "published"); assert.equal(normalizeProvenance("ai-draft"), "ai-draft"); }); test("trims and lowercases before matching", () => { assert.equal(normalizeProvenance(" Human "), "human"); assert.equal(normalizeProvenance("AI-DRAFT"), "ai-draft"); }); test("throws on anything outside the provenance vocabulary", () => { assert.throws(() => normalizeProvenance("robot")); assert.throws(() => normalizeProvenance("")); assert.throws(() => normalizeProvenance("humanoid")); }); }); });