"use strict"; // Tests for unslop-check.js. Run: node test-unslop-check.js // Exit 0 = all pass. Cases include a red control (slop must fail) and a green // control (clean prose must pass) per evidence discipline. const assert = require("node:assert"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { spawnSync } = require("node:child_process"); const { checkText, stripCode, loadLists } = require("./unslop-check.js"); const SLOP = `Certainly! Let me delve into the evolving tapestry of database technology — it’s truly “pivotal” — and intricate. In order to understand it — we should leverage this interplay of systems — deeply. I hope this helps!`; const CLEAN = `The loader parses the file and validates each row. Rows that fail are logged and skipped. We measured a range from 1 to 10 seconds. Use "straight quotes" and commas, not dashes. That is the whole finding.`; // Code-stripping control: banned words inside code must not count. const WITH_CODE = [ "The config uses `utilize=false` internally.", "```", "delve tapestry — pivotal", "```", "The config file sets one flag. It is parsed at startup.", ].join("\n"); const results = []; function t(name, fn) { try { fn(); results.push([name, true]); } catch (e) { results.push([name, false]); console.error(`FAIL ${name}: ${e.message}`); } } t("slop fixture is flagged (red control)", () => { const r = checkText(SLOP); assert.ok(!r.clean, "slop must not be clean"); const details = r.findings.map((f) => f.detail).join("; "); assert.ok(r.findings.some((f) => f.detail.includes("delve")), `delve missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("tapestry")), `tapestry missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("pivotal")), `pivotal missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("em dash")), `em dash missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("curly")), `curly missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("in order to")), `in order to missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("i hope this helps")), `chatbot phrase missing: ${details}`); assert.ok(r.findings.some((f) => f.detail.includes("certainly")), `certainly missing: ${details}`); }); t("clean fixture passes (green control)", () => { const r = checkText(CLEAN); assert.deepStrictEqual(r.findings, [], `unexpected findings: ${JSON.stringify(r.findings)}`); }); t("numeric range is not a false range flag", () => { const r = checkText(CLEAN); assert.ok(!r.findings.some((f) => f.rule === "pattern"), "must not flag numeric ranges"); }); t("code blocks and inline code are stripped", () => { const r = checkText(WITH_CODE); assert.deepStrictEqual(r.findings, [], `code leaked into check: ${JSON.stringify(r.findings)}`); }); t("stripCode removes fenced and inline code", () => { const s = stripCode("a `x — y` b\n```\ndelve\n```\nc"); assert.ok(!s.includes("delve"), "fenced code not stripped"); assert.ok(!s.includes("—"), "inline code not stripped"); assert.ok(s.includes("a") && s.includes("b") && s.includes("c"), "prose lost"); }); t("not-just-but pattern is detected", () => { const r = checkText("This is not just a cache but a coordination layer."); assert.ok(r.findings.some((f) => f.rule === "pattern"), "pattern missed"); }); t("light dash use is not flagged (below threshold)", () => { const prose = "The loader parses each row and validates it against the schema. Rows that fail " + "are logged — with their line numbers — and skipped. The operator reviews the log " + "daily and reconciles the rejects against the source system by hand, which takes " + "a few minutes and has never once produced a discrepancy worth acting on."; const r = checkText(prose); assert.ok(!r.findings.some((f) => f.detail.includes("dash")), "2 dashes in ~330 chars must not flag"); }); t("dash overuse is flagged (above threshold)", () => { const r = checkText("One — two — three — four. That is the whole sentence."); assert.ok(r.findings.some((f) => f.detail.includes("em dash")), "4 dashes in 50 chars must flag"); }); // ── C1: lists.json machine source ────────────────────────────────────── // The committed lists are the single source of truth; these tests pin the // file's validity, its shape, and the loader's fail-closed behavior. const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), "unslop-c1-")); const tmp = (n) => path.join(tmpdir, n); function brokenVariant(mutate) { const l = JSON.parse(JSON.stringify(loadLists())); mutate(l); return l; } function writeTmp(name, data) { const f = tmp(name); fs.writeFileSync(f, typeof data === "string" ? data : JSON.stringify(data)); return f; } t("lists.json (the real file) validates and is pinned in size", () => { const l = loadLists(); assert.strictEqual(l.version, 1); // Counts pin the migration: 16 words, 17 phrases, 3 punct, 1 pattern moved // from the old inline constants. Changing a count means changing this test // too, consciously. assert.strictEqual(l.words.length, 16, "word count drifted"); assert.strictEqual(l.phrases.length, 17, "phrase count drifted"); assert.strictEqual(l.punct.length, 3, "punct count drifted"); assert.strictEqual(l.patterns.length, 1, "pattern count drifted"); assert.ok(l.convention.length > 50, "mention convention must be present"); assert.ok(l.punctPolicy.length > 50, "punct divergence policy must be present"); for (const e of [...l.words, ...l.phrases, ...l.punct, ...l.patterns]) { assert.ok(e.source && e.source.trim(), `entry missing source: ${JSON.stringify(e)}`); } }); t("loader rejects an empty file", () => { const f = writeTmp("empty.json", ""); assert.throws(() => loadLists(f), /empty file/); }); t("loader rejects unparseable JSON", () => { const f = writeTmp("bad.json", "{nope"); assert.throws(() => loadLists(f), /unparseable/); }); t("loader rejects a missing file", () => { assert.throws(() => loadLists(tmp("does-not-exist.json")), /cannot read/); }); t("loader rejects missing keys", () => { const f = writeTmp("nokeys.json", { version: 1 }); assert.throws(() => loadLists(f), /missing key/); }); t("loader rejects an emptied word list", () => { const f = writeTmp("emptywords.json", brokenVariant((l) => { l.words = []; })); assert.throws(() => loadLists(f), /words must be a non-empty array/); }); t("loader rejects entries without provenance", () => { const f = writeTmp("nosource.json", brokenVariant((l) => { delete l.phrases[0].source; })); assert.throws(() => loadLists(f), /source/); }); t("loader rejects duplicate values", () => { const f = writeTmp("dup.json", brokenVariant((l) => { l.words.push({ ...l.words[0] }); })); assert.throws(() => loadLists(f), /duplicate/); }); t("loader rejects a non-compiling pattern regex", () => { const f = writeTmp("badregex.json", brokenVariant((l) => { l.patterns[0].regex = "("; })); assert.throws(() => loadLists(f), /does not compile/); }); t("CLI exits 2 on broken lists (red control)", () => { const f = writeTmp("cli-broken.json", ""); const r = spawnSync(process.execPath, [path.join(__dirname, "unslop-check.js")], { input: "some prose", encoding: "utf8", env: { ...process.env, UNSLOP_LISTS: f }, }); assert.strictEqual(r.status, 2, `expected exit 2, got ${r.status} (stderr: ${r.stderr})`); assert.ok(r.stderr.includes("lists.json invalid"), `stderr must name the fault: ${r.stderr}`); }); t("CLI honors UNSLOP_LISTS for a valid file (green control)", () => { const r = spawnSync(process.execPath, [path.join(__dirname, "unslop-check.js")], { input: "plain prose with no tells at all", encoding: "utf8", env: { ...process.env, UNSLOP_LISTS: path.join(__dirname, "lists.json") }, }); assert.strictEqual(r.status, 0, `expected exit 0, got ${r.status} (stderr: ${r.stderr})`); }); // ── Review follow-up (rev-code-01, 2026-08-19): F1, F2, F3, S3 ──────── t("loader rejects non-finite thresholds (F1)", () => { // Infinity cannot round-trip JSON.stringify, so the fixture is a raw string // edit of the real file — exactly the hand-edit that produced the finding. const real = fs.readFileSync(path.join(__dirname, "lists.json"), "utf8"); const f = writeTmp("inf-threshold.json", real.replace('"punctMinCount": 3', '"punctMinCount": 1e999')); assert.ok(real !== fs.readFileSync(f, "utf8") || !real.includes('"punctMinCount": 3'), "fixture mutation did not apply; test is vacuous"); assert.throws(() => loadLists(f), /finite/); const f2 = writeTmp("inf-density.json", real.replace('"punctDensityPer1k": 1.0', '"punctDensityPer1k": 1e999')); assert.throws(() => loadLists(f2), /finite/); }); t("curly-apostrophe phrase rendering is flagged (F2 red control)", () => { const r = checkText("Here\u2019s the honest truth about the deploy."); assert.ok(!r.clean, "curly apostrophe must not defeat phrase matching"); assert.ok(r.findings.some((x) => x.detail.includes("here's the honest truth")), `main entry must match, got: ${JSON.stringify(r.findings)}`); }); t("curly apostrophes still fire the punct rule alongside phrases (F2 ordering)", () => { // Normalization for phrases must not eat the punct signal: four curly // quotes in short text must flag punct, not only the phrase. const r = checkText("It\u2019s \u2019one\u2019 \u2019two\u2019 \u2019three\u2019 \u2019four\u2019 done."); assert.ok(r.findings.some((f) => f.rule === "punct"), `punct must fire on original text: ${JSON.stringify(r.findings)}`); }); t("loader rejects non-lowercase phrase values (F3)", () => { const f = writeTmp("cap-phrase.json", brokenVariant((l) => { l.phrases[0].value = "Worth Stating Plainly"; })); assert.throws(() => loadLists(f), /lowercase/); }); t("CLI exits 2 on unreadable input file (S3)", () => { const r = spawnSync(process.execPath, [path.join(__dirname, "unslop-check.js"), tmp("definitely-absent.txt")], { encoding: "utf8", }); assert.strictEqual(r.status, 2, `expected exit 2, got ${r.status} (stderr: ${r.stderr})`); assert.ok(r.stderr.includes("cannot read input"), `stderr must name the fault: ${r.stderr}`); }); let failed = 0; for (const [name, ok] of results) { console.log(`${ok ? "PASS" : "FAIL"} ${name}`); if (!ok) failed++; } console.log(`${results.length - failed}/${results.length} passed`); try { fs.rmSync(tmpdir, { recursive: true, force: true }); } catch {} process.exit(failed ? 1 : 0);