/** * Unit tests for refresh-lookups.mjs — the generated lookup tables. * * Run: node --test library/skills/hoen-data-assist/scripts/refresh-lookups.test.mjs */ import { test } from "node:test"; import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { existsSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { buildTables, compareTables, csvCell, DEFAULT_MODEL_PATH, needRows, parseArgs, questionRows, renderText, summarise, toCsv, writeTables, } from "./refresh-lookups.mjs"; const dir = dirname(fileURLToPath(import.meta.url)); const script = join(dir, "refresh-lookups.mjs"); const hasModel = existsSync(DEFAULT_MODEL_PATH); const skipWithoutModel = hasModel ? false : "requires packages/hoen-model/dist/model.json (hoen-assessment)"; const model = { version: "v8", needs: [ { id: "1-people-purpose", level: 1, name: "People & Purpose" }, { id: "2-alerting", level: 2, name: "Alerting" }, ], surveyQuestions: [ { id: "q0001", text: 'We trust alerting, mostly, and say "yes".', priority: 2, needWeights: { "2-alerting": 1, "1-people-purpose": 0.5, "3-unused": 0 }, }, ], }; test("csvCell quotes commas, quotes and newlines, and doubles inner quotes", () => { assert.equal(csvCell("plain"), "plain"); assert.equal(csvCell("a,b"), '"a,b"'); assert.equal(csvCell('say "yes"'), '"say ""yes"""'); assert.equal(csvCell(null), ""); }); test("toCsv joins rows and ends with a trailing newline", () => { assert.equal(toCsv([["a", "b"], ["c", "d"]]), "a,b\nc,d\n"); }); test("needRows carries the header, id, level and name in model order", () => { assert.deepEqual(needRows(model), [ ["need_id", "level", "name"], ["1-people-purpose", 1, "People & Purpose"], ["2-alerting", 2, "Alerting"], ]); }); test("questionRows drops zero weights and sorts feeds_needs by weight", () => { const [, row] = questionRows(model); assert.equal(row[0], "q0001"); assert.equal(row[3], "2-alerting:1 | 1-people-purpose:0.5"); }); test("question prose with a comma survives the CSV round trip quoted", () => { const csv = toCsv(questionRows(model)); assert.match(csv, /"We trust alerting, mostly, and say ""yes""\."/); }); test("buildTables produces both files", () => { assert.deepEqual(Object.keys(buildTables(model)), [ "need-ids.csv", "survey-questions.csv", ]); }); test("compareTables reports in-step, drifted and missing", () => { const tables = buildTables(model); const results = compareTables(tables, "/nowhere", (path) => path.endsWith("need-ids.csv") ? tables["need-ids.csv"] : path.endsWith("survey-questions.csv") ? "stale\n" : null, ); assert.deepEqual(results, [ { file: "need-ids.csv", status: "in-step" }, { file: "survey-questions.csv", status: "drifted" }, ]); assert.equal(compareTables(tables, "/nowhere", () => null)[0].status, "missing"); }); test("writeTables writes each file and reports it, without touching disk here", () => { const written = []; const results = writeTables(buildTables(model), "/out", (path, contents) => written.push([path, contents.length]), ); assert.equal(written.length, 2); assert.deepEqual( results.map((result) => result.status), ["written", "written"], ); }); test("summarise counts drift but not writes", () => { assert.equal( summarise(model, [{ file: "a", status: "drifted" }, { file: "b", status: "in-step" }]) .drifted, 1, ); assert.equal( summarise(model, [{ file: "a", status: "written" }]).drifted, 0, ); }); test("renderText names the drift and the remedy", () => { const text = renderText(summarise(model, [{ file: "a.csv", status: "drifted" }])); assert.match(text, /model v8: 2 needs, 1 questions/); assert.match(text, /re-run with --write/); }); test("parseArgs defaults to check, and rejects unknown arguments", () => { assert.equal(parseArgs([]).mode, "check"); assert.equal(parseArgs(["--write"]).mode, "write"); assert.equal(parseArgs(["--nope"]).error, "unknown argument '--nope'"); assert.equal(parseArgs(["-h"]).help, true); }); test("CLI: the committed tables are in step with the model", { skip: skipWithoutModel }, () => { const result = spawnSync(process.execPath, [script, "--check"], { encoding: "utf8" }); assert.equal(result.status, 0, result.stdout + result.stderr); assert.match(result.stdout, /in step with the model/); }); test("CLI: --format json emits a parseable summary", { skip: skipWithoutModel }, () => { const result = spawnSync(process.execPath, [script, "--format", "json"], { encoding: "utf8", }); const summary = JSON.parse(result.stdout); assert.equal(summary.needs, 41); assert.equal(summary.questions, 161); assert.equal(summary.drifted, 0); }); test("CLI: --help exits 0, unknown argument exits 2", () => { assert.equal(spawnSync(process.execPath, [script, "--help"]).status, 0); assert.equal(spawnSync(process.execPath, [script, "--nope"]).status, 2); });