openclaw/apps/web/app/components/chat-panel.stream-parser.test.ts

98 lines
3.2 KiB
TypeScript

import { describe, expect, it } from "vitest";
import { createStreamParser } from "./chat-panel";
describe("createStreamParser", () => {
it("treats user-message as turn boundary and keeps user/assistant sequence stable", () => {
const parser = createStreamParser();
parser.processEvent({ type: "user-message", id: "u-1", text: "Draft an email" });
parser.processEvent({ type: "text-start" });
parser.processEvent({ type: "text-delta", delta: "Sure — drafting now." });
parser.processEvent({ type: "text-end" });
parser.processEvent({ type: "user-message", id: "u-2", text: "Also include follow-up" });
parser.processEvent({ type: "text-start" });
parser.processEvent({ type: "text-delta", delta: "Added a follow-up section." });
parser.processEvent({ type: "text-end" });
expect(parser.getParts()).toEqual([
{ type: "user-message", id: "u-1", text: "Draft an email" },
{ type: "text", text: "Sure — drafting now." },
{ type: "user-message", id: "u-2", text: "Also include follow-up" },
{ type: "text", text: "Added a follow-up section." },
]);
});
it("accumulates tool input/output by toolCallId and preserves terminal status", () => {
const parser = createStreamParser();
parser.processEvent({
type: "tool-input-start",
toolCallId: "tool-1",
toolName: "searchDocs",
});
parser.processEvent({
type: "tool-input-available",
toolCallId: "tool-1",
input: { query: "workspace lock" },
});
parser.processEvent({
type: "tool-output-available",
toolCallId: "tool-1",
output: { hits: 3 },
});
parser.processEvent({
type: "tool-input-start",
toolCallId: "tool-2",
toolName: "writeFile",
});
parser.processEvent({
type: "tool-output-error",
toolCallId: "tool-2",
errorText: "permission denied",
});
expect(parser.getParts()).toEqual([
{
type: "dynamic-tool",
toolCallId: "tool-1",
toolName: "searchDocs",
state: "output-available",
input: { query: "workspace lock" },
output: { hits: 3 },
},
{
type: "dynamic-tool",
toolCallId: "tool-2",
toolName: "writeFile",
state: "error",
input: {},
output: { error: "permission denied" },
},
]);
});
it("closes reasoning state on reasoning-end to prevent stuck streaming badges", () => {
const parser = createStreamParser();
parser.processEvent({ type: "reasoning-start" });
parser.processEvent({ type: "reasoning-delta", delta: "Planning edits..." });
parser.processEvent({ type: "reasoning-end" });
expect(parser.getParts()).toEqual([
{ type: "reasoning", text: "Planning edits..." },
]);
});
it("ignores unknown events without corrupting previously parsed parts", () => {
const parser = createStreamParser();
parser.processEvent({ type: "text-start" });
parser.processEvent({ type: "text-delta", delta: "Stable output" });
parser.processEvent({ type: "unrecognized-event", payload: "noise" });
parser.processEvent({ type: "text-end" });
expect(parser.getParts()).toEqual([{ type: "text", text: "Stable output" }]);
});
});