diff --git a/src/adapter.ts b/src/adapter.ts index c41ca5c..164dd06 100644 --- a/src/adapter.ts +++ b/src/adapter.ts @@ -51,7 +51,7 @@ export interface AdapterResponse { body: JsonObject; } -interface CommandResult extends JsonObject { +export interface CommandResult extends JsonObject { args: string[]; returncode: number; stdout: string; @@ -179,7 +179,7 @@ function createFreshChildDirectory(parent: string, name: string, label: string): return path; } -function createFreshTaskAttemptDirectory(root: string, taskId: string, attempt: number, label: string): string { +export function createFreshTaskAttemptDirectory(root: string, taskId: string, attempt: number, label: string): string { if (!validRecordId(taskId)) throw new Error(`Invalid task ID for ${label}`); if (!Number.isSafeInteger(attempt) || attempt <= 0) throw new Error(`Invalid attempt number for ${label}`); const taskRoot = ensureManagedChildDirectory(root, taskId, `${label} task directory`); @@ -1613,6 +1613,7 @@ async function runTaskUnlocked(config: AdapterConfig, taskId: string): Promise { + const diagnostic = runtimeDiagnostic({ + args: ["coven-code", "--headless"], + returncode: 2, + stdout: "", + stderr: "API error 429: rate limit for model `gpt-5.4-mini` on account `reviewer (reviewer@example.com)`. Bearer topsecret sk-proj-1234567890\nProvider: codex", + signal: null, + timed_out: false, + spawn_error: "", + }); + assert.match(String(diagnostic), /API error 429/); + assert.match(String(diagnostic), /gpt-5\.4-mini/); + assert.match(String(diagnostic), /configured account/); + assert.doesNotMatch(String(diagnostic), /reviewer@example\.com|reviewer \(|topsecret|1234567890/); + + const stateDir = tempStateDir(); + const config = testConfig(stateDir); + const task = reviewTask("provider-failure-diagnostic"); + task.runtime_diagnostic = diagnostic; + prepareReviewWorkspace(config, task); + const result = completeReview(); + result.status = "failure"; + const resultPath = join(stateDir, "provider-failure.json"); + writeFileSync(resultPath, JSON.stringify(result)); + let payload: JsonObject = {}; + await withGithubApiMock((url, init) => { + const read = githubReadFixture(url, init); + if (read) return read; + payload = JSON.parse(String(init.body)) as JsonObject; + return {id: 409, state: "PENDING", html_url: "https://github.com/OpenCoven/example/pull/7#pullrequestreview-409"}; + }, async () => publishResultIfConfigured(config, task, resultPath, "token")); + assert.equal(payload.event, "COMMENT"); + assert.match(String(payload.body), /### Runtime diagnostic/); + assert.match(String(payload.body), /API error 429/); + assert.doesNotMatch(String(payload.body), /reviewer@example\.com|reviewer \(|topsecret|1234567890/); +}); + +test("creates isolated attempt directories and refuses stale artifact reuse", () => { + const root = tempStateDir(); + const first = createFreshTaskAttemptDirectory(root, "review-task", 1, "test attempt"); + writeFileSync(join(first, "run.json"), "stale artifact\n"); + const second = createFreshTaskAttemptDirectory(root, "review-task", 2, "test attempt"); + assert.notEqual(first, second); + assert.equal(readFileSync(join(first, "run.json"), "utf8"), "stale artifact\n"); + assert.deepEqual(readdirSync(second), []); + assert.throws( + () => createFreshTaskAttemptDirectory(root, "review-task", 1, "test attempt"), + /already exists/, + ); +}); + test("keeps idempotency marker after truncating and redacts issue publication text", async () => { const stateDir = tempStateDir(); const task: JsonObject = {