|
|
@@ -260,3 +260,190 @@ test("shows pipeline stages and resolves remote confirmations", async () => {
|
|
|
await rm(home, { recursive: true, force: true })
|
|
|
}
|
|
|
})
|
|
|
+
|
|
|
+test("preserves completed artifacts and forbids automatic reruns after failure", async () => {
|
|
|
+ const home = await mkdtemp(path.join(tmpdir(), "agentpaas-bridge-failure-"))
|
|
|
+ const originalHome = process.env.HOME
|
|
|
+ const originalFetch = globalThis.fetch
|
|
|
+ const updates: Array<{ title?: string; metadata?: Record<string, unknown> }> = []
|
|
|
+ const workspacePath = "/workspace/runs/run_failure"
|
|
|
+ const events: Array<[string, unknown]> = [
|
|
|
+ ["started", { run_id: "run_failure", thread_id: "thread_failure" }],
|
|
|
+ [
|
|
|
+ "stage_started",
|
|
|
+ {
|
|
|
+ pipeline_id: "research67-one-round",
|
|
|
+ stage_id: "paper-writing",
|
|
|
+ stage_name: "实验前研究草稿撰写",
|
|
|
+ attempt: 1,
|
|
|
+ status: "running",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ ["tool_call", { tool: "WriteFile", content: '{"file_path":"/run/paper-writing/artifacts/outline.json"}' }],
|
|
|
+ ["tool_result", { tool: "WriteFile", content: "[OK] Created outline.json" }],
|
|
|
+ ["tool_call", { tool: "CompileLatex", content: '{"tex_path":"/run/paper-writing/artifacts/paper.tex"}' }],
|
|
|
+ ["tool_result", { tool: "CompileLatex", content: "[TOOL_ERROR] UnicodeDecodeError" }],
|
|
|
+ [
|
|
|
+ "stage_failed",
|
|
|
+ {
|
|
|
+ pipeline_id: "research67-one-round",
|
|
|
+ stage_id: "paper-writing",
|
|
|
+ stage_name: "实验前研究草稿撰写",
|
|
|
+ attempt: 1,
|
|
|
+ status: "failed",
|
|
|
+ error: "Native function-calling loop produced no final output",
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ [
|
|
|
+ "error",
|
|
|
+ {
|
|
|
+ code: "AGENT_EXECUTION_FAILED",
|
|
|
+ message: "Native function-calling loop produced no final output",
|
|
|
+ workspace_path: workspacePath,
|
|
|
+ },
|
|
|
+ ],
|
|
|
+ ["done", { run_id: "run_failure", thread_id: "thread_failure", status: "failed" }],
|
|
|
+ ]
|
|
|
+
|
|
|
+ try {
|
|
|
+ process.env.HOME = home
|
|
|
+ globalThis.fetch = Object.assign(
|
|
|
+ async (input: URL | RequestInfo) => {
|
|
|
+ const pathname = new URL(input instanceof Request ? input.url : input.toString()).pathname
|
|
|
+ if (pathname === "/api/v1/agents") {
|
|
|
+ return Response.json({
|
|
|
+ agents: [{ id: "ag_pipeline", name: "Research67 三阶段研究草稿流水线" }],
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (pathname === "/api/v1/agents/ag_pipeline/run/stream") {
|
|
|
+ const body =
|
|
|
+ events.map(([event, data]) => `event: ${event}\ndata: ${JSON.stringify(data)}`).join("\n\n") + "\n\n"
|
|
|
+ return new Response(body, { headers: { "Content-Type": "text/event-stream" } })
|
|
|
+ }
|
|
|
+ return new Response("not found", { status: 404 })
|
|
|
+ },
|
|
|
+ { preconnect: originalFetch.preconnect },
|
|
|
+ )
|
|
|
+ const configDir = path.join(home, ".agentpaas")
|
|
|
+ await mkdir(configDir)
|
|
|
+ await writeFile(
|
|
|
+ path.join(configDir, "config.json"),
|
|
|
+ JSON.stringify({ server: "http://agentpaas.test", api_key: "test-key" }),
|
|
|
+ )
|
|
|
+ const bridge = (await import(`../../../../.opencode/tool/agentpaas_run.ts?failure=${Date.now()}`)).default
|
|
|
+ const result = await bridge.execute(
|
|
|
+ { input: "调查课题", agent_name: "Research67 三阶段研究草稿流水线", mode: "iterate" },
|
|
|
+ {
|
|
|
+ sessionID: "session_failure",
|
|
|
+ messageID: "message_failure",
|
|
|
+ agent: "research-67-pipeline",
|
|
|
+ directory: "/workspace",
|
|
|
+ worktree: "/workspace",
|
|
|
+ abort: new AbortController().signal,
|
|
|
+ metadata: (update: (typeof updates)[number]) => updates.push(update),
|
|
|
+ ask: async () => undefined,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ expect(result.output).toContain("Native function-calling loop produced no final output")
|
|
|
+ expect(result.output).toContain(workspacePath)
|
|
|
+ expect(result.output).toContain("失败严禁自动重跑、拆分任务或调用其他 subagent")
|
|
|
+ expect(result.metadata).toMatchObject({
|
|
|
+ run_id: "run_failure",
|
|
|
+ status: "failed",
|
|
|
+ error_code: "AGENT_EXECUTION_FAILED",
|
|
|
+ workspace_path: workspacePath,
|
|
|
+ research67_progress: {
|
|
|
+ rows: expect.arrayContaining([
|
|
|
+ expect.objectContaining({ id: "paper-writing:outline", status: "completed" }),
|
|
|
+ expect.objectContaining({ id: "paper-writing:compile", status: "failed" }),
|
|
|
+ expect.objectContaining({ id: "paper-writing:report", status: "pending" }),
|
|
|
+ ]),
|
|
|
+ },
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ globalThis.fetch = originalFetch
|
|
|
+ process.env.HOME = originalHome
|
|
|
+ await rm(home, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+test("cancels the remote run when the local request is aborted", async () => {
|
|
|
+ const home = await mkdtemp(path.join(tmpdir(), "agentpaas-bridge-cancel-"))
|
|
|
+ const originalHome = process.env.HOME
|
|
|
+ const originalFetch = globalThis.fetch
|
|
|
+ const abort = new AbortController()
|
|
|
+ const cancelledRuns: string[] = []
|
|
|
+ let markStarted: (() => void) | undefined
|
|
|
+ const started = new Promise<void>((resolve) => {
|
|
|
+ markStarted = resolve
|
|
|
+ })
|
|
|
+
|
|
|
+ try {
|
|
|
+ process.env.HOME = home
|
|
|
+ globalThis.fetch = Object.assign(
|
|
|
+ async (input: URL | RequestInfo, init?: RequestInit) => {
|
|
|
+ const pathname = new URL(input instanceof Request ? input.url : input.toString()).pathname
|
|
|
+ if (pathname === "/api/v1/agents") {
|
|
|
+ return Response.json({
|
|
|
+ agents: [{ id: "ag_pipeline", name: "Research67 三阶段研究草稿流水线" }],
|
|
|
+ })
|
|
|
+ }
|
|
|
+ if (pathname === "/api/v1/agents/ag_pipeline/run/stream") {
|
|
|
+ const body = new ReadableStream<Uint8Array>({
|
|
|
+ start(controller) {
|
|
|
+ controller.enqueue(
|
|
|
+ new TextEncoder().encode('event: started\ndata: {"run_id":"run_abort","thread_id":"thread_abort"}\n\n'),
|
|
|
+ )
|
|
|
+ init?.signal?.addEventListener(
|
|
|
+ "abort",
|
|
|
+ () => controller.error(new DOMException("Aborted", "AbortError")),
|
|
|
+ {
|
|
|
+ once: true,
|
|
|
+ },
|
|
|
+ )
|
|
|
+ },
|
|
|
+ })
|
|
|
+ return new Response(body, { headers: { "Content-Type": "text/event-stream" } })
|
|
|
+ }
|
|
|
+ if (pathname === "/api/v1/agents/ag_pipeline/runs/run_abort/cancel") {
|
|
|
+ cancelledRuns.push("run_abort")
|
|
|
+ return Response.json({ run_id: "run_abort", status: "cancelled" })
|
|
|
+ }
|
|
|
+ return new Response("not found", { status: 404 })
|
|
|
+ },
|
|
|
+ { preconnect: originalFetch.preconnect },
|
|
|
+ )
|
|
|
+ const configDir = path.join(home, ".agentpaas")
|
|
|
+ await mkdir(configDir)
|
|
|
+ await writeFile(
|
|
|
+ path.join(configDir, "config.json"),
|
|
|
+ JSON.stringify({ server: "http://agentpaas.test", api_key: "test-key" }),
|
|
|
+ )
|
|
|
+ const bridge = (await import(`../../../../.opencode/tool/agentpaas_run.ts?cancel=${Date.now()}`)).default
|
|
|
+ const running = bridge.execute(
|
|
|
+ { input: "调查课题", agent_name: "Research67 三阶段研究草稿流水线", mode: "iterate" },
|
|
|
+ {
|
|
|
+ sessionID: "session_abort",
|
|
|
+ messageID: "message_abort",
|
|
|
+ agent: "research-67-pipeline",
|
|
|
+ directory: "/workspace",
|
|
|
+ worktree: "/workspace",
|
|
|
+ abort: abort.signal,
|
|
|
+ metadata: (update: { metadata?: Record<string, unknown> }) => {
|
|
|
+ if (update.metadata?.run_id === "run_abort") markStarted?.()
|
|
|
+ },
|
|
|
+ ask: async () => undefined,
|
|
|
+ },
|
|
|
+ )
|
|
|
+
|
|
|
+ await started
|
|
|
+ abort.abort()
|
|
|
+ await expect(running).rejects.toThrow("Aborted")
|
|
|
+ expect(cancelledRuns).toEqual(["run_abort"])
|
|
|
+ } finally {
|
|
|
+ globalThis.fetch = originalFetch
|
|
|
+ process.env.HOME = originalHome
|
|
|
+ await rm(home, { recursive: true, force: true })
|
|
|
+ }
|
|
|
+})
|