Files
sucker.severian.dev/tests/errors.test.ts
T
Severian a8b3669c17 Name the setting that did not come through
The instructions mark exactly two steps REQUIRED, and the two markers
sucker looks for are those two steps. So a failure can say which one is
missing — the custom prompt, the persona, or both — instead of sending
someone back to re-read a page they have already followed.

Truncation causes the same symptom, so it is still named, but second:
it only applies once the settings really are set.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
2026-09-11 06:06:36 +08:00

80 lines
3.2 KiB
TypeScript

/**
* 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));
});