11import path from "node:path" ;
22import os from "node:os" ;
33import { mkdtempSync , rmSync } from "node:fs" ;
4- import { mkdir , writeFile } from "node:fs/promises" ;
4+ import { chmod , mkdir , writeFile } from "node:fs/promises" ;
55
66import { afterEach , describe , expect , it } from "vitest" ;
77
@@ -22,15 +22,18 @@ afterEach(() => {
2222describe ( "runDoctorReport" , ( ) => {
2323 it ( "includes harness diagnostics for current workspace runs" , async ( ) => {
2424 const workspace = createTempWorkspace ( "autolabos-doctor-harness-" ) ;
25+ await seedDoctorTooling ( workspace ) ;
2526 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
2627 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
2728
28- const report = await runDoctorReport ( createCodexStub ( ) , {
29- workspaceRoot : workspace ,
30- includeHarnessValidation : true ,
31- includeHarnessTestRecords : false ,
32- maxHarnessFindings : 10
33- } ) ;
29+ const report = await withWorkspacePath ( workspace , ( ) =>
30+ runDoctorReport ( createCodexStub ( ) , {
31+ workspaceRoot : workspace ,
32+ includeHarnessValidation : true ,
33+ includeHarnessTestRecords : false ,
34+ maxHarnessFindings : 10
35+ } )
36+ ) ;
3437
3538 expect ( report . checks . length ) . toBeGreaterThan ( 0 ) ;
3639 expect ( report . harness ) . toBeDefined ( ) ;
@@ -44,6 +47,7 @@ describe("runDoctorReport", () => {
4447
4548 it ( "includes the latest compiled paper page-budget check when available" , async ( ) => {
4649 const workspace = createTempWorkspace ( "autolabos-doctor-page-budget-" ) ;
50+ await seedDoctorTooling ( workspace ) ;
4751 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
4852 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , {
4953 runs : [ { id : "run-1" , updatedAt : "2026-03-19T12:00:00.000Z" } ]
@@ -57,10 +61,12 @@ describe("runDoctorReport", () => {
5761 message : "Compiled PDF is only 3 pages, below the configured minimum_main_pages of 8."
5862 } ) ;
5963
60- const report = await runDoctorReport ( createCodexStub ( ) , {
61- workspaceRoot : workspace ,
62- includeHarnessValidation : false
63- } ) ;
64+ const report = await withWorkspacePath ( workspace , ( ) =>
65+ runDoctorReport ( createCodexStub ( ) , {
66+ workspaceRoot : workspace ,
67+ includeHarnessValidation : false
68+ } )
69+ ) ;
6470
6571 expect ( report . checks ) . toContainEqual (
6672 expect . objectContaining ( {
@@ -77,19 +83,22 @@ describe("runDoctorReport", () => {
7783
7884 it ( "captures readiness snapshot fields for approval mode and workspace write probing" , async ( ) => {
7985 const workspace = createTempWorkspace ( "autolabos-doctor-readiness-" ) ;
86+ await seedDoctorTooling ( workspace ) ;
8087 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
8188 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
8289
83- const report = await runDoctorReport ( createCodexStub ( ) , {
84- workspaceRoot : workspace ,
85- includeHarnessValidation : false ,
86- approvalMode : "manual" ,
87- executionApprovalMode : "risk_ack" ,
88- dependencyMode : "docker" ,
89- sessionMode : "existing" ,
90- codeExecutionExpected : true ,
91- candidateIsolation : "attempt_worktree"
92- } ) ;
90+ const report = await withWorkspacePath ( workspace , ( ) =>
91+ runDoctorReport ( createCodexStub ( ) , {
92+ workspaceRoot : workspace ,
93+ includeHarnessValidation : false ,
94+ approvalMode : "manual" ,
95+ executionApprovalMode : "risk_ack" ,
96+ dependencyMode : "docker" ,
97+ sessionMode : "existing" ,
98+ codeExecutionExpected : true ,
99+ candidateIsolation : "attempt_worktree"
100+ } )
101+ ) ;
93102
94103 expect ( report . readiness . approvalMode ) . toBe ( "manual" ) ;
95104 expect ( report . readiness . executionApprovalMode ) . toBe ( "risk_ack" ) ;
@@ -107,20 +116,23 @@ describe("runDoctorReport", () => {
107116
108117 it ( "treats local snapshot isolation plus disabled network as ready for code execution" , async ( ) => {
109118 const workspace = createTempWorkspace ( "autolabos-doctor-local-isolation-" ) ;
119+ await seedDoctorTooling ( workspace ) ;
110120 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
111121 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
112122
113- const report = await runDoctorReport ( createCodexStub ( ) , {
114- workspaceRoot : workspace ,
115- includeHarnessValidation : false ,
116- approvalMode : "manual" ,
117- executionApprovalMode : "manual" ,
118- dependencyMode : "local" ,
119- sessionMode : "existing" ,
120- codeExecutionExpected : true ,
121- candidateIsolation : "attempt_snapshot_restore" ,
122- allowNetwork : false
123- } ) ;
123+ const report = await withWorkspacePath ( workspace , ( ) =>
124+ runDoctorReport ( createCodexStub ( ) , {
125+ workspaceRoot : workspace ,
126+ includeHarnessValidation : false ,
127+ approvalMode : "manual" ,
128+ executionApprovalMode : "manual" ,
129+ dependencyMode : "local" ,
130+ sessionMode : "existing" ,
131+ codeExecutionExpected : true ,
132+ candidateIsolation : "attempt_snapshot_restore" ,
133+ allowNetwork : false
134+ } )
135+ ) ;
124136
125137 expect ( report . readiness . blocked ) . toBe ( false ) ;
126138 expect ( report . checks ) . toContainEqual (
@@ -142,22 +154,25 @@ describe("runDoctorReport", () => {
142154
143155 it ( "downgrades declared networked execution to a warning instead of a hard failure" , async ( ) => {
144156 const workspace = createTempWorkspace ( "autolabos-doctor-network-declared-" ) ;
157+ await seedDoctorTooling ( workspace ) ;
145158 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
146159 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
147160
148- const report = await runDoctorReport ( createCodexStub ( ) , {
149- workspaceRoot : workspace ,
150- includeHarnessValidation : false ,
151- approvalMode : "manual" ,
152- executionApprovalMode : "risk_ack" ,
153- dependencyMode : "local" ,
154- sessionMode : "fresh" ,
155- codeExecutionExpected : true ,
156- candidateIsolation : "attempt_snapshot_restore" ,
157- allowNetwork : true ,
158- networkPolicy : "declared" ,
159- networkPurpose : "logging"
160- } ) ;
161+ const report = await withWorkspacePath ( workspace , ( ) =>
162+ runDoctorReport ( createCodexStub ( ) , {
163+ workspaceRoot : workspace ,
164+ includeHarnessValidation : false ,
165+ approvalMode : "manual" ,
166+ executionApprovalMode : "risk_ack" ,
167+ dependencyMode : "local" ,
168+ sessionMode : "fresh" ,
169+ codeExecutionExpected : true ,
170+ candidateIsolation : "attempt_snapshot_restore" ,
171+ allowNetwork : true ,
172+ networkPolicy : "declared" ,
173+ networkPurpose : "logging"
174+ } )
175+ ) ;
161176
162177 expect ( report . readiness . blocked ) . toBe ( false ) ;
163178 expect ( report . readiness . networkPolicy ) . toBe ( "declared" ) ;
@@ -179,22 +194,25 @@ describe("runDoctorReport", () => {
179194
180195 it ( "surfaces required networked execution as a stronger warning with explicit highlight guidance" , async ( ) => {
181196 const workspace = createTempWorkspace ( "autolabos-doctor-network-required-" ) ;
197+ await seedDoctorTooling ( workspace ) ;
182198 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
183199 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
184200
185- const report = await runDoctorReport ( createCodexStub ( ) , {
186- workspaceRoot : workspace ,
187- includeHarnessValidation : false ,
188- approvalMode : "manual" ,
189- executionApprovalMode : "risk_ack" ,
190- dependencyMode : "remote_gpu" ,
191- sessionMode : "fresh" ,
192- codeExecutionExpected : true ,
193- candidateIsolation : "attempt_snapshot_restore" ,
194- allowNetwork : true ,
195- networkPolicy : "required" ,
196- networkPurpose : "remote_inference"
197- } ) ;
201+ const report = await withWorkspacePath ( workspace , ( ) =>
202+ runDoctorReport ( createCodexStub ( ) , {
203+ workspaceRoot : workspace ,
204+ includeHarnessValidation : false ,
205+ approvalMode : "manual" ,
206+ executionApprovalMode : "risk_ack" ,
207+ dependencyMode : "remote_gpu" ,
208+ sessionMode : "fresh" ,
209+ codeExecutionExpected : true ,
210+ candidateIsolation : "attempt_snapshot_restore" ,
211+ allowNetwork : true ,
212+ networkPolicy : "required" ,
213+ networkPurpose : "remote_inference"
214+ } )
215+ ) ;
198216
199217 expect ( report . readiness . blocked ) . toBe ( false ) ;
200218 expect ( report . readiness . networkPolicy ) . toBe ( "required" ) ;
@@ -215,20 +233,23 @@ describe("runDoctorReport", () => {
215233
216234 it ( "fails doctor readiness when network access is enabled without a declared policy" , async ( ) => {
217235 const workspace = createTempWorkspace ( "autolabos-doctor-network-undeclared-" ) ;
236+ await seedDoctorTooling ( workspace ) ;
218237 await writeFile ( path . join ( workspace , "ISSUES.md" ) , VALID_ISSUE_MARKDOWN , "utf8" ) ;
219238 await writeJson ( path . join ( workspace , ".autolabos" , "runs" , "runs.json" ) , { runs : [ ] } ) ;
220239
221- const report = await runDoctorReport ( createCodexStub ( ) , {
222- workspaceRoot : workspace ,
223- includeHarnessValidation : false ,
224- approvalMode : "manual" ,
225- executionApprovalMode : "manual" ,
226- dependencyMode : "local" ,
227- sessionMode : "fresh" ,
228- codeExecutionExpected : true ,
229- candidateIsolation : "attempt_snapshot_restore" ,
230- allowNetwork : true
231- } ) ;
240+ const report = await withWorkspacePath ( workspace , ( ) =>
241+ runDoctorReport ( createCodexStub ( ) , {
242+ workspaceRoot : workspace ,
243+ includeHarnessValidation : false ,
244+ approvalMode : "manual" ,
245+ executionApprovalMode : "manual" ,
246+ dependencyMode : "local" ,
247+ sessionMode : "fresh" ,
248+ codeExecutionExpected : true ,
249+ candidateIsolation : "attempt_snapshot_restore" ,
250+ allowNetwork : true
251+ } )
252+ ) ;
232253
233254 expect ( report . readiness . blocked ) . toBe ( true ) ;
234255 expect ( report . readiness . networkDeclarationPresent ) . toBe ( false ) ;
@@ -258,6 +279,34 @@ function createTempWorkspace(prefix: string): string {
258279 return dir ;
259280}
260281
282+ async function seedDoctorTooling ( workspace : string ) : Promise < void > {
283+ const binDir = path . join ( workspace , "bin" ) ;
284+ await mkdir ( binDir , { recursive : true } ) ;
285+ await writeExecutable ( path . join ( binDir , "python3" ) , "#!/bin/sh\nexit 0\n" ) ;
286+ await writeExecutable ( path . join ( binDir , "pip3" ) , "#!/bin/sh\nexit 0\n" ) ;
287+ await writeExecutable ( path . join ( binDir , "pdflatex" ) , "#!/bin/sh\nexit 0\n" ) ;
288+ }
289+
290+ async function writeExecutable ( filePath : string , content : string ) : Promise < void > {
291+ await writeFile ( filePath , content , "utf8" ) ;
292+ await chmod ( filePath , 0o755 ) ;
293+ }
294+
295+ async function withWorkspacePath < T > ( workspace : string , fn : ( ) => Promise < T > ) : Promise < T > {
296+ const originalPath = process . env . PATH ;
297+ const binDir = path . join ( workspace , "bin" ) ;
298+ process . env . PATH = `${ binDir } ${ path . delimiter } ${ originalPath || "" } ` ;
299+ try {
300+ return await fn ( ) ;
301+ } finally {
302+ if ( originalPath === undefined ) {
303+ delete process . env . PATH ;
304+ } else {
305+ process . env . PATH = originalPath ;
306+ }
307+ }
308+ }
309+
261310async function writeJson ( filePath : string , value : unknown ) : Promise < void > {
262311 await mkdir ( path . dirname ( filePath ) , { recursive : true } ) ;
263312 await writeFile ( filePath , `${ JSON . stringify ( value , null , 2 ) } \n` , "utf8" ) ;
0 commit comments