From 595005b1cf7db0ce6eb5f92e26db62cc944b4a6e Mon Sep 17 00:00:00 2001 From: StanPonomarev Date: Sat, 18 Jul 2026 17:35:05 +0200 Subject: [PATCH] fix: improve executor failure handling --- app/src/adapters/opencode/transport.ts | 23 +++++++++++++----- app/src/application/publication/service.ts | 11 ++++++++- app/src/tests/adapters/opencode.test.ts | 24 ++++++++++++++++++- app/src/tests/application/publication.test.ts | 13 ++++++++++ deploy/Dockerfile | 2 +- 5 files changed, 64 insertions(+), 9 deletions(-) create mode 100644 app/src/tests/application/publication.test.ts diff --git a/app/src/adapters/opencode/transport.ts b/app/src/adapters/opencode/transport.ts index 2aa71db..0ca0d35 100644 --- a/app/src/adapters/opencode/transport.ts +++ b/app/src/adapters/opencode/transport.ts @@ -1,5 +1,5 @@ import type { createOpencodeClient } from "@opencode-ai/sdk/v2"; -import { Agent, type Dispatcher } from "undici"; +import { Agent, fetch as undiciFetch } from "undici"; import { log } from "../../core/contracts.js"; type OpenCodeClient = ReturnType; @@ -27,12 +27,23 @@ export async function withPromptTransport( bodyTimeout: 0, }); try { - return await operation((input, init) => - fetch(input, { - ...init, + const customFetch: typeof fetch = async (input, init) => { + const request = new Request(input, init); + const body = + request.method === "GET" || request.method === "HEAD" + ? undefined + : new Uint8Array(await request.arrayBuffer()); + const response = await undiciFetch(request.url, { + method: request.method, + headers: Object.fromEntries(request.headers), + ...(body ? { body } : {}), + signal: request.signal, + redirect: request.redirect, dispatcher, - } as RequestInit & { dispatcher: Dispatcher }), - ); + }); + return response as unknown as Response; + }; + return await operation(customFetch); } finally { await dispatcher.close(); } diff --git a/app/src/application/publication/service.ts b/app/src/application/publication/service.ts index b120f07..2bf4601 100644 --- a/app/src/application/publication/service.ts +++ b/app/src/application/publication/service.ts @@ -31,7 +31,7 @@ export async function publishJob( context.botLogin, job, `Agent ${job.mode} failed`, - `Request \`${job.id.slice(0, 12)}\` failed. Inspect the redacted executor and controller logs.`, + failureDetail(job.id, result.message), ); return { terminal: "failed" }; } @@ -39,3 +39,12 @@ export async function publishJob( ? publishPlan(context, job) : publishImplementation(context, job); } + +export function failureDetail(jobId: string, message: string): string { + const failure = message + .replaceAll("\r\n", "\n") + .split("\n") + .map((line) => ` ${line}`) + .join("\n"); + return `Request \`${jobId.slice(0, 12)}\` failed.\n\nFailure:\n\n${failure}`; +} diff --git a/app/src/tests/adapters/opencode.test.ts b/app/src/tests/adapters/opencode.test.ts index 29918aa..1b11918 100644 --- a/app/src/tests/adapters/opencode.test.ts +++ b/app/src/tests/adapters/opencode.test.ts @@ -1,6 +1,28 @@ import assert from "node:assert/strict"; +import { createServer } from "node:http"; import test from "node:test"; -import { withWorkBudget } from "../../adapters/opencode/transport.js"; +import { + withPromptTransport, + withWorkBudget, +} from "../../adapters/opencode/transport.js"; + +test("uses a fetch implementation compatible with the prompt dispatcher", async () => { + const server = createServer((_request, response) => response.end("ok")); + await new Promise((resolve) => + server.listen(0, "127.0.0.1", resolve), + ); + try { + const address = server.address(); + assert.ok(address && typeof address === "object"); + const response = await withPromptTransport((customFetch) => + customFetch(new Request(`http://127.0.0.1:${address.port}/prompt`)), + ); + assert.equal(response.status, 200); + assert.equal(await response.text(), "ok"); + } finally { + await new Promise((resolve) => server.close(() => resolve())); + } +}); test("returns completed OpenCode work without finalization", async () => { let stopped = false; diff --git a/app/src/tests/application/publication.test.ts b/app/src/tests/application/publication.test.ts new file mode 100644 index 0000000..8f63fa9 --- /dev/null +++ b/app/src/tests/application/publication.test.ts @@ -0,0 +1,13 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { failureDetail } from "../../application/publication/service.js"; + +test("renders the full executor failure as literal issue comment text", () => { + assert.equal( + failureDetail( + "15f9edbf-ff6f-4b29-8231-5fc394071976", + "OpenCode prompt failed\n```embedded markdown```\nfinal detail", + ), + "Request `15f9edbf-ff6` failed.\n\nFailure:\n\n OpenCode prompt failed\n ```embedded markdown```\n final detail", + ); +}); diff --git a/deploy/Dockerfile b/deploy/Dockerfile index 6aa9df9..792c662 100644 --- a/deploy/Dockerfile +++ b/deploy/Dockerfile @@ -42,7 +42,7 @@ CMD ["node", "/opt/ci-agents/dist/application/controller/main.js"] FROM runtime AS executor ARG TARGETARCH=amd64 -ARG OPENCODE_VERSION=1.18.2 +ARG OPENCODE_VERSION=1 ARG GITEA_MCP_VERSION=1.3.0 USER root RUN npm install --global --omit=dev "opencode-ai@${OPENCODE_VERSION}" \