- scripts/agent.sh <name>: launches interactive pi TUI in the container with contracts + optional mission + agent identity + named session + optional workspace/tools; the Mosaic alternative to vanilla pi - pi adapter: MOSAIC_INTERACTIVE branch (clean TUI, no -p, no initial prompt); headless exec rebuilt via positional args (no word-splitting on the request); MOSAIC_AGENT_NAME optional in headless - loader: AGENT IDENTITY section when the launcher names the agent - compose: fixed command removed (request defaults live in run-agent.sh); MOSAIC_INTERACTIVE/MOSAIC_AGENT_NAME passthrough - docs/TOOLS.md: full on-demand tool reference; AGENTS.md routes to it - RELEASE -> 0.0.8 (container change); build verified Closes #35
229 lines
10 KiB
JavaScript
229 lines
10 KiB
JavaScript
"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);
|