






















@@ -15,8 +15,11 @@ vi.mock("../../packages/terminal-core/src/note.js", () => ({
1515}));
16161717import {
18+detectSessionSnapshotHealthIssues,
1819noteSessionSnapshotHealth,
1920scanSessionStoreForStaleRuntimeSnapshotPaths,
21+sessionSnapshotIssueToHealthFinding,
22+sessionSnapshotIssueToRepairEffect,
2023} from "./doctor-session-snapshots.js";
21242225function sessionEntry(patch: Partial<SessionEntry>): SessionEntry {
@@ -66,6 +69,23 @@ async function writeSessionStore(
6669await fs.writeFile(storePath, JSON.stringify(store, null, 2));
6770}
687172+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", () => {
7090let root = "";
7191let bundledSkillsDir = "";
@@ -135,6 +155,57 @@ describe("doctor session snapshot stale runtime metadata", () => {
135155]);
136156});
137157158+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+138209it("expands home-relative cached bundled skill locations before classifying them", () => {
139210const homeDir = path.join(root, "home");
140211const stalePath = "~/old-runtime/node_modules/openclaw/skills/doctor/SKILL.md";
@@ -456,8 +527,9 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
456527});
457528458529const 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"));
461533expect(note).toHaveBeenCalledTimes(1);
462534const [message] = note.mock.calls[0] as [string, string];
463535expect(message).toContain("Repaired");
@@ -535,9 +607,13 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
535607536608const raw = await fs.readFile(storePath, "utf-8");
537609const 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);
541617expect(note).toHaveBeenCalledTimes(1);
542618const [message] = note.mock.calls[0] as [string, string];
543619expect(message).toContain("Repaired");
@@ -576,9 +652,12 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
576652});
577653578654const 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));
582661expect(note).toHaveBeenCalledTimes(1);
583662const [message] = note.mock.calls[0] as [string, string];
584663expect(message).toContain("Repaired");
@@ -743,7 +822,8 @@ describe("doctor session snapshot repair (shouldRepair)", () => {
743822expect(backupFiles.length).toBe(1);
744823745824const 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});
748828749829it("is idempotent — second repair finds nothing", async () => {
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。