094ebd6118
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
175 lines
7.0 KiB
TypeScript
175 lines
7.0 KiB
TypeScript
/**
|
|
* LiveTailTracker unit tests: open-fragment accumulation into synthetic `partial_* start`
|
|
* messages, closure on stop / matching complete message / clear, origin preservation for
|
|
* subagent fragments, and the tail-keeping cap.
|
|
*/
|
|
import { describe, expect, it } from "vitest";
|
|
import {
|
|
assistantText,
|
|
partialText,
|
|
partialThinking,
|
|
partialToolCall,
|
|
partialToolCallOutput,
|
|
requestBegin,
|
|
thinkingMessage,
|
|
toolCall,
|
|
toolCallOutput,
|
|
userText,
|
|
withOrigin,
|
|
} from "@prismshadow/penguin-core";
|
|
import type {
|
|
PartialTextPayload,
|
|
PartialToolCallOutputPayload,
|
|
PartialToolCallPayload,
|
|
} from "@prismshadow/penguin-core";
|
|
import { LiveTailTracker } from "../src/runtime/live-tail.js";
|
|
|
|
const SID = "s1";
|
|
|
|
describe("live-tail", () => {
|
|
it("accumulates text/thinking/tool-call/tool-output fragments into synthetic starts with full prefixes", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialThinking("start"));
|
|
t.observe(SID, partialThinking("delta", "let me "));
|
|
t.observe(SID, partialThinking("delta", "think"));
|
|
t.observe(SID, partialText("start", ""));
|
|
t.observe(SID, partialText("delta", "Hel"));
|
|
t.observe(SID, partialText("delta", "lo"));
|
|
t.observe(SID, partialToolCall({ eventType: "start", name: "exec_command", toolCallId: "t1" }));
|
|
t.observe(
|
|
SID,
|
|
partialToolCall({
|
|
eventType: "delta",
|
|
name: "",
|
|
arguments: '{"cmd":"ls"}',
|
|
toolCallId: "t1",
|
|
}),
|
|
);
|
|
|
|
const frags = t.fragments(SID);
|
|
expect(frags.map((f) => (f.payload as { type: string }).type)).toEqual([
|
|
"partial_thinking",
|
|
"partial_text",
|
|
"partial_tool_call",
|
|
]);
|
|
// Every synthetic message is a start carrying the accumulated content.
|
|
for (const f of frags) {
|
|
expect((f.payload as { event_type: string }).event_type).toBe("start");
|
|
}
|
|
expect((frags[0]!.payload as { thinking: string }).thinking).toBe("let me think");
|
|
expect((frags[1]!.payload as PartialTextPayload).text).toBe("Hello");
|
|
const call = frags[2]!.payload as PartialToolCallPayload;
|
|
expect(call.name).toBe("exec_command");
|
|
expect(call.arguments).toBe('{"cmd":"ls"}');
|
|
expect(call.tool_call_id).toBe("t1");
|
|
});
|
|
|
|
it("stop closes the fragment; a matching complete message also closes it (stop-less closure)", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", "a"));
|
|
t.observe(SID, partialThinking("start", "b"));
|
|
expect(t.fragments(SID)).toHaveLength(2);
|
|
t.observe(SID, partialText("stop"));
|
|
expect(t.fragments(SID)).toHaveLength(1);
|
|
// Complete thinking closes the open thinking fragment even without a stop.
|
|
t.observe(SID, thinkingMessage("b (complete)"));
|
|
expect(t.fragments(SID)).toEqual([]);
|
|
});
|
|
|
|
it("a complete tool_call/tool_call_output closes only the fragment with the same tool_call_id", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialToolCallOutput({ eventType: "start", toolCallId: "t1" }));
|
|
t.observe(
|
|
SID,
|
|
partialToolCallOutput({ eventType: "delta", output: "line 1\n", toolCallId: "t1" }),
|
|
);
|
|
t.observe(SID, partialToolCall({ eventType: "start", name: "x", toolCallId: "t2" }));
|
|
t.observe(SID, toolCallOutput({ output: "other", toolCallId: "t9" }));
|
|
expect(t.fragments(SID)).toHaveLength(2);
|
|
t.observe(SID, toolCallOutput({ output: "line 1\nline 2\n", toolCallId: "t1" }));
|
|
t.observe(SID, toolCall({ name: "x", arguments: "{}", toolCallId: "t2" }));
|
|
expect(t.fragments(SID)).toEqual([]);
|
|
});
|
|
|
|
it("a user text (steering) does not close the model's open text fragment; events are ignored", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", "prefix"));
|
|
t.observe(SID, userText("steer this way"));
|
|
t.observe(SID, requestBegin());
|
|
const frags = t.fragments(SID);
|
|
expect(frags).toHaveLength(1);
|
|
expect((frags[0]!.payload as PartialTextPayload).text).toBe("prefix");
|
|
// The assistant's complete text does close it.
|
|
t.observe(SID, assistantText("prefix done"));
|
|
expect(t.fragments(SID)).toEqual([]);
|
|
});
|
|
|
|
it("preserves origin: subagent fragments are keyed and emitted with their origin chain", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", "parent "));
|
|
t.observe(SID, withOrigin(partialText("start", "child "), "c1"));
|
|
t.observe(SID, withOrigin(partialText("delta", "progress"), "c1"));
|
|
// The parent's complete text closes only the parent fragment (same type, different origin).
|
|
t.observe(SID, assistantText("parent done"));
|
|
const frags = t.fragments(SID);
|
|
expect(frags).toHaveLength(1);
|
|
expect(frags[0]!.origin).toEqual(["c1"]);
|
|
expect((frags[0]!.payload as PartialTextPayload).text).toBe("child progress");
|
|
});
|
|
|
|
it("tool-output images ride the synthetic start (whole-set semantics)", () => {
|
|
const dataUrl = "data:image/png;base64,AAAA";
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialToolCallOutput({ eventType: "start", toolCallId: "t1" }));
|
|
t.observe(
|
|
SID,
|
|
partialToolCallOutput({
|
|
eventType: "delta",
|
|
output: "image/png",
|
|
toolCallId: "t1",
|
|
images: [dataUrl],
|
|
}),
|
|
);
|
|
const frag = t.fragments(SID)[0]!.payload as PartialToolCallOutputPayload;
|
|
expect(frag.output).toBe("image/png");
|
|
expect(frag.images).toEqual([dataUrl]);
|
|
});
|
|
|
|
it("caps a runaway fragment by keeping the TAIL (the seed then joins the live deltas seamlessly)", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", ""));
|
|
const chunk = "x".repeat(64 * 1024);
|
|
for (let i = 0; i < 12; i += 1) t.observe(SID, partialText("delta", chunk)); // 768KB total
|
|
t.observe(SID, partialText("delta", "THE-TAIL"));
|
|
const text = (t.fragments(SID)[0]!.payload as PartialTextPayload).text;
|
|
expect(text.length).toBeLessThanOrEqual(512 * 1024 + 64 * 1024);
|
|
expect(text.endsWith("THE-TAIL")).toBe(true);
|
|
});
|
|
|
|
it("clear drops every fragment of the session; other sessions are unaffected", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", "a"));
|
|
t.observe("s2", partialText("start", "b"));
|
|
t.clear(SID);
|
|
expect(t.fragments(SID)).toEqual([]);
|
|
expect(t.fragments("s2")).toHaveLength(1);
|
|
});
|
|
|
|
it("a lenient delta without a start still opens a fragment (mirrors core PartialAggregator); a bare stop is a no-op", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("stop"));
|
|
expect(t.fragments(SID)).toEqual([]);
|
|
t.observe(SID, partialText("delta", "late"));
|
|
expect((t.fragments(SID)[0]!.payload as PartialTextPayload).text).toBe("late");
|
|
});
|
|
|
|
it("a reopening start replaces the previous same-key fragment", () => {
|
|
const t = new LiveTailTracker();
|
|
t.observe(SID, partialText("start", "first"));
|
|
t.observe(SID, partialText("start", "second"));
|
|
const frags = t.fragments(SID);
|
|
expect(frags).toHaveLength(1);
|
|
expect((frags[0]!.payload as PartialTextPayload).text).toBe("second");
|
|
});
|
|
});
|