fix: unify chat process and reply presentation

This commit is contained in:
2026-10-29 17:54:34 +08:00
parent 7621bf0f86
commit 9474788db6
46 changed files with 3160 additions and 500 deletions
@@ -0,0 +1,83 @@
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> = {}): 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<InteractionRequest>[]) {
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]);
}
});
});