@@ -15,8 +15,11 @@ vi.mock("../../packages/terminal-core/src/note.js", () => ({
1515} ) ) ;
1616
1717import {
18+ detectSessionSnapshotHealthIssues ,
1819 noteSessionSnapshotHealth ,
1920 scanSessionStoreForStaleRuntimeSnapshotPaths ,
21+ sessionSnapshotIssueToHealthFinding ,
22+ sessionSnapshotIssueToRepairEffect ,
2023} from "./doctor-session-snapshots.js" ;
2124
2225function sessionEntry ( patch : Partial < SessionEntry > ) : SessionEntry {
@@ -66,6 +69,23 @@ async function writeSessionStore(
6669 await fs . writeFile ( storePath , JSON . stringify ( store , null , 2 ) ) ;
6770}
6871
72+ function readMainSessionEntry ( raw : string ) : SessionEntry {
73+ const parsed = JSON . parse ( raw ) as Record < string , SessionEntry > ;
74+ const entry = parsed [ "agent:main" ] ;
75+ if ( ! entry ) {
76+ throw new Error ( "expected agent:main session entry" ) ;
77+ }
78+ return entry ;
79+ }
80+
81+ function readMainSkillsSnapshot ( raw : string ) : NonNullable < SessionEntry [ "skillsSnapshot" ] > {
82+ const snapshot = readMainSessionEntry ( raw ) . skillsSnapshot ;
83+ if ( ! snapshot ) {
84+ throw new Error ( "expected agent:main skills snapshot" ) ;
85+ }
86+ return snapshot ;
87+ }
88+
6989describe ( "doctor session snapshot stale runtime metadata" , ( ) => {
7090 let root = "" ;
7191 let bundledSkillsDir = "" ;
@@ -135,6 +155,57 @@ describe("doctor session snapshot stale runtime metadata", () => {
135155 ] ) ;
136156 } ) ;
137157
158+ it ( "maps stale snapshot paths to structured findings and dry-run effects" , async ( ) => {
159+ const stalePath = path . join (
160+ root ,
161+ "old-runtime" ,
162+ "node_modules" ,
163+ "openclaw" ,
164+ "skills" ,
165+ "doctor" ,
166+ "SKILL.md" ,
167+ ) ;
168+ const storePath = path . join ( root , "state" , "agents" , "main" , "sessions" , "sessions.json" ) ;
169+ await writeSessionStore ( storePath , {
170+ "agent:main" : sessionEntry ( {
171+ skillsSnapshot : {
172+ prompt : skillPrompt ( stalePath ) ,
173+ skills : [ { name : "doctor" } ] ,
174+ } ,
175+ } ) ,
176+ } ) ;
177+
178+ const [ issue ] = await detectSessionSnapshotHealthIssues ( {
179+ storePaths : [ storePath ] ,
180+ bundledSkillsDir,
181+ } ) ;
182+
183+ if ( ! issue ) {
184+ throw new Error ( "expected session snapshot health issue" ) ;
185+ }
186+ expect ( issue ) . toMatchObject ( {
187+ storePath,
188+ sessionKey : "agent:main" ,
189+ field : "skillsSnapshot.prompt" ,
190+ cachedPath : stalePath ,
191+ expectedPath : path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ,
192+ } ) ;
193+ expect ( sessionSnapshotIssueToHealthFinding ( issue ) ) . toMatchObject ( {
194+ checkId : "core/doctor/session-snapshots" ,
195+ severity : "info" ,
196+ path : storePath ,
197+ target : stalePath ,
198+ requirement : expect . stringContaining ( bundledSkillsDir ) ,
199+ fixHint : expect . stringContaining ( "openclaw doctor --fix" ) ,
200+ } ) ;
201+ expect ( sessionSnapshotIssueToRepairEffect ( issue ) ) . toEqual ( {
202+ kind : "file" ,
203+ action : "would-rewrite-session-snapshot-path" ,
204+ target : storePath ,
205+ dryRunSafe : false ,
206+ } ) ;
207+ } ) ;
208+
138209 it ( "expands home-relative cached bundled skill locations before classifying them" , ( ) => {
139210 const homeDir = path . join ( root , "home" ) ;
140211 const stalePath = "~/old-runtime/node_modules/openclaw/skills/doctor/SKILL.md" ;
@@ -456,8 +527,9 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
456527 } ) ;
457528
458529 const raw = await fs . readFile ( storePath , "utf-8" ) ;
459- expect ( raw ) . not . toContain ( stalePath ) ;
460- expect ( raw ) . toContain ( path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ) ;
530+ const snapshot = readMainSkillsSnapshot ( raw ) ;
531+ expect ( snapshot . prompt ) . not . toContain ( stalePath ) ;
532+ expect ( snapshot . prompt ) . toContain ( path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ) ;
461533 expect ( note ) . toHaveBeenCalledTimes ( 1 ) ;
462534 const [ message ] = note . mock . calls [ 0 ] as [ string , string ] ;
463535 expect ( message ) . toContain ( "Repaired" ) ;
@@ -535,9 +607,13 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
535607
536608 const raw = await fs . readFile ( storePath , "utf-8" ) ;
537609 const expectedBaseDir = path . dirname ( path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ) ;
538- expect ( raw ) . toContain ( path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ) ;
539- expect ( raw ) . toContain ( expectedBaseDir ) ;
540- expect ( raw ) . not . toContain ( path . join ( root , "old-runtime" ) ) ;
610+ const expectedPath = path . join ( bundledSkillsDir , "doctor" , "SKILL.md" ) ;
611+ const snapshot = readMainSkillsSnapshot ( raw ) ;
612+ const skill = snapshot . resolvedSkills ?. [ 0 ] ;
613+ expect ( skill ?. filePath ) . toBe ( expectedPath ) ;
614+ expect ( skill ?. baseDir ) . toBe ( expectedBaseDir ) ;
615+ expect ( skill ?. sourceInfo . path ) . toBe ( expectedPath ) ;
616+ expect ( skill ?. sourceInfo . baseDir ) . toBe ( expectedBaseDir ) ;
541617 expect ( note ) . toHaveBeenCalledTimes ( 1 ) ;
542618 const [ message ] = note . mock . calls [ 0 ] as [ string , string ] ;
543619 expect ( message ) . toContain ( "Repaired" ) ;
@@ -576,9 +652,12 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
576652 } ) ;
577653
578654 const raw = await fs . readFile ( storePath , "utf-8" ) ;
579- expect ( raw ) . toContain ( currentPath ) ;
580- expect ( raw ) . toContain ( path . dirname ( currentPath ) ) ;
581- expect ( raw ) . not . toContain ( path . join ( root , "old-runtime" ) ) ;
655+ const snapshot = readMainSkillsSnapshot ( raw ) ;
656+ const repairedSkill = snapshot . resolvedSkills ?. [ 0 ] ;
657+ expect ( repairedSkill ?. filePath ) . toBe ( currentPath ) ;
658+ expect ( repairedSkill ?. baseDir ) . toBe ( path . dirname ( currentPath ) ) ;
659+ expect ( repairedSkill ?. sourceInfo . path ) . toBe ( currentPath ) ;
660+ expect ( repairedSkill ?. sourceInfo . baseDir ) . toBe ( path . dirname ( currentPath ) ) ;
582661 expect ( note ) . toHaveBeenCalledTimes ( 1 ) ;
583662 const [ message ] = note . mock . calls [ 0 ] as [ string , string ] ;
584663 expect ( message ) . toContain ( "Repaired" ) ;
@@ -743,7 +822,8 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
743822 expect ( backupFiles . length ) . toBe ( 1 ) ;
744823
745824 const backupContent = await fs . readFile ( path . join ( dir , backupFiles [ 0 ] ) , "utf-8" ) ;
746- expect ( backupContent ) . toContain ( stalePath ) ;
825+ const backupSnapshot = readMainSkillsSnapshot ( backupContent ) ;
826+ expect ( backupSnapshot . prompt ) . toContain ( stalePath ) ;
747827 } ) ;
748828
749829 it ( "is idempotent — second repair finds nothing" , async ( ) => {
0 commit comments