mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-10-18 01:20:29 +00:00
Read the request by role, not by position
Some users' JanitorAI sends a "." user turn between the card and the greeting; others' does not. Sucker read fixed indices, so on the shorter shape every slot was off by one: first_mes was set to the name line, and the name slot did not exist at all. That TypeError was caught by the handler and reported as the "you dingus, read the directions" message, which blamed the user for a shape they never chose. src/lib/messages.ts now resolves the slots by role instead — the card is the system turn, the greeting is the first assistant turn, the user's latest input is the last user turn, and sucker's own previous reply is the last assistant turn. All four hold in both shapes and in a continued conversation, so the conversation token is found in either as well. Extraction now throws a description of what is missing rather than a TypeError, and tests/messages.test.ts pins both shapes. The tests need no framework: `npm test` runs node's built-in runner. Existing cards are unaffected: the update path already discards the extracted name and keeps the original. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
This commit is contained in:
@@ -0,0 +1,68 @@
|
||||
/**
|
||||
* Both request shapes JanitorAI sends, and a continued conversation.
|
||||
* Run with `npm test` — no test framework, just node's built-in runner.
|
||||
*/
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { selectMessageSlots } from "../src/lib/messages.ts";
|
||||
|
||||
const CARD =
|
||||
"<.>\n<Gaius's Persona>a tired innkeeper</Gaius's Persona><UserPersona>.</UserPersona>";
|
||||
const GREETING = "The clink of bottles is the only greeting you get.";
|
||||
const NAME = "{{user}}: Gaius";
|
||||
|
||||
/** What most clients send: a throwaway "." user turn before the greeting. */
|
||||
const withProbe = [
|
||||
{ role: "system", content: CARD },
|
||||
{ role: "user", content: "." },
|
||||
{ role: "assistant", content: GREETING },
|
||||
{ role: "user", content: NAME },
|
||||
];
|
||||
|
||||
/** What some clients send: no "." turn, so every index shifts by one. */
|
||||
const withoutProbe = [
|
||||
{ role: "system", content: CARD },
|
||||
{ role: "assistant", content: GREETING },
|
||||
{ role: "user", content: NAME },
|
||||
];
|
||||
|
||||
test("both shapes resolve to the same slots", () => {
|
||||
for (const [label, messages] of [
|
||||
["with probe", withProbe],
|
||||
["without probe", withoutProbe],
|
||||
] as const) {
|
||||
const slots = selectMessageSlots(messages);
|
||||
assert.equal(slots.system?.content, CARD, `${label}: card`);
|
||||
assert.equal(slots.greeting?.content, GREETING, `${label}: greeting`);
|
||||
assert.equal(slots.lastUser?.content, NAME, `${label}: name slot`);
|
||||
}
|
||||
});
|
||||
|
||||
test("the greeting stays the first assistant turn as a chat continues", () => {
|
||||
const continued = [
|
||||
...withProbe,
|
||||
{ role: "assistant", content: "Character updated (v2)." },
|
||||
{ role: "user", content: "{{user}}: [sucker:conv=abc123]" },
|
||||
];
|
||||
const slots = selectMessageSlots(continued);
|
||||
assert.equal(slots.greeting?.content, GREETING);
|
||||
// Sucker's own reply, where a conversation token would be found.
|
||||
assert.equal(slots.lastAssistant?.content, "Character updated (v2).");
|
||||
assert.equal(slots.lastUser?.content, "{{user}}: [sucker:conv=abc123]");
|
||||
});
|
||||
|
||||
test("a card with no role on the system turn still resolves", () => {
|
||||
const slots = selectMessageSlots([
|
||||
{ content: CARD },
|
||||
{ role: "assistant", content: GREETING },
|
||||
{ role: "user", content: NAME },
|
||||
]);
|
||||
assert.equal(slots.system?.content, CARD);
|
||||
});
|
||||
|
||||
test("nothing throws on empty or malformed input", () => {
|
||||
for (const input of [[], undefined, [{}, { role: "user" }]]) {
|
||||
assert.doesNotThrow(() => selectMessageSlots(input as never));
|
||||
}
|
||||
assert.equal(selectMessageSlots([]).greeting, undefined);
|
||||
});
|
||||
Reference in New Issue
Block a user