mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-10-18 01:20:29 +00:00
Stop giving advice for situations nobody can cause
The previous commit split failures into explained and unexplained, but the split that matters is fixable and not. A request with no card, no greeting, or nothing in it cannot come out of Janitor. Telling someone to "open the chat until the bot's first message shows" is the dingus message wearing a new coat: instructions for something they did not do and cannot undo. Only the truncated card is a user's to fix, so only it is shown verbatim, with the context-size advice. The rest are ImpossibleRequest now: the reply says plainly that it is not their fault and there is nothing for them to fix, and asks them to report it, carrying a short tag so the report says which assumption broke rather than expecting the user to explain it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
This commit is contained in:
+43
-24
@@ -1,44 +1,63 @@
|
||||
/**
|
||||
* Which failures explain themselves, and which ask to be reported.
|
||||
* 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,
|
||||
TRUNCATED_CARD,
|
||||
isImpossibleRequest,
|
||||
isSuckerError,
|
||||
userFacingMessage,
|
||||
} from "../src/lib/errors.ts";
|
||||
|
||||
test("an explained failure is shown to the user as written", () => {
|
||||
for (const e of [NO_CARD, NO_GREETING, TOO_FEW_MESSAGES, TRUNCATED_CARD]) {
|
||||
assert.equal(userFacingMessage(e), e.message);
|
||||
assert.ok(!/dingus/i.test(e.message), "nobody is called a dingus for these");
|
||||
}
|
||||
});
|
||||
|
||||
test("an unexplained failure owns it and asks to be reported", () => {
|
||||
const msg = userFacingMessage(new TypeError("Cannot read properties of undefined"));
|
||||
assert.match(msg, /not your fault/i);
|
||||
assert.ok(msg.includes(CONTACT), "must say where to report it");
|
||||
// The internals must not leak into a user-facing reply.
|
||||
assert.ok(!msg.includes("Cannot read properties"));
|
||||
});
|
||||
|
||||
test("the truncated-card case keeps the context-size advice", () => {
|
||||
test("the one fixable failure is shown as written, with the advice that fixes it", () => {
|
||||
assert.equal(userFacingMessage(TRUNCATED_CARD), TRUNCATED_CARD.message);
|
||||
assert.match(TRUNCATED_CARD.message, /Context Size/);
|
||||
assert.ok(TRUNCATED_CARD.message.includes(CONTACT));
|
||||
});
|
||||
|
||||
test("recognised across module boundaries, not just by identity", () => {
|
||||
assert.ok(isSuckerError(new SuckerError("x")));
|
||||
// What a second copy of the class in another bundle would look like.
|
||||
assert.ok(isSuckerError({ name: "SuckerError", message: "x" }));
|
||||
assert.ok(!isSuckerError(new Error("x")));
|
||||
assert.ok(!isSuckerError(null));
|
||||
assert.ok(!isSuckerError("SuckerError"));
|
||||
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 [TRUNCATED_CARD, 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));
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user