Files
next-tjwater-frontend/src/features/agent/session-state.test.ts
T
jiang 692bbf609c refactor: consolidate requests and finalize agent tool states
Share project JSON transport, interaction replies and report selectors.
Settle tool progress consistently for direct streams, reconnects and history, preserving failure reasons and stopped state.
2026-11-09 17:55:00 +08:00

161 lines
5.3 KiB
TypeScript

import { describe, expect, it } from "vitest";
import {
applyPermissionResponse,
applyQuestionResponse,
finalizeAssistantMessageAfterAbort,
toTodoUpdate,
upsertPermission,
upsertProgress,
upsertQuestion
} from "./session-state";
import type { AgentChatMessage } from "./types";
describe("agent session state", () => {
it("upserts progress events by id", () => {
const first = upsertProgress(undefined, {
id: "tool-1",
phase: "tool",
status: "running",
title: "调用工具",
elapsed_ms: 20
});
const next = upsertProgress(first, {
id: "tool-1",
phase: "tool",
status: "completed",
title: "工具完成",
duration_ms: 40
});
expect(next).toHaveLength(1);
expect(next[0]).toMatchObject({ id: "tool-1", status: "completed", title: "工具完成", durationMs: 40 });
});
it("retains full tool inputs across stream updates and persisted history", () => {
const input = { address: "完整地址".repeat(70), point: { longitude: 121.5, latitude: 31.2 }, radius: 500 };
const first = upsertProgress(undefined, { id: "tool", phase: "tool", tool: { name: "mcp_tjwater_geocode", call_id: "native-call", input } });
const next = upsertProgress(first, { id: "tool", phase: "tool", status: "completed" });
expect(next[0].tool).toEqual({ name: "mcp_tjwater_geocode", callId: "native-call", input });
expect(upsertProgress(undefined, next[0])[0].tool).toEqual(next[0].tool);
expect(upsertProgress(undefined, { tool: { name: 3, input: [] } })[0].tool).toBeUndefined();
});
it("normalizes todo updates", () => {
expect(
toTodoUpdate({
session_id: "session-1",
todos: [{ id: "todo-1", content: "分析压力", status: "in_progress", priority: "high" }],
created_at: 123
})
).toEqual({
sessionId: "session-1",
messageId: undefined,
todos: [{ id: "todo-1", content: "分析压力", status: "in_progress", priority: "high" }],
createdAt: 123
});
});
it("tracks permission request and response", () => {
const permissions = upsertPermission(undefined, {
session_id: "session-1",
request_id: "permission-1",
permission: "bash",
patterns: ["pnpm test"],
always: [],
created_at: 100
});
const next = applyPermissionResponse(permissions, {
request_id: "permission-1",
reply: "once"
});
expect(next?.[0]).toMatchObject({
requestId: "permission-1",
status: "approved_once"
});
expect(applyPermissionResponse(permissions, { request_id: "permission-1", status: "error", error: "native unavailable" })?.[0])
.toMatchObject({ status: "error", error: "native unavailable" });
});
it("dedupes question requests by tool call id", () => {
const questions = upsertQuestion(undefined, {
session_id: "session-1",
request_id: "call-1",
tool: { messageID: "message-1", callID: "call-1" },
questions: [{ header: "范围", question: "选择范围", options: [] }],
created_at: 100
});
const next = upsertQuestion(questions, {
session_id: "session-1",
request_id: "question-1",
tool: { messageID: "message-1", callID: "call-1" },
questions: [{ header: "范围", question: "选择范围", options: [] }],
created_at: 120
});
const answered = applyQuestionResponse(next, {
request_id: "question-1",
answers: [["东部"]]
});
expect(next).toHaveLength(1);
expect(answered?.[0]).toMatchObject({
requestId: "question-1",
status: "answered",
answers: [["东部"]]
});
});
it("preserves an actionable question id when its tool placeholder arrives later", () => {
const questions = upsertQuestion(undefined, {
session_id: "session-1",
request_id: "question-1",
tool: { messageID: "message-1", callID: "call-1" },
questions: [{ header: "范围", question: "选择范围", options: [] }],
created_at: 100
});
const withPlaceholder = upsertQuestion(questions, {
session_id: "session-1",
request_id: "call-1",
tool: { messageID: "message-1", callID: "call-1" },
questions: [{ header: "范围", question: "选择范围", options: [] }],
created_at: 120
});
const rejected = applyQuestionResponse(withPlaceholder, {
request_id: "question-1",
rejected: true
});
expect(withPlaceholder).toHaveLength(1);
expect(rejected?.[0]).toMatchObject({
requestId: "question-1",
status: "rejected"
});
});
it("finalizes open assistant state after abort", () => {
const message: AgentChatMessage = {
id: "assistant-1",
role: "assistant",
content: "",
progress: [{ id: "run", phase: "run", status: "running", title: "处理中", startedAt: Date.now() }],
permissions: [
{
requestId: "permission-1",
sessionId: "session-1",
permission: "bash",
patterns: [],
always: [],
createdAt: 100,
status: "pending"
}
]
};
const next = finalizeAssistantMessageAfterAbort(message);
expect(next.content).toBe("请求已中止。");
expect(next.progress?.[0].status).toBe("cancelled");
expect(next.permissions?.[0].status).toBe("aborted");
});
});