Files
2026-07-13 13:39:12 +08:00

172 lines
5.6 KiB
TypeScript

/**
* Unit tests for the Kiro inline `<thinking>` stream splitter.
*
* Ported from decolua/9router#1273 (tests/unit/kiroThinking.test.js).
* Tests are framework-agnostic: Node.js native test runner (tsx/esm).
*/
import { describe, it } from "node:test";
import assert from "node:assert/strict";
import { splitInlineThinking, flushPendingThinking } from "../../open-sse/executors/kiroThinking.ts";
/** Build a fresh state + recorders for each test. */
function makeHarness() {
const state = { thinkingMode: false, pendingTag: "" };
const contentParts: string[] = [];
const reasoningParts: string[] = [];
const onContent = (s: string) => contentParts.push(s);
const onReasoning = (s: string) => reasoningParts.push(s);
const feed = (raw: string | null | undefined) =>
splitInlineThinking(state, raw, onContent, onReasoning);
const flush = () => flushPendingThinking(state, onContent, onReasoning);
return {
state,
feed,
flush,
get content() {
return contentParts.join("");
},
get reasoning() {
return reasoningParts.join("");
},
};
}
describe("splitInlineThinking", () => {
it("passes through plain content with no tags", () => {
const h = makeHarness();
h.feed("Bonjour, this is just an answer.");
h.flush();
assert.equal(h.content, "Bonjour, this is just an answer.");
assert.equal(h.reasoning, "");
assert.equal(h.state.thinkingMode, false);
assert.equal(h.state.pendingTag, "");
});
it("splits a single-shot input with one full block", () => {
const h = makeHarness();
h.feed("Hello <thinking>secret thoughts</thinking> world");
h.flush();
assert.equal(h.content, "Hello world");
assert.equal(h.reasoning, "secret thoughts");
assert.equal(h.state.thinkingMode, false);
});
it("handles a tag split across two slices (open tag)", () => {
// The opening `<thinking>` arrives across two reads.
const h = makeHarness();
h.feed("Hi <thi");
assert.equal(h.content, "Hi "); // only the safe prefix is flushed
assert.equal(h.state.pendingTag, "<thi");
h.feed("nking>thoughts</thinking> bye");
h.flush();
assert.equal(h.content, "Hi bye");
assert.equal(h.reasoning, "thoughts");
assert.equal(h.state.pendingTag, "");
});
it("handles a tag split across two slices (close tag)", () => {
const h = makeHarness();
h.feed("<thinking>step 1</thi");
assert.equal(h.reasoning, "step 1");
assert.equal(h.state.thinkingMode, true);
assert.equal(h.state.pendingTag, "</thi");
h.feed("nking>final answer");
h.flush();
assert.equal(h.content, "final answer");
assert.equal(h.reasoning, "step 1");
assert.equal(h.state.thinkingMode, false);
});
it("handles a tag split character-by-character", () => {
const h = makeHarness();
const stream = "before <thinking>hidden</thinking>after";
for (const ch of stream) h.feed(ch);
h.flush();
assert.equal(h.content, "before after");
assert.equal(h.reasoning, "hidden");
assert.equal(h.state.thinkingMode, false);
assert.equal(h.state.pendingTag, "");
});
it("handles multiple thinking blocks in sequence", () => {
const h = makeHarness();
h.feed("<thinking>plan A</thinking>step1<thinking>plan B</thinking>step2");
h.flush();
assert.equal(h.content, "step1step2");
assert.equal(h.reasoning, "plan Aplan B");
assert.equal(h.state.thinkingMode, false);
});
it("emits leftover content when the stream ends mid-tag", () => {
const h = makeHarness();
h.feed("ok <thi");
assert.equal(h.state.pendingTag, "<thi");
h.flush();
// Not in thinking mode — leftover goes to content.
assert.equal(h.content, "ok <thi");
assert.equal(h.reasoning, "");
});
it("emits leftover reasoning when the stream ends mid-close-tag", () => {
const h = makeHarness();
h.feed("<thinking>partial</thi");
assert.equal(h.state.thinkingMode, true);
assert.equal(h.state.pendingTag, "</thi");
h.flush();
// Leftover stays in reasoning because we never saw the close tag.
assert.equal(h.reasoning, "partial</thi");
assert.equal(h.content, "");
});
it("does not consume tag-shaped content that isn't a real tag boundary", () => {
const h = makeHarness();
h.feed("a<b>c</b>d");
h.flush();
assert.equal(h.content, "a<b>c</b>d");
assert.equal(h.reasoning, "");
});
it("only holds back trailing characters that look like a partial OPEN tag (outside thinking mode)", () => {
const h = makeHarness();
h.feed("answer text <think");
assert.equal(h.content, "answer text "); // safe prefix flushed eagerly
assert.equal(h.state.pendingTag, "<think");
h.feed("ing>secret</thinking>tail");
h.flush();
assert.equal(h.content, "answer text tail");
assert.equal(h.reasoning, "secret");
});
it("does not hold back partial CLOSE tag while outside thinking mode", () => {
const h = makeHarness();
h.feed("answer text </think");
h.feed(" something else");
h.flush();
assert.equal(h.content, "answer text </think something else");
assert.equal(h.reasoning, "");
});
it("survives empty / null / undefined slices", () => {
const h = makeHarness();
h.feed("");
h.feed(undefined);
h.feed(null);
h.feed("hello");
h.flush();
assert.equal(h.content, "hello");
assert.equal(h.reasoning, "");
});
it("flushPendingThinking is a no-op when nothing is pending", () => {
const h = makeHarness();
h.feed("just content");
h.flush();
h.flush(); // second flush should be a no-op
assert.equal(h.content, "just content");
assert.equal(h.reasoning, "");
});
});