





















@@ -0,0 +1,371 @@
1+import fs from "node:fs/promises";
2+import os from "node:os";
3+import path from "node:path";
4+import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5+import type { SessionEntry } from "../config/sessions/types.js";
6+import type { OpenClawConfig } from "../config/types.openclaw.js";
7+8+const note = vi.hoisted(() => vi.fn());
9+10+vi.mock("../terminal/note.js", () => ({
11+ note,
12+}));
13+14+import {
15+noteSessionSnapshotHealth,
16+scanSessionStoreForStaleRuntimeSnapshotPaths,
17+} from "./doctor-session-snapshots.js";
18+19+function sessionEntry(patch: Partial<SessionEntry>): SessionEntry {
20+return {
21+sessionId: "session-1",
22+updatedAt: Date.now(),
23+ ...patch,
24+};
25+}
26+27+function skillPrompt(location: string): string {
28+return [
29+"<available_skills>",
30+" <skill>",
31+" <name>doctor</name>",
32+" <description>Doctor skill</description>",
33+` <location>${location}</location>`,
34+" </skill>",
35+"</available_skills>",
36+].join("\n");
37+}
38+39+async function writeSessionStore(
40+storePath: string,
41+store: Record<string, SessionEntry>,
42+): Promise<void> {
43+await fs.mkdir(path.dirname(storePath), { recursive: true });
44+await fs.writeFile(storePath, JSON.stringify(store, null, 2));
45+}
46+47+describe("doctor session snapshot stale runtime metadata", () => {
48+let root = "";
49+let bundledSkillsDir = "";
50+51+beforeEach(async () => {
52+note.mockClear();
53+root = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-doctor-session-snapshots-"));
54+bundledSkillsDir = path.join(root, "current", "skills");
55+await fs.mkdir(path.join(bundledSkillsDir, "doctor"), { recursive: true });
56+await fs.writeFile(path.join(bundledSkillsDir, "doctor", "SKILL.md"), "# Doctor\n");
57+});
58+59+afterEach(async () => {
60+await fs.rm(root, { recursive: true, force: true });
61+});
62+63+it("flags cached bundled skill locations from inactive and temp-backed runtime roots", () => {
64+const stalePath = path.join(
65+root,
66+"old-runtime",
67+"node_modules",
68+"openclaw",
69+"skills",
70+"doctor",
71+"SKILL.md",
72+);
73+const tempBackedPath = path.join(
74+path.sep,
75+"private",
76+"tmp",
77+"openclaw",
78+"skills",
79+"doctor",
80+"SKILL.md",
81+);
82+const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
83+ bundledSkillsDir,
84+store: {
85+"agent:main": sessionEntry({
86+skillsSnapshot: {
87+prompt: skillPrompt(stalePath),
88+skills: [{ name: "doctor" }],
89+},
90+}),
91+"agent:temp": sessionEntry({
92+skillsSnapshot: {
93+prompt: skillPrompt(tempBackedPath),
94+skills: [{ name: "doctor" }],
95+},
96+}),
97+},
98+});
99+100+expect(findings).toEqual([
101+{
102+sessionKey: "agent:main",
103+field: "skillsSnapshot.prompt",
104+cachedPath: stalePath,
105+expectedPath: path.join(bundledSkillsDir, "doctor", "SKILL.md"),
106+},
107+{
108+sessionKey: "agent:temp",
109+field: "skillsSnapshot.prompt",
110+cachedPath: tempBackedPath,
111+expectedPath: path.join(bundledSkillsDir, "doctor", "SKILL.md"),
112+},
113+]);
114+});
115+116+it("expands home-relative cached bundled skill locations before classifying them", () => {
117+const homeDir = path.join(root, "home");
118+const stalePath = "~/old-runtime/node_modules/openclaw/skills/doctor/SKILL.md";
119+120+const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
121+ bundledSkillsDir,
122+env: { HOME: homeDir },
123+store: {
124+"agent:home": sessionEntry({
125+skillsSnapshot: {
126+prompt: skillPrompt(stalePath),
127+skills: [{ name: "doctor" }],
128+},
129+}),
130+},
131+});
132+133+expect(findings).toEqual([
134+{
135+sessionKey: "agent:home",
136+field: "skillsSnapshot.prompt",
137+cachedPath: stalePath,
138+expectedPath: path.join(bundledSkillsDir, "doctor", "SKILL.md"),
139+},
140+]);
141+});
142+143+it("ignores current bundled locations and unrelated workspace skill locations", () => {
144+const currentPath = path.join(bundledSkillsDir, "doctor", "SKILL.md");
145+const workspacePath = path.join(root, "workspace", "skills", "doctor", "SKILL.md");
146+const openClawWorkspacePath = path.join(
147+root,
148+"projects",
149+"openclaw",
150+"skills",
151+"doctor",
152+"SKILL.md",
153+);
154+const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
155+ bundledSkillsDir,
156+store: {
157+"agent:current": sessionEntry({
158+skillsSnapshot: { prompt: skillPrompt(currentPath), skills: [{ name: "doctor" }] },
159+}),
160+"agent:workspace": sessionEntry({
161+skillsSnapshot: { prompt: skillPrompt(workspacePath), skills: [{ name: "doctor" }] },
162+}),
163+"agent:openclaw-workspace": sessionEntry({
164+skillsSnapshot: {
165+prompt: skillPrompt(openClawWorkspacePath),
166+skills: [{ name: "doctor" }],
167+},
168+}),
169+},
170+pathExists: (filePath) => filePath === currentPath,
171+});
172+173+expect(findings).toEqual([]);
174+});
175+176+it("handles Windows current and stale bundled skill paths without false positives", () => {
177+const windowsBundledSkillsDir = path.win32.join(
178+"C:\\",
179+"Users",
180+"alice",
181+".openclaw",
182+"lib",
183+"node_modules",
184+"openclaw",
185+"skills",
186+);
187+const currentPath = path.win32.join(windowsBundledSkillsDir, "doctor", "SKILL.md");
188+const stalePath = path.win32.join(
189+"C:\\",
190+"opt",
191+"node_modules",
192+"openclaw",
193+"skills",
194+"doctor",
195+"SKILL.md",
196+);
197+198+const findings = scanSessionStoreForStaleRuntimeSnapshotPaths({
199+bundledSkillsDir: windowsBundledSkillsDir,
200+store: {
201+"agent:current": sessionEntry({
202+skillsSnapshot: { prompt: skillPrompt(currentPath), skills: [{ name: "doctor" }] },
203+}),
204+"agent:stale": sessionEntry({
205+skillsSnapshot: { prompt: skillPrompt(stalePath), skills: [{ name: "doctor" }] },
206+}),
207+},
208+pathExists: (filePath) => filePath === currentPath,
209+});
210+211+expect(findings).toEqual([
212+{
213+sessionKey: "agent:stale",
214+field: "skillsSnapshot.prompt",
215+cachedPath: stalePath,
216+expectedPath: currentPath,
217+},
218+]);
219+});
220+221+it("reports stale cached metadata while distinguishing the live runtime root", async () => {
222+const stalePath = path.join(
223+root,
224+"old-runtime",
225+"node_modules",
226+"openclaw",
227+"skills",
228+"doctor",
229+"SKILL.md",
230+);
231+const storePath = path.join(root, "state", "agents", "main", "sessions", "sessions.json");
232+await writeSessionStore(storePath, {
233+"agent:main": sessionEntry({
234+skillsSnapshot: {
235+prompt: skillPrompt(stalePath),
236+skills: [{ name: "doctor" }],
237+},
238+}),
239+});
240+241+await noteSessionSnapshotHealth({ storePaths: [storePath], bundledSkillsDir });
242+243+expect(note).toHaveBeenCalledTimes(1);
244+const [message, title] = note.mock.calls[0] as [string, string];
245+expect(title).toBe("Session snapshots");
246+expect(message).toContain("stale cached session metadata paths");
247+expect(message).toContain("Live bundled skills root is healthy");
248+expect(message).toContain("inactive runtime root");
249+expect(message).toContain(stalePath);
250+expect(message).toContain(path.join(bundledSkillsDir, "doctor", "SKILL.md"));
251+});
252+253+it("scans resolvedSkills before session store normalization strips them", async () => {
254+const stalePath = path.join(
255+root,
256+"old-runtime",
257+"node_modules",
258+"openclaw",
259+"skills",
260+"doctor",
261+"SKILL.md",
262+);
263+const storePath = path.join(root, "state", "agents", "main", "sessions", "sessions.json");
264+await writeSessionStore(storePath, {
265+"agent:main": sessionEntry({
266+skillsSnapshot: {
267+prompt: "",
268+skills: [{ name: "doctor" }],
269+resolvedSkills: [
270+{
271+name: "doctor",
272+description: "Doctor skill",
273+source: "bundled",
274+filePath: stalePath,
275+baseDir: path.dirname(stalePath),
276+sourceInfo: {
277+path: stalePath,
278+source: "bundled",
279+scope: "user",
280+origin: "top-level",
281+baseDir: path.dirname(stalePath),
282+},
283+disableModelInvocation: false,
284+},
285+],
286+},
287+}),
288+});
289+290+await noteSessionSnapshotHealth({ storePaths: [storePath], bundledSkillsDir });
291+292+expect(note).toHaveBeenCalledTimes(1);
293+const [message] = note.mock.calls[0] as [string, string];
294+expect(message).toContain("agent:main");
295+expect(message).toContain("skillsSnapshot.resolvedSkills");
296+expect(message).toContain(stalePath);
297+});
298+299+it("reports stale cached metadata from configured session stores", async () => {
300+const stalePath = path.join(
301+root,
302+"old-runtime",
303+"node_modules",
304+"openclaw",
305+"skills",
306+"doctor",
307+"SKILL.md",
308+);
309+const stateDir = path.join(root, "state");
310+const defaultStorePath = path.join(stateDir, "agents", "main", "sessions", "sessions.json");
311+const configuredStorePath = path.join(root, "configured-sessions.json");
312+await writeSessionStore(defaultStorePath, {});
313+await writeSessionStore(configuredStorePath, {
314+"agent:configured": sessionEntry({
315+skillsSnapshot: {
316+prompt: skillPrompt(stalePath),
317+skills: [{ name: "doctor" }],
318+},
319+}),
320+});
321+322+await noteSessionSnapshotHealth({
323+cfg: { session: { store: configuredStorePath } } as OpenClawConfig,
324+ bundledSkillsDir,
325+env: { OPENCLAW_STATE_DIR: stateDir },
326+});
327+328+expect(note).toHaveBeenCalledTimes(1);
329+const [message] = note.mock.calls[0] as [string, string];
330+expect(message).toContain(configuredStorePath);
331+expect(message).toContain("agent:configured");
332+expect(message).toContain(stalePath);
333+});
334+335+it("reports stale cached metadata from templated configured session stores", async () => {
336+const stalePath = path.join(
337+root,
338+"old-runtime",
339+"node_modules",
340+"openclaw",
341+"skills",
342+"doctor",
343+"SKILL.md",
344+);
345+const templatedStore = path.join(root, "stores", "{agentId}", "sessions.json");
346+const opsStorePath = path.join(root, "stores", "ops", "sessions.json");
347+await writeSessionStore(opsStorePath, {
348+"agent:ops": sessionEntry({
349+skillsSnapshot: {
350+prompt: skillPrompt(stalePath),
351+skills: [{ name: "doctor" }],
352+},
353+}),
354+});
355+356+await noteSessionSnapshotHealth({
357+cfg: {
358+session: { store: templatedStore },
359+agents: { list: [{ id: "main" }, { id: "ops" }] },
360+} as OpenClawConfig,
361+ bundledSkillsDir,
362+env: { OPENCLAW_STATE_DIR: path.join(root, "state") },
363+});
364+365+expect(note).toHaveBeenCalledTimes(1);
366+const [message] = note.mock.calls[0] as [string, string];
367+expect(message).toContain(opsStorePath);
368+expect(message).toContain("agent:ops");
369+expect(message).toContain(stalePath);
370+});
371+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。