@@ -2,7 +2,7 @@ import assert from "node:assert/strict";
22import { join } from "node:path" ;
33import { chmod , mkdtemp , mkdir , readFile , writeFile } from "node:fs/promises" ;
44import { tmpdir } from "node:os" ;
5- import { test } from "vitest" ;
5+ import { afterEach , test } from "vitest" ;
66import {
77 ClaudeAdapter ,
88 CodexAdapter ,
@@ -22,7 +22,10 @@ async function createWorkspace(): Promise<{ root: string; artifactDir: string; w
2222 return { root, artifactDir, workspacePath } ;
2323}
2424
25- function createRequest ( executorId : TaskExecutionRequest [ "executor" ] [ "executorId" ] ) : TaskExecutionRequest {
25+ function createRequest (
26+ executorId : TaskExecutionRequest [ "executor" ] [ "executorId" ] ,
27+ options : { model ?: string ; provider ?: string ; readOnly ?: boolean } = { } ,
28+ ) : TaskExecutionRequest {
2629 return {
2730 protocolVersion : PROTOCOL_VERSION ,
2831 taskId : `task-${ executorId } ` ,
@@ -33,14 +36,23 @@ function createRequest(executorId: TaskExecutionRequest["executor"]["executorId"
3336 sourceRepoPath : "/tmp/repo" ,
3437 workBranch : `devagent/${ executorId } /task` ,
3538 isolation : "temp-copy" ,
39+ readOnly : options . readOnly ,
40+ } ,
41+ executor : {
42+ executorId,
43+ model : options . model ?? "test-model" ,
44+ provider : options . provider ,
3645 } ,
37- executor : { executorId, model : "test-model" } ,
3846 constraints : { } ,
3947 context : { summary : "smoke" } ,
4048 expectedArtifacts : [ "triage-report" ] ,
4149 } ;
4250}
4351
52+ afterEach ( ( ) => {
53+ delete process . env . DEVAGENT_RUNNER_CODEX_BIN ;
54+ } ) ;
55+
4456async function createStub ( path : string , contents : string ) : Promise < void > {
4557 await writeFile ( path , contents ) ;
4658 await chmod ( path , 0o755 ) ;
@@ -185,26 +197,31 @@ const fs = require("fs");
185197const args = process.argv.slice(2);
186198const outIndex = args.indexOf("-o");
187199if (outIndex >= 0) fs.writeFileSync(args[outIndex + 1], "stub codex output\\n");
188- process.stdout.write("{\\"type\\":\\"result\\",\\"message\\":\\"ok\\"}\\n");
200+ process.stdout.write("{\\"type\\":\\"thread.started\\"}\\n");
201+ process.stdout.write("{\\"type\\":\\"turn.started\\"}\\n");
202+ process.stdout.write("{\\"type\\":\\"item.completed\\",\\"item\\":{\\"type\\":\\"agent_message\\",\\"text\\":\\"stub codex output\\"}}\\n");
203+ process.stdout.write("{\\"type\\":\\"turn.completed\\"}\\n");
189204` ) ;
190205
206+ process . env . DEVAGENT_RUNNER_CODEX_BIN = `${ process . execPath } ${ stubPath } ` ;
191207 const { events, result } = await collectEvents (
192- new CodexAdapter ( ` ${ process . execPath } ${ stubPath } ` ) ,
208+ new CodexAdapter ( ) ,
193209 createRequest ( "codex" ) ,
194210 workspacePath ,
195211 artifactDir ,
196212 ) ;
197213
198214 assert . equal ( result . status , "success" ) ;
199- assert . equal ( events . at ( - 1 ) ? .type , "completed" ) ;
215+ assert . deepEqual ( events . map ( ( event ) => event . type ) , [ "started" , "progress" , "progress" , "progress" , "progress" ] ) ;
200216 assert . match ( await readFile ( join ( artifactDir , "triage-report.md" ) , "utf8" ) , / s t u b c o d e x o u t p u t / ) ;
201217} ) ;
202218
203219test ( "ClaudeAdapter smoke test with stub executable" , async ( ) => {
204220 const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
205221 const stubPath = join ( root , "claude-stub.js" ) ;
206222 await createStub ( stubPath , `#!/usr/bin/env node
207- process.stdout.write("claude stub output\\n");
223+ process.stdout.write(JSON.stringify({ type: "assistant", message: { content: [{ type: "text", text: "claude stub output" }] } }) + "\\n");
224+ process.stdout.write(JSON.stringify({ type: "result", subtype: "success", result: "claude stub output" }) + "\\n");
208225` ) ;
209226
210227 const { events, result } = await collectEvents (
@@ -215,23 +232,128 @@ process.stdout.write("claude stub output\\n");
215232 ) ;
216233
217234 assert . equal ( result . status , "success" ) ;
218- assert . equal ( events . at ( - 1 ) ?. type , "completed" ) ;
235+ assert . deepEqual ( events . map ( ( event ) => event . type ) , [ "started" , "progress" , "progress" ] ) ;
236+ assert . match ( await readFile ( join ( artifactDir , "triage-report.md" ) , "utf8" ) , / c l a u d e s t u b o u t p u t / ) ;
219237} ) ;
220238
221239test ( "OpenCodeAdapter smoke test with stub executable" , async ( ) => {
222240 const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
223241 const stubPath = join ( root , "opencode-stub.js" ) ;
224242 await createStub ( stubPath , `#!/usr/bin/env node
225- process.stdout.write("opencode stub output\\n");
243+ const args = process.argv.slice(2);
244+ const agentIndex = args.indexOf("--agent");
245+ if (agentIndex === -1 || args[agentIndex + 1] !== "build") {
246+ throw new Error("expected build agent");
247+ }
248+ const permissions = process.env.OPENCODE_PERMISSION || "";
249+ if (!permissions.includes('"*":"deny"') || !permissions.includes('"read":"allow"') || !permissions.includes('"edit":"deny"') || !permissions.includes('"write":"deny"')) {
250+ throw new Error("expected read-only permissions");
251+ }
252+ if (process.argv.includes("--model")) {
253+ throw new Error("unexpected --model flag");
254+ }
255+ process.stdout.write(JSON.stringify({ type: "step_start", part: { type: "step-start" } }) + "\\n");
256+ process.stdout.write(JSON.stringify({ type: "text", part: { type: "text", text: "opencode stub output" } }) + "\\n");
257+ process.stdout.write(JSON.stringify({ type: "step_finish", part: { type: "step-finish" } }) + "\\n");
226258` ) ;
227259
228260 const { events, result } = await collectEvents (
261+ new OpenCodeAdapter ( `${ process . execPath } ${ stubPath } ` ) ,
262+ createRequest ( "opencode" , { readOnly : true } ) ,
263+ workspacePath ,
264+ artifactDir ,
265+ ) ;
266+
267+ assert . equal ( result . status , "success" ) ;
268+ assert . deepEqual ( events . map ( ( event ) => event . type ) , [ "started" , "progress" , "progress" , "progress" ] ) ;
269+ assert . match ( await readFile ( join ( artifactDir , "triage-report.md" ) , "utf8" ) , / o p e n c o d e s t u b o u t p u t / ) ;
270+ } ) ;
271+
272+ test ( "OpenCodeAdapter passes provider-qualified model names through" , async ( ) => {
273+ const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
274+ const stubPath = join ( root , "opencode-model-stub.js" ) ;
275+ await createStub ( stubPath , `#!/usr/bin/env node
276+ const args = process.argv.slice(2);
277+ const modelIndex = args.indexOf("--model");
278+ if (modelIndex === -1 || args[modelIndex + 1] !== "opencode/big-pickle") {
279+ throw new Error("expected provider-qualified --model");
280+ }
281+ process.stdout.write(JSON.stringify({ type: "text", part: { type: "text", text: "opencode model output" } }) + "\\n");
282+ ` ) ;
283+
284+ const { result } = await collectEvents (
285+ new OpenCodeAdapter ( `${ process . execPath } ${ stubPath } ` ) ,
286+ createRequest ( "opencode" , { provider : "opencode" , model : "big-pickle" } ) ,
287+ workspacePath ,
288+ artifactDir ,
289+ ) ;
290+
291+ assert . equal ( result . status , "success" ) ;
292+ assert . match ( await readFile ( join ( artifactDir , "triage-report.md" ) , "utf8" ) , / o p e n c o d e m o d e l o u t p u t / ) ;
293+ } ) ;
294+
295+ test ( "OpenCodeAdapter surfaces structured errors without mislabeling them as permissions" , async ( ) => {
296+ const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
297+ const stubPath = join ( root , "opencode-error-stub.js" ) ;
298+ await createStub ( stubPath , `#!/usr/bin/env node
299+ process.stdout.write(JSON.stringify({
300+ type: "error",
301+ error: { data: { message: "Model not found: opencode/missing-model" } }
302+ }) + "\\n");
303+ process.exit(1);
304+ ` ) ;
305+
306+ const { result } = await collectEvents (
229307 new OpenCodeAdapter ( `${ process . execPath } ${ stubPath } ` ) ,
230308 createRequest ( "opencode" ) ,
231309 workspacePath ,
232310 artifactDir ,
233311 ) ;
234312
313+ assert . equal ( result . status , "failed" ) ;
314+ assert . equal ( result . error ?. message , "Model not found: opencode/missing-model" ) ;
315+ } ) ;
316+
317+ test ( "ClaudeAdapter fails when no final assistant text is produced" , async ( ) => {
318+ const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
319+ const stubPath = join ( root , "claude-empty-stub.js" ) ;
320+ await createStub ( stubPath , `#!/usr/bin/env node
321+ process.stdout.write(JSON.stringify({ type: "result", subtype: "success", result: "" }) + "\\n");
322+ ` ) ;
323+
324+ const { result } = await collectEvents (
325+ new ClaudeAdapter ( `${ process . execPath } ${ stubPath } ` ) ,
326+ createRequest ( "claude" ) ,
327+ workspacePath ,
328+ artifactDir ,
329+ ) ;
330+
331+ assert . equal ( result . status , "failed" ) ;
332+ assert . equal ( result . artifacts . length , 0 ) ;
333+ } ) ;
334+
335+ test ( "ClaudeAdapter captures plan-mode file output when no final assistant text is emitted" , async ( ) => {
336+ const { root, artifactDir, workspacePath } = await createWorkspace ( ) ;
337+ const stubPath = join ( root , "claude-plan-stub.js" ) ;
338+ await createStub ( stubPath , `#!/usr/bin/env node
339+ process.stdout.write(JSON.stringify({
340+ type: "user",
341+ tool_use_result: {
342+ type: "create",
343+ filePath: "/Users/test/.claude/plans/example-plan.md",
344+ content: "# Example Plan\\n\\nExecutor claude handled task type plan."
345+ }
346+ }) + "\\n");
347+ process.stdout.write(JSON.stringify({ type: "result", subtype: "success", result: "" }) + "\\n");
348+ ` ) ;
349+
350+ const { result } = await collectEvents (
351+ new ClaudeAdapter ( `${ process . execPath } ${ stubPath } ` ) ,
352+ createRequest ( "claude" , { readOnly : true } ) ,
353+ workspacePath ,
354+ artifactDir ,
355+ ) ;
356+
235357 assert . equal ( result . status , "success" ) ;
236- assert . equal ( events . at ( - 1 ) ?. type , "completed" ) ;
358+ assert . match ( await readFile ( join ( artifactDir , "triage-report.md" ) , "utf8" ) , / E x a m p l e P l a n / ) ;
237359} ) ;
0 commit comments