import { describe, expect, it } from "vitest"; import type { InteractionRequest } from "../api/interaction-requests"; import type { AgentChatMessage } from "../types"; import { groupMessageProcesses } from "./message-process-groups"; const assistant = (id: string, content = ""): AgentChatMessage => ({ id, role: "assistant", content, progress: [{ id: "query", phase: "tool", status: "completed", title: id }] }); const user = (id: string): AgentChatMessage => ({ id, role: "user", content: id }); const request = (source: string, reply: string, extra: Partial = {}): InteractionRequest => ({ version: "agent-interaction-request@1", interaction_id: `question-${source}`, message_id: source, turn_id: `turn-${source}`, call_id: `call-${source}`, status: "answered", revision: 1, draft_revision: 0, questions: [{ question_id: "scope", title: "范围", body: "核查范围?", mode: "single", required: true, allow_custom: true, options: [] }], draft: [], created_at: "2026-10-28T10:00:00Z", delivery: { native_message_id: reply, revision: 1, status: "delivered" }, ...extra }); describe("process groups across clarification replies", () => { const source = assistant("question"); const reply = user("reply"); const result = assistant("answer_reply", "结果正文"); const messages = [user("original"), source, reply, result]; it("moves the source process to the related continuation without editing stored messages or progress ids", () => { const original = structuredClone(messages); const groups = groupMessageProcesses(messages, [request(source.id, reply.id)]); expect(groups.get(source.id)).toEqual([]); expect(groups.get(result.id)).toEqual([source, result]); expect(groups.get(result.id)?.map((message) => message.progress?.[0].id)).toEqual(["query", "query"]); expect(messages).toEqual(original); }); it("groups an existing result even when delivery is blocked and there is no final_answer", () => { const record = request(source.id, reply.id, { delivery: { native_message_id: reply.id, revision: 2, status: "blocked", error_code: "CONTINUATION_UNCONFIRMED" } }); expect(groupMessageProcesses(messages, [record]).get(result.id)).toEqual([source, result]); expect(record.delivery?.status).toBe("blocked"); }); it("keeps processes in place while waiting, cancelled, missing a source page or missing a continuation", () => { for (const extra of [{ status: "pending" }, { status: "cancelled" }, { delivery: undefined }, { delivery: { native_message_id: reply.id, revision: 2, status: "cancelled" } }] satisfies Partial[]) { expect(groupMessageProcesses(messages, [request(source.id, reply.id, extra)]).get(source.id)).toEqual([source]); } expect(groupMessageProcesses(messages.slice(0, -1), [request(source.id, reply.id)]).get(source.id)).toEqual([source]); expect(groupMessageProcesses(messages.slice(2), [request(source.id, reply.id)]).get(result.id)).toEqual([result]); }); it("does not merge across an unrelated user request on either side of the saved reply", () => { for (const list of [ [source, user("other-request"), reply, result], [source, reply, user("other-request"), result], [reply, source, result] ]) { const groups = groupMessageProcesses(list, [request(source.id, reply.id)]); expect(groups.get(source.id)).toEqual([source]); expect(groups.get(result.id)).toEqual([result]); } }); it("combines successive clarifications in order and ignores duplicate records", () => { const reply2 = user("reply2"); const final = assistant("answer_reply2", "最终结果"); const first = request(source.id, reply.id); const second = request(result.id, reply2.id); const groups = groupMessageProcesses([...messages, reply2, final], [second, first, first]); expect(groups.get(source.id)).toEqual([]); expect(groups.get(result.id)).toEqual([]); expect(groups.get(final.id)).toEqual([source, result, final]); }); it("retains actionable questions and permissions at their original message", () => { const pendingQuestion: AgentChatMessage = { ...source, questions: [{ requestId: "pending", sessionId: "s", questions: [], createdAt: 0, status: "checking" }] }; const pendingPermission: AgentChatMessage = { ...source, permissions: [{ requestId: "pending", sessionId: "s", permission: "simulate", patterns: [], always: [], createdAt: 0, status: "pending" }] }; for (const pending of [pendingQuestion, pendingPermission]) { expect(groupMessageProcesses([pending, reply, result], [request(source.id, reply.id)]).get(source.id)).toEqual([pending]); } }); });