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.
This commit is contained in:
@@ -3,11 +3,42 @@ import type { ChatTransport, UIMessageChunk } from "ai";
|
||||
import { AgentSessionStreamError, AgentStreamDisconnectedError, type AgentApiClient } from "@/features/agent";
|
||||
import { WorkbenchSessionResources } from "./workbench-session-resources";
|
||||
import { createAgentRuntime, mergeHistoryMessages, resumeAgentRuntime, runtimeRunning } from "./agent-session-runtime";
|
||||
import type { AgentUiMessage } from "./agent-session-message-state";
|
||||
import { toAgentChatMessages, type AgentUiMessage } from "./agent-session-message-state";
|
||||
|
||||
const message = (id: string, text = id): AgentUiMessage => ({ id, role: "assistant", parts: [{ type: "text", text }] });
|
||||
|
||||
describe("independent conversation runtimes", () => {
|
||||
it.each(["completed", "error", "aborted"] as const)("settles direct tool progress when the response is %s without a final progress event", async (status) => {
|
||||
const resources = new WorkbenchSessionResources();
|
||||
let controller!: ReadableStreamDefaultController<UIMessageChunk>;
|
||||
const runtime = createAgentRuntime(resources, "project:a", {} as AgentApiClient, {
|
||||
sendMessages: async ({ abortSignal }) => new ReadableStream<UIMessageChunk>({ start(stream) {
|
||||
controller = stream;
|
||||
abortSignal?.addEventListener("abort", () => stream.error(new DOMException("Aborted", "AbortError")), { once: true });
|
||||
} }),
|
||||
reconnectToStream: async () => null,
|
||||
});
|
||||
resources.activeAgent = runtime;
|
||||
try {
|
||||
const request = runtime.chat.sendMessage({ text: "核查工具状态" });
|
||||
await vi.waitFor(() => expect(controller).toBeDefined());
|
||||
controller.enqueue({ type: "start", messageId: "answer" });
|
||||
controller.enqueue({ type: "data-progress", data: { id: "tool", phase: "tool", title: "正在处理", status: "running" } });
|
||||
await vi.waitFor(() => expect(toAgentChatMessages(runtime.chat.messages, {}, {}).at(-1)?.progress?.[0].status).toBe("running"));
|
||||
if (status === "aborted") await runtime.chat.stop();
|
||||
else {
|
||||
if (status === "error") controller.enqueue({ type: "error", errorText: "工具执行失败" });
|
||||
controller.enqueue({ type: "finish", finishReason: status === "error" ? "error" : "stop" });
|
||||
controller.close();
|
||||
}
|
||||
await request;
|
||||
const answer = toAgentChatMessages(runtime.chat.messages, {}, {}).at(-1);
|
||||
expect(answer?.progress?.[0].status).toBe(status === "aborted" ? "cancelled" : status);
|
||||
expect(runtime.runStatus).toBe(status);
|
||||
if (status === "error") expect(answer?.runError).toBe("工具执行失败");
|
||||
} finally { resources.dispose(); }
|
||||
});
|
||||
|
||||
it("retries a disconnected subscription and preserves the draft, pages and reading position", async () => {
|
||||
vi.useFakeTimers();
|
||||
const resources = new WorkbenchSessionResources();
|
||||
@@ -141,7 +172,9 @@ describe("independent conversation runtimes", () => {
|
||||
});
|
||||
resumeAgentRuntime(resources, runtime);
|
||||
await vi.waitFor(() => expect(runtime.controller).toBeUndefined());
|
||||
expect(runtime.chat.messages).toEqual([message("a"), message("b", "new"), message("c")]);
|
||||
expect(toAgentChatMessages(runtime.chat.messages, {}, {})).toMatchObject([
|
||||
{ id: "a", content: "a" }, { id: "b", content: "new" }, { id: "c", content: "c" }
|
||||
]);
|
||||
expect(submit).toHaveBeenCalledWith("a", "action-a", expect.objectContaining({
|
||||
status: "failed", error: expect.objectContaining({ code: "SESSION_NOT_ACTIVE" }),
|
||||
}));
|
||||
|
||||
Reference in New Issue
Block a user