import { describe, expect, it, vi, beforeEach } from "vitest"; import { runAgent } from "../../src/agent/runner.js"; const queryMock = vi.hoisted(() => vi.fn()); vi.mock("@anthropic-ai/claude-agent-sdk", () => ({ query: queryMock, })); const stubPrisma = { projectAgentLock: { update: async () => ({}) }, } as unknown as import("@prisma/client").PrismaClient; function assistantMessage(text: string) { return { type: "assistant", message: { content: [{ type: "text", text }], usage: { input_tokens: 3, output_tokens: 5 }, }, }; } function resultMessage(sessionId: string) { return { type: "result", subtype: "success", session_id: sessionId, }; } function messages(...items: unknown[]) { return (async function* () { for (const item of items) yield item; })(); } describe("runAgent", () => { beforeEach(() => { queryMock.mockReset(); }); it("passes the previous Claude SDK session id through resume", async () => { queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-new"))); const result = await runAgent({ prompt: "再发一次", model: "anthropic/claude-sonnet-5", project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" }, systemPrompt: undefined, resumeSessionId: "sdk-session-old", runId: "run-1", sessionId: "hub-session-1", prisma: stubPrisma, }); expect(queryMock).toHaveBeenCalledWith({ prompt: "再发一次", options: expect.objectContaining({ cwd: "/tmp/ws", model: "anthropic/claude-sonnet-5", resume: "sdk-session-old", // ADR-0018: agent execution surface bounded by workspace. permissionMode: "bypassPermissions", allowDangerouslySkipPermissions: true, sandbox: expect.objectContaining({ enabled: true, failIfUnavailable: true, filesystem: expect.objectContaining({ allowWrite: ["/tmp/ws"], }), }), }), }); }); it("does not send resume for a fresh Hub session", async () => { queryMock.mockReturnValue(messages(assistantMessage("fresh"), resultMessage("sdk-session-1"))); await runAgent({ prompt: "你好", model: undefined, project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" }, systemPrompt: undefined, runId: "run-1", sessionId: "hub-session-1", prisma: stubPrisma, }); const call = queryMock.mock.calls[0]?.[0] as { options?: Record } | undefined; expect(call?.options).not.toHaveProperty("resume"); }); it("passes provider env per run without mutating process env", async () => { delete process.env["CPH_TEST_PROVIDER_ENV_MUTATION"]; queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1"))); await runAgent({ prompt: "你好", model: undefined, project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" }, systemPrompt: undefined, providerEnv: { ANTHROPIC_BASE_URL: "https://openrouter.ai/api", ANTHROPIC_AUTH_TOKEN: "test-token", ANTHROPIC_API_KEY: "", CPH_TEST_PROVIDER_ENV_MUTATION: "per-run", }, runId: "run-1", sessionId: "hub-session-1", prisma: stubPrisma, }); const call = queryMock.mock.calls[0]?.[0] as { options?: { env?: Record } } | undefined; expect(call?.options?.env).toMatchObject({ ANTHROPIC_BASE_URL: "https://openrouter.ai/api", ANTHROPIC_AUTH_TOKEN: "test-token", ANTHROPIC_API_KEY: "", CPH_TEST_PROVIDER_ENV_MUTATION: "per-run", }); expect(process.env["CPH_TEST_PROVIDER_ENV_MUTATION"]).toBeUndefined(); }); it("persists structured user and assistant messages best-effort", async () => { queryMock.mockReturnValue(messages(assistantMessage("ok"), resultMessage("sdk-session-1"))); const createMessage = vi.fn().mockResolvedValue({}); const prisma = { projectAgentLock: { update: async () => ({}) }, agentMessage: { create: createMessage }, } as unknown as import("@prisma/client").PrismaClient; await runAgent({ prompt: "你好", model: undefined, project: { projectId: "p", boundChatId: "c", workspaceDir: "/tmp/ws" }, systemPrompt: undefined, runId: "run-1", sessionId: "hub-session-1", prisma, }); expect(createMessage).toHaveBeenCalledTimes(2); expect(createMessage).toHaveBeenNthCalledWith(1, { data: expect.objectContaining({ sessionId: "hub-session-1", runId: "run-1", role: "user", content: "你好", }), }); expect(createMessage).toHaveBeenNthCalledWith(2, { data: expect.objectContaining({ sessionId: "hub-session-1", runId: "run-1", role: "assistant", content: "ok", }), }); }); });