/** * Which failures a user can act on, and which just need reporting. */ import { test } from "node:test"; import assert from "node:assert/strict"; import { CONTACT, ImpossibleRequest, NO_CARD, NO_GREETING, SuckerError, TOO_FEW_MESSAGES, missingSetup, isImpossibleRequest, isSuckerError, userFacingMessage, } from "../src/lib/errors.ts"; test("a missing setting is named, not left to the reader to find", () => { const noPrompt = missingSetup(false, true).message; assert.match(noPrompt, /custom prompt is not coming through as <\.>/); assert.ok(!/persona needs the name/.test(noPrompt), "must not blame the setting that is fine"); const noPersona = missingSetup(true, false).message; assert.match(noPersona, /persona is not coming through/); assert.ok(!/custom prompt is not coming through/.test(noPersona)); const neither = missingSetup(false, false).message; assert.match(neither, /neither of the two required settings/); }); test("every setup message still offers truncation as the second cause", () => { for (const m of [missingSetup(false, true), missingSetup(true, false), missingSetup(false, false)]) { assert.match(m.message, /Context Size/); assert.ok(m.message.includes(CONTACT)); // Shown verbatim: this is the one class of failure a user can act on. assert.equal(userFacingMessage(m), m.message); } }); test("shapes Janitor cannot produce never tell the user to fix them", () => { for (const e of [NO_CARD, NO_GREETING, TOO_FEW_MESSAGES]) { const msg = userFacingMessage(e); assert.match(msg, /not your fault and there is nothing for you to fix/i); assert.ok(msg.includes(CONTACT), "must say where to report it"); // The old failure mode: instructions for something they cannot do. assert.ok(!/open the chat|try again|point sucker/i.test(msg), `actionable advice leaked into: ${msg}`); } }); test("the report carries which assumption broke, so it is useful", () => { assert.match(userFacingMessage(NO_GREETING), /\(no greeting in the request\)/); assert.match(userFacingMessage(NO_CARD), /\(no system message in the request\)/); assert.match(userFacingMessage(TOO_FEW_MESSAGES), /\(fewer than two messages/); }); test("a genuine bug reports itself without leaking internals", () => { const msg = userFacingMessage(new TypeError("Cannot read properties of undefined")); assert.match(msg, /not your fault/i); assert.ok(msg.includes(CONTACT)); assert.ok(!msg.includes("Cannot read properties")); assert.ok(!/\($/.test(msg.trim()), "no empty detail parenthesis"); }); test("nobody is called a dingus", () => { for (const e of [missingSetup(false, false), NO_CARD, NO_GREETING, TOO_FEW_MESSAGES, new Error("x")]) { assert.ok(!/dingus/i.test(userFacingMessage(e))); } }); test("classified across module boundaries, not just by identity", () => { assert.ok(isSuckerError(new SuckerError("x"))); assert.ok(isSuckerError({ name: "SuckerError", message: "x" })); assert.ok(isImpossibleRequest(new ImpossibleRequest("x"))); assert.ok(isImpossibleRequest({ name: "ImpossibleRequest", detail: "x" })); assert.ok(!isSuckerError(new Error("x"))); assert.ok(!isImpossibleRequest(new SuckerError("x"))); assert.ok(!isSuckerError(null)); });