149 lines
6.2 KiB
JavaScript
149 lines
6.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|||
|
|
import test from "node:test";
|
||
|
|
import {
|
||
|
|
appendStreamingDelta,
|
||
|
|
asStreamingAssistantMetadata,
|
||
|
|
clearStreamingAssistantTrace,
|
||
|
|
createStreamingAssistantMetadata,
|
||
|
|
createStreamingAttemptState,
|
||
|
|
finalizeStreamingAssistant,
|
||
|
|
promoteStreamingAssistantTrace,
|
||
|
|
setStreamingAssistantSegment,
|
||
|
|
transitionStreamingToolCall,
|
||
|
|
} from "../src/lib/chat-stream-presentation.ts";
|
||
|
|
|
||
|
|
function assistantMessage(content = "") {
|
||
|
|
return {
|
||
|
|
id: "temp-assistant-test",
|
||
|
|
createdAt: "2026-08-30T00:00:00.000Z",
|
||
|
|
role: "assistant",
|
||
|
|
content,
|
||
|
|
name: null,
|
||
|
|
metadata: createStreamingAssistantMetadata(),
|
||
|
|
};
|
||
|
|
}
|
||
|
|
|
||
|
|
function metadata(messages) {
|
||
|
|
return asStreamingAssistantMetadata(messages.at(-1)?.metadata);
|
||
|
|
}
|
||
|
|
|
||
|
|
test("a tool boundary promotes the current segment and the next delta starts the answer in its place", () => {
|
||
|
|
let messages = [assistantMessage()];
|
||
|
|
let attempt = appendStreamingDelta(createStreamingAttemptState(), "I'll search for that.");
|
||
|
|
messages = setStreamingAssistantSegment(messages, "temp-assistant-", attempt.segmentText);
|
||
|
|
|
||
|
|
const toolTransition = transitionStreamingToolCall(attempt, {
|
||
|
|
toolCallId: "search-1",
|
||
|
|
status: "initiated",
|
||
|
|
});
|
||
|
|
attempt = toolTransition.attempt;
|
||
|
|
messages = promoteStreamingAssistantTrace(messages, "temp-assistant-", toolTransition.traceText ?? "");
|
||
|
|
|
||
|
|
assert.equal(messages.at(-1).content, "");
|
||
|
|
assert.deepEqual(metadata(messages), {
|
||
|
|
kind: "streaming_assistant",
|
||
|
|
ephemeralTrace: "I'll search for that.",
|
||
|
|
traceRevision: 1,
|
||
|
|
});
|
||
|
|
|
||
|
|
attempt = appendStreamingDelta(attempt, "Here is the answer");
|
||
|
|
messages = setStreamingAssistantSegment(messages, "temp-assistant-", attempt.segmentText);
|
||
|
|
|
||
|
|
assert.equal(messages.at(-1).content, "Here is the answer");
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, null);
|
||
|
|
|
||
|
|
messages = finalizeStreamingAssistant(messages, "temp-assistant-", "Here is the answer.");
|
||
|
|
assert.equal(messages.at(-1).content, "Here is the answer.");
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, null);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("a later tool round replaces the previous trace and parallel initiated calls do not erase it", () => {
|
||
|
|
let messages = [assistantMessage("First trace")];
|
||
|
|
let attempt = appendStreamingDelta(createStreamingAttemptState(), "First trace");
|
||
|
|
let transition = transitionStreamingToolCall(attempt, { toolCallId: "tool-1", status: "initiated" });
|
||
|
|
attempt = transition.attempt;
|
||
|
|
messages = promoteStreamingAssistantTrace(messages, "temp-assistant-", transition.traceText ?? "");
|
||
|
|
|
||
|
|
attempt = appendStreamingDelta(attempt, "I need one more lookup.");
|
||
|
|
messages = setStreamingAssistantSegment(messages, "temp-assistant-", attempt.segmentText);
|
||
|
|
transition = transitionStreamingToolCall(attempt, { toolCallId: "tool-2", status: "initiated" });
|
||
|
|
attempt = transition.attempt;
|
||
|
|
messages = promoteStreamingAssistantTrace(messages, "temp-assistant-", transition.traceText ?? "");
|
||
|
|
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, "I need one more lookup.");
|
||
|
|
assert.equal(metadata(messages)?.traceRevision, 2);
|
||
|
|
|
||
|
|
transition = transitionStreamingToolCall(attempt, { toolCallId: "tool-3", status: "initiated" });
|
||
|
|
assert.equal(transition.startedNewToolCall, true);
|
||
|
|
assert.equal(transition.traceText, null);
|
||
|
|
messages = promoteStreamingAssistantTrace(messages, "temp-assistant-", transition.traceText ?? "");
|
||
|
|
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, "I need one more lookup.");
|
||
|
|
assert.equal(metadata(messages)?.traceRevision, 2);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("terminal and duplicate tool events never promote or reset the in-progress segment", () => {
|
||
|
|
const initial = appendStreamingDelta(createStreamingAttemptState(), "candidate");
|
||
|
|
const completed = transitionStreamingToolCall(initial, { toolCallId: "tool-1", status: "completed" });
|
||
|
|
|
||
|
|
assert.equal(completed.startedNewToolCall, false);
|
||
|
|
assert.strictEqual(completed.attempt, initial);
|
||
|
|
|
||
|
|
const initiated = transitionStreamingToolCall(initial, { toolCallId: "tool-1", status: "initiated" });
|
||
|
|
const withNextSegment = appendStreamingDelta(initiated.attempt, "next segment");
|
||
|
|
const duplicate = transitionStreamingToolCall(withNextSegment, { toolCallId: "tool-1", status: "initiated" });
|
||
|
|
|
||
|
|
assert.equal(duplicate.startedNewToolCall, false);
|
||
|
|
assert.strictEqual(duplicate.attempt, withNextSegment);
|
||
|
|
assert.equal(duplicate.attempt.segmentText, "next segment");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("a fresh per-attempt accumulator can replay and reclassify an already-seen tool round", () => {
|
||
|
|
let messages = [assistantMessage("stale partial answer")];
|
||
|
|
let replayAttempt = appendStreamingDelta(createStreamingAttemptState(), "Replayed trace");
|
||
|
|
messages = setStreamingAssistantSegment(messages, "temp-assistant-", replayAttempt.segmentText);
|
||
|
|
|
||
|
|
const replayedTool = transitionStreamingToolCall(replayAttempt, { toolCallId: "same-tool-id", status: "initiated" });
|
||
|
|
messages = promoteStreamingAssistantTrace(messages, "temp-assistant-", replayedTool.traceText ?? "");
|
||
|
|
|
||
|
|
assert.equal(replayedTool.startedNewToolCall, true);
|
||
|
|
assert.equal(messages.at(-1).content, "");
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, "Replayed trace");
|
||
|
|
});
|
||
|
|
|
||
|
|
test("message helpers preserve identity when no matching temporary assistant exists", () => {
|
||
|
|
const messages = [
|
||
|
|
{
|
||
|
|
id: "persisted-assistant",
|
||
|
|
createdAt: "2026-08-30T00:00:00.000Z",
|
||
|
|
role: "assistant",
|
||
|
|
content: "Persisted",
|
||
|
|
name: null,
|
||
|
|
metadata: null,
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
assert.strictEqual(setStreamingAssistantSegment(messages, "temp-assistant-", "new"), messages);
|
||
|
|
assert.strictEqual(promoteStreamingAssistantTrace(messages, "temp-assistant-", "trace"), messages);
|
||
|
|
assert.strictEqual(finalizeStreamingAssistant(messages, "temp-assistant-", "done"), messages);
|
||
|
|
assert.strictEqual(clearStreamingAssistantTrace(messages, "temp-assistant-"), messages);
|
||
|
|
});
|
||
|
|
|
||
|
|
test("a terminal error clears the ephemeral trace without discarding a partial answer", () => {
|
||
|
|
let messages = [
|
||
|
|
{
|
||
|
|
...assistantMessage("Partial answer"),
|
||
|
|
metadata: {
|
||
|
|
...createStreamingAssistantMetadata(),
|
||
|
|
ephemeralTrace: "Still checking one source.",
|
||
|
|
traceRevision: 1,
|
||
|
|
},
|
||
|
|
},
|
||
|
|
];
|
||
|
|
|
||
|
|
messages = clearStreamingAssistantTrace(messages, "temp-assistant-");
|
||
|
|
|
||
|
|
assert.equal(messages.at(-1).content, "Partial answer");
|
||
|
|
assert.equal(metadata(messages)?.ephemeralTrace, null);
|
||
|
|
});
|