mirror of
https://github.com/severian-dev/sucker.severian.dev.git
synced 2026-10-18 01:20:29 +00:00
e7c9adbcc5
The first pass read the name from the last user turn and the token from the last assistant turn. Both are wrong once a conversation continues. Only sucker's *first* reply carries a conversation token — later ones say "Character updated" or "unchanged" with no token in them — so reading the last assistant turn lost the link three turns in and would have created a duplicate card on every message. tokenCandidates() now searches the whole conversation, still preferring whatever the user just typed, since pasting a token is deliberate. The name is the first user turn after the greeting, not the last one. That is what the old code read at messages[3], and unlike the last turn it stays put as messages are appended. Tests cover a three-turn conversation for both. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01NX2SsmUqQftWTZAY4BDuFa
91 lines
3.4 KiB
TypeScript
91 lines
3.4 KiB
TypeScript
/**
|
|
* Both request shapes JanitorAI sends, and conversations that keep going.
|
|
* 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, tokenCandidates } 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";
|
|
const TOKEN = "[sucker:conv=abc123]";
|
|
|
|
/** What most clients send: a throwaway "." user turn before the greeting. */
|
|
const withProbe: { role: string; content: string }[] = [
|
|
{ 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: string; content: string }[] = [
|
|
{ role: "system", content: CARD },
|
|
{ role: "assistant", content: GREETING },
|
|
{ role: "user", content: NAME },
|
|
];
|
|
|
|
/** Three turns in. Only sucker's FIRST reply carries the token. */
|
|
const continued = [
|
|
...withProbe,
|
|
{ role: "assistant", content: `New character "Gaius" created (v1). ${TOKEN}` },
|
|
{ role: "user", content: "{{user}}: keep going" },
|
|
{ role: "assistant", content: "Character data unchanged (v1, message 2)." },
|
|
{ role: "user", content: "{{user}}: and again" },
|
|
];
|
|
|
|
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.nameSlot?.content, NAME, `${label}: name`);
|
|
}
|
|
});
|
|
|
|
test("the greeting and the name stay put as a conversation grows", () => {
|
|
const slots = selectMessageSlots(continued);
|
|
assert.equal(slots.greeting?.content, GREETING);
|
|
// Not "keep going" or "and again": the name is what started the chat.
|
|
assert.equal(slots.nameSlot?.content, NAME);
|
|
assert.equal(slots.lastUser?.content, "{{user}}: and again");
|
|
});
|
|
|
|
test("the token is still found once later replies stop carrying it", () => {
|
|
const found = tokenCandidates(continued).find((c) => c.includes("sucker:conv="));
|
|
assert.ok(found, "a token must still be discoverable three turns in");
|
|
assert.match(found!, /\[sucker:conv=abc123\]/);
|
|
});
|
|
|
|
test("a token the user just pasted outranks an older one", () => {
|
|
const candidates = tokenCandidates([
|
|
...withProbe,
|
|
{ role: "assistant", content: `created (v1). ${TOKEN}` },
|
|
{ role: "user", content: "{{user}}: [sucker:conv=zzz999]" },
|
|
]);
|
|
assert.match(candidates[0], /zzz999/);
|
|
});
|
|
|
|
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.doesNotThrow(() => tokenCandidates(input as never));
|
|
}
|
|
assert.equal(selectMessageSlots([]).greeting, undefined);
|
|
assert.deepEqual(tokenCandidates(undefined), []);
|
|
});
|