@@ -246,6 +246,121 @@ test("explainCommand writes explanations.json from persisted explain questions",
246246 ] ) ;
247247} ) ;
248248
249+ test ( "explainCommand resolves a nested failed artifact directory from the execution root" , async ( ) => {
250+ const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "skillgym-cli-" ) ) ;
251+ tempDirs . push ( tempDir ) ;
252+ const executionArtifactDir = path . join ( tempDir , "alpha" , "open-main" ) ;
253+ const artifactDir = path . join ( executionArtifactDir , "repeat-1" , "session-2" ) ;
254+ await mkdir ( artifactDir , { recursive : true } ) ;
255+ await writeFile (
256+ path . join ( artifactDir , "report.json" ) ,
257+ `${ JSON . stringify ( {
258+ runner : {
259+ id : "open-main" ,
260+ pathKey : "open-main" ,
261+ agent : { type : "opencode" , model : "openai/gpt-5" } ,
262+ } ,
263+ prompt : "prompt" ,
264+ usage : {
265+ inputChars : 0 ,
266+ outputChars : 0 ,
267+ reasoningChars : 0 ,
268+ source : { input : "chars" , output : "chars" , reasoning : "chars" } ,
269+ } ,
270+ files : { observedReads : [ ] , observedSkillReads : [ ] } ,
271+ detectedSkills : [ ] ,
272+ events : [ ] ,
273+ finalOutput : "" ,
274+ rawArtifacts : { } ,
275+ } ) } \n`,
276+ "utf8" ,
277+ ) ;
278+ await writeFile (
279+ path . join ( artifactDir , "explain.json" ) ,
280+ `${ JSON . stringify ( {
281+ suitePath : path . join ( tempDir , "suite.ts" ) ,
282+ caseId : "alpha" ,
283+ runnerId : "open-main" ,
284+ cwd : tempDir ,
285+ sessionId : "ses_123" ,
286+ questions : [
287+ {
288+ question : "Why did you skip SKILL.md?" ,
289+ source : { filePath : path . join ( tempDir , "suite.ts" ) , line : "12" , column : "5" } ,
290+ } ,
291+ ] ,
292+ } ) } \n`,
293+ "utf8" ,
294+ ) ;
295+
296+ const explain = vi . fn ( async ( ) => ( {
297+ answers : [
298+ {
299+ question : {
300+ question : "Why did you skip SKILL.md?" ,
301+ source : { filePath : path . join ( tempDir , "suite.ts" ) , line : "12" , column : "5" } ,
302+ } ,
303+ answer : "Because the prompt looked sufficient." ,
304+ sessionId : "ses_123" ,
305+ startedAt : "2026-05-07T10:00:00.000Z" ,
306+ endedAt : "2026-05-07T10:00:01.000Z" ,
307+ durationMs : 1_000 ,
308+ rawArtifacts : { } ,
309+ } ,
310+ ] ,
311+ } ) ) ;
312+
313+ vi . resetModules ( ) ;
314+ vi . doMock ( "../src/config.js" , ( ) => ( {
315+ loadConfig : vi . fn ( async ( ) => ( {
316+ config : {
317+ runners : {
318+ "open-main" : { agent : { type : "opencode" , model : "openai/gpt-5" } } ,
319+ } ,
320+ } ,
321+ } ) ) ,
322+ } ) ) ;
323+ vi . doMock ( "../src/adapters/index.js" , ( ) => ( {
324+ getAdapter : vi . fn ( ( ) => ( {
325+ run : vi . fn ( ) ,
326+ collect : vi . fn ( ) ,
327+ normalize : vi . fn ( ) ,
328+ explain,
329+ } ) ) ,
330+ } ) ) ;
331+
332+ const output : string [ ] = [ ] ;
333+ const stdout = {
334+ isTTY : false ,
335+ write ( value : string ) {
336+ output . push ( value ) ;
337+ return true ;
338+ } ,
339+ } ;
340+ try {
341+ const { explainCommandWithWriter } = await import ( "../src/cli/explain.js" ) ;
342+ await explainCommandWithWriter ( { artifactDir : executionArtifactDir } , stdout ) ;
343+ } finally {
344+ vi . doUnmock ( "../src/config.js" ) ;
345+ vi . doUnmock ( "../src/adapters/index.js" ) ;
346+ vi . resetModules ( ) ;
347+ }
348+
349+ expect ( explain ) . toHaveBeenCalledWith (
350+ expect . objectContaining ( {
351+ artifactDir,
352+ cwd : tempDir ,
353+ sessionId : "ses_123" ,
354+ } ) ,
355+ ) ;
356+ expect ( output . join ( "" ) ) . toContain ( artifactDir ) ;
357+ expect (
358+ JSON . parse ( await readFile ( path . join ( artifactDir , "explanations.json" ) , "utf8" ) ) as {
359+ sessionId : string ;
360+ } ,
361+ ) . toMatchObject ( { sessionId : "ses_123" } ) ;
362+ } ) ;
363+
249364test ( "explainCommand reuses existing explanations.json by default" , async ( ) => {
250365 const tempDir = await mkdtemp ( path . join ( os . tmpdir ( ) , "skillgym-cli-" ) ) ;
251366 tempDirs . push ( tempDir ) ;
0 commit comments