feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import type { ChatTransport, UIMessageChunk } from "ai";
|
||||
import 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";
|
||||
|
||||
const message = (id: string, text = id): AgentUiMessage => ({ id, role: "assistant", parts: [{ type: "text", text }] });
|
||||
|
||||
describe("independent conversation runtimes", () => {
|
||||
it("keeps the original response streaming after selecting another conversation", async () => {
|
||||
const resources = new WorkbenchSessionResources();
|
||||
let controller!: ReadableStreamDefaultController<UIMessageChunk>;
|
||||
const transport: ChatTransport<AgentUiMessage> = {
|
||||
sendMessages: async () => new ReadableStream({ start(stream) { controller = stream; } }),
|
||||
reconnectToStream: async () => null,
|
||||
};
|
||||
const client = { refreshCredential: vi.fn(async () => {}), submitFrontendActionResult: vi.fn(async () => {}) } as unknown as AgentApiClient;
|
||||
const first = createAgentRuntime(resources, "project:a", client, transport);
|
||||
const second = createAgentRuntime(resources, "project:a", client, transport);
|
||||
resources.activeAgent = first;
|
||||
first.sessionId = "server-a";
|
||||
const response = first.chat.sendMessage({ text: "核对城南压力" });
|
||||
await vi.waitFor(() => expect(controller).toBeDefined());
|
||||
controller.enqueue({ type: "start", messageId: "answer-a" });
|
||||
controller.enqueue({ type: "text-start", id: "text-a" });
|
||||
controller.enqueue({ type: "text-delta", id: "text-a", delta: "第一段" });
|
||||
await vi.waitFor(() => expect(first.chat.messages.at(-1)?.parts).toEqual(expect.arrayContaining([expect.objectContaining({ text: "第一段" })])));
|
||||
resources.activeAgent = second;
|
||||
controller.enqueue({ type: "data-progress", data: { session_id: "another-session", title: "迟到的其他会话事件" } });
|
||||
expect(runtimeRunning(first)).toBe(true);
|
||||
controller.enqueue({ type: "data-credential_refresh_required", data: { request_id: "refresh-a", session_id: "server-a" } });
|
||||
controller.enqueue({ type: "text-delta", id: "text-a", delta: ",后台完成" });
|
||||
controller.enqueue({ type: "text-end", id: "text-a" });
|
||||
controller.enqueue({ type: "finish", finishReason: "stop" });
|
||||
controller.close();
|
||||
await response;
|
||||
expect(first.chat.messages.at(-1)?.parts).toEqual(expect.arrayContaining([expect.objectContaining({ text: "第一段,后台完成" })]));
|
||||
expect(second.chat.messages).toEqual([]);
|
||||
expect(first.sessionId).toBe("server-a");
|
||||
expect(client.refreshCredential).toHaveBeenCalledWith("server-a", "refresh-a");
|
||||
resources.dispose();
|
||||
});
|
||||
it("merges overlapping pages and a newer authoritative tail without duplicates", () => {
|
||||
expect(mergeHistoryMessages([message("a"), message("b", "old")], [message("b", "new"), message("c")]))
|
||||
.toEqual([message("a"), message("b", "new"), message("c")]);
|
||||
expect(mergeHistoryMessages([message("c"), message("d")], [message("a"), message("b"), message("c", "new")]))
|
||||
.toEqual([message("a"), message("b"), message("c", "new"), message("d")]);
|
||||
});
|
||||
it("routes background browser actions to a scoped refusal and keeps old message pages on resume", async () => {
|
||||
const resources = new WorkbenchSessionResources();
|
||||
const submit = vi.fn(async () => {});
|
||||
const client = { submitFrontendActionResult: submit,
|
||||
streamSession: vi.fn(async (_id, options) => {
|
||||
options.onEvent({ type: "state", sessionId: "a", messages: [message("b", "new"), message("c")], isStreaming: true });
|
||||
options.onEvent({ type: "frontend_action", sessionId: "a", data: {
|
||||
version: "frontend-action@1", sessionId: "a", actionId: "action-a", toolCallId: "call-a", name: "locate_features",
|
||||
params: {}, issuedAt: Date.now(), expiresAt: Date.now() + 15_000,
|
||||
} });
|
||||
options.onEvent({ type: "done", sessionId: "a", data: {} });
|
||||
}),
|
||||
} as unknown as AgentApiClient;
|
||||
const transport = {} as ChatTransport<AgentUiMessage>;
|
||||
const runtime = createAgentRuntime(resources, "project:a", client, transport, {
|
||||
id: "a", sessionId: "a", title: "压力复核", createdAt: 1, updatedAt: 1, purpose: "ordinary_chat",
|
||||
isTitleManuallyEdited: false, messages: [message("a"), message("b", "old")],
|
||||
});
|
||||
resumeAgentRuntime(resources, runtime);
|
||||
await vi.waitFor(() => expect(runtime.controller).toBeUndefined());
|
||||
expect(runtime.chat.messages).toEqual([message("a"), message("b", "new"), message("c")]);
|
||||
expect(submit).toHaveBeenCalledWith("a", "action-a", expect.objectContaining({
|
||||
status: "failed", error: expect.objectContaining({ code: "SESSION_NOT_ACTIVE" }),
|
||||
}));
|
||||
resources.dispose();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user