Centralize session scope, chat projections and typed task bridges. Keep historical interaction replies authoritative and publish terminal state to subscribers. Validate Server contracts before replacing generated artifacts.
48 lines
3.5 KiB
TypeScript
48 lines
3.5 KiB
TypeScript
import { expect, it } from "bun:test";
|
|
import type { UIMessageChunk } from "ai";
|
|
import { createChatRunAccumulator, publishRunEvent } from "../../src/routes/chat-run-accumulator.js";
|
|
import { createInitialStreamingMessages, type ActiveRun } from "../../src/routes/chatUiState.js";
|
|
|
|
it.each([false, true])("projects one event stream into reconnect and persisted messages (collaboration=%s)", (collaboration) => {
|
|
const run: ActiveRun = {
|
|
clientSessionId: "s", controller: new AbortController(), status: "running",
|
|
messages: createInitialStreamingMessages([], "查询"), pendingPermissions: new Map(), pendingQuestions: new Map(), subscribers: new Set(),
|
|
};
|
|
const chunks: UIMessageChunk[] = [];
|
|
const stream = createChatRunAccumulator({ run, messages: [{ id: "user", role: "user", parts: [{ type: "text", text: "查询" }] }],
|
|
messageId: "answer", sessionId: "s", collaboration, write: (chunk) => chunks.push(chunk), onStatus() {} });
|
|
stream.apply("progress", { id: "query", status: "running", title: "查询资料" });
|
|
stream.apply("token", { content: "已有数据" });
|
|
stream.apply("permission_request", { request_id: "permission", session_id: "s", permission: "shell", patterns: [], always: [], created_at: 1 });
|
|
stream.apply("permission_response", { request_id: "permission", reply: "once" });
|
|
stream.appendInput({ id: "extra", role: "user", parts: [{ type: "text", text: "补充" }] }, "补充");
|
|
stream.appendInput({ id: "extra", role: "user", parts: [] }, "重复");
|
|
stream.apply("final_answer", { content: "结论" });
|
|
stream.apply("done", {});
|
|
expect(run.status).toBe("completed");
|
|
expect(run.pendingPermissions.size).toBe(0);
|
|
expect(run.messages.at(-1)).toMatchObject({ id: "answer", content: "结论", permissions: [{ status: "approved_once" }], progress: [{ status: "completed" }, { status: "completed" }] });
|
|
const saved = stream.uiMessages();
|
|
expect(saved.map((message) => message.id)).toEqual(["user", "extra", "answer"]);
|
|
expect(saved.at(-1)?.parts).toEqual((run.messages.at(-1) as { parts: typeof saved[number]["parts"] }).parts);
|
|
expect(saved.at(-1)?.parts.some((part) => part.type === "data-progress")).toBe(!collaboration);
|
|
expect(chunks.filter((chunk) => chunk.type === "finish")).toHaveLength(1);
|
|
});
|
|
|
|
it.each(["done", "error", "aborted"])("publishes the same %s terminal state to reconnect subscribers and storage", (event) => {
|
|
const run: ActiveRun = {
|
|
clientSessionId: "s", controller: new AbortController(), status: "running",
|
|
messages: createInitialStreamingMessages([], "查询"), pendingPermissions: new Map(), pendingQuestions: new Map(), subscribers: new Set(),
|
|
};
|
|
const received: Array<{ event: string; data: Record<string, unknown> }> = [];
|
|
run.subscribers.add({ write: (event, data) => received.push({ event, data }), close() {} });
|
|
const stream = createChatRunAccumulator({ run, messages: [], messageId: "answer", sessionId: "s",
|
|
collaboration: false, write() {}, onStatus() {} });
|
|
publishRunEvent(run, "progress", { id: "tool", title: "正在处理", status: "running", phase: "tool" });
|
|
publishRunEvent(run, "token", { content: "部分结果" });
|
|
publishRunEvent(run, event, { session_id: "s", message: "执行已停止" });
|
|
const canonical = stream.uiMessages().at(-1)?.parts.find((part) => part.type === "data-run_status");
|
|
expect(received.at(-2)).toMatchObject({ event: "run_status", data: { status: event === "done" ? "completed" : event } });
|
|
expect(canonical).toMatchObject({ data: { status: received.at(-2)?.data.status } });
|
|
});
|