forked from EduCraft/curriculum-project-hub
161 lines
4.9 KiB
TypeScript
161 lines
4.9 KiB
TypeScript
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<string, unknown> } | 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<string, string | undefined> } } | 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",
|
|
}),
|
|
});
|
|
});
|
|
});
|