





















@@ -0,0 +1,205 @@
1+import fs from "node:fs/promises";
2+import path from "node:path";
3+import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
4+import { resolveEmbeddedRunSkillEntries } from "../../agents/pi-embedded-runner/skills-runtime.js";
5+import { createCanonicalFixtureSkill } from "../../agents/skills.test-helpers.js";
6+import type { Skill } from "../../agents/skills/skill-contract.js";
7+import { createSuiteTempRootTracker } from "../../test-helpers/temp-dir.js";
8+import type { SessionEntry, SessionSkillSnapshot } from "./types.js";
9+10+vi.mock("../config.js", async () => ({
11+ ...(await vi.importActual<typeof import("../config.js")>("../config.js")),
12+getRuntimeConfig: vi.fn().mockReturnValue({}),
13+}));
14+15+import {
16+clearSessionStoreCacheForTest,
17+loadSessionStore,
18+saveSessionStore,
19+updateSessionStore,
20+} from "./store.js";
21+22+const suiteRootTracker = createSuiteTempRootTracker({ prefix: "openclaw-skills-strip-" });
23+24+function makeFixtureSkill(name: string, bodySize = 3000): Skill {
25+// 3KB body simulates a realistic SKILL.md.
26+const source = `# ${name}\n\n${"x".repeat(bodySize)}`;
27+return createCanonicalFixtureSkill({
28+ name,
29+description: `${name} skill description`,
30+filePath: `/skills/${name}/SKILL.md`,
31+baseDir: `/skills/${name}`,
32+ source,
33+});
34+}
35+36+function makeSnapshot(skillCount: number): SessionSkillSnapshot {
37+const resolved = Array.from({ length: skillCount }, (_, i) => makeFixtureSkill(`skill-${i}`));
38+return {
39+prompt: "<available_skills>...</available_skills>",
40+skills: resolved.map((s) => ({ name: s.name })),
41+skillFilter: undefined,
42+resolvedSkills: resolved,
43+version: 1,
44+};
45+}
46+47+function makeEntry(sessionId: string, snapshot?: SessionSkillSnapshot): SessionEntry {
48+return {
49+ sessionId,
50+updatedAt: Date.now(),
51+skillsSnapshot: snapshot,
52+};
53+}
54+55+describe("session store strips resolvedSkills from persistence", () => {
56+let testDir: string;
57+let storePath: string;
58+let savedCacheTtl: string | undefined;
59+60+beforeAll(async () => {
61+await suiteRootTracker.setup();
62+});
63+64+afterAll(async () => {
65+await suiteRootTracker.cleanup();
66+});
67+68+beforeEach(async () => {
69+testDir = await suiteRootTracker.make("case");
70+storePath = path.join(testDir, "sessions.json");
71+savedCacheTtl = process.env.OPENCLAW_SESSION_CACHE_TTL_MS;
72+process.env.OPENCLAW_SESSION_CACHE_TTL_MS = "0";
73+clearSessionStoreCacheForTest();
74+});
75+76+afterEach(() => {
77+clearSessionStoreCacheForTest();
78+if (savedCacheTtl === undefined) {
79+delete process.env.OPENCLAW_SESSION_CACHE_TTL_MS;
80+} else {
81+process.env.OPENCLAW_SESSION_CACHE_TTL_MS = savedCacheTtl;
82+}
83+});
84+85+it("does not write resolvedSkills to disk", async () => {
86+const store = {
87+"agent:main:test:1": makeEntry("session-1", makeSnapshot(5)),
88+};
89+90+await saveSessionStore(storePath, store, { skipMaintenance: true });
91+92+const raw = await fs.readFile(storePath, "utf-8");
93+expect(raw).not.toContain("resolvedSkills");
94+expect(raw).not.toContain("xxxxx"); // none of the skill source bodies leaked
95+const parsed = JSON.parse(raw) as Record<string, SessionEntry>;
96+expect(parsed["agent:main:test:1"]?.skillsSnapshot?.resolvedSkills).toBeUndefined();
97+});
98+99+it("preserves prompt, skills, skillFilter, and version on roundtrip", async () => {
100+const snapshot = makeSnapshot(3);
101+snapshot.skillFilter = ["skill-0"];
102+const store = {
103+"agent:main:test:1": makeEntry("session-1", snapshot),
104+};
105+106+await saveSessionStore(storePath, store, { skipMaintenance: true });
107+const loaded = loadSessionStore(storePath, { skipCache: true });
108+109+const persistedSnapshot = loaded["agent:main:test:1"]?.skillsSnapshot;
110+expect(persistedSnapshot).toBeDefined();
111+expect(persistedSnapshot?.prompt).toBe(snapshot.prompt);
112+expect(persistedSnapshot?.skills).toEqual(snapshot.skills);
113+expect(persistedSnapshot?.skillFilter).toEqual(["skill-0"]);
114+expect(persistedSnapshot?.version).toBe(1);
115+expect(persistedSnapshot?.resolvedSkills).toBeUndefined();
116+});
117+118+it("strips resolvedSkills from a legacy sessions.json on load", async () => {
119+// Hand-craft a pre-fix file with embedded resolvedSkills.
120+const legacy = {
121+"agent:main:test:1": makeEntry("session-1", makeSnapshot(4)),
122+};
123+await fs.mkdir(path.dirname(storePath), { recursive: true });
124+const rawLegacy = JSON.stringify(legacy, null, 2);
125+expect(rawLegacy).toContain("resolvedSkills");
126+await fs.writeFile(storePath, rawLegacy, "utf-8");
127+128+const loaded = loadSessionStore(storePath, { skipCache: true });
129+expect(loaded["agent:main:test:1"]?.skillsSnapshot?.resolvedSkills).toBeUndefined();
130+expect(loaded["agent:main:test:1"]?.skillsSnapshot?.prompt).toBe(
131+legacy["agent:main:test:1"].skillsSnapshot?.prompt,
132+);
133+134+// Saving the loaded record should rewrite the file in stripped form.
135+await saveSessionStore(storePath, loaded, { skipMaintenance: true });
136+const rawAfter = await fs.readFile(storePath, "utf-8");
137+expect(rawAfter).not.toContain("resolvedSkills");
138+});
139+140+it("strips resolvedSkills written via updateSessionStore mutator", async () => {
141+// Simulate the production hot path where ensureSkillSnapshot puts a
142+// freshly-built snapshot (with resolvedSkills) into the store via mutator.
143+await updateSessionStore(
144+storePath,
145+(store) => {
146+store["agent:main:test:1"] = makeEntry("session-1", makeSnapshot(6));
147+},
148+{ skipMaintenance: true },
149+);
150+151+const raw = await fs.readFile(storePath, "utf-8");
152+expect(raw).not.toContain("resolvedSkills");
153+const reloaded = loadSessionStore(storePath, { skipCache: true });
154+expect(reloaded["agent:main:test:1"]?.skillsSnapshot?.resolvedSkills).toBeUndefined();
155+expect(reloaded["agent:main:test:1"]?.skillsSnapshot?.skills).toHaveLength(6);
156+});
157+158+it("keeps the on-disk file small with many sessions and skills", async () => {
159+const SESSION_COUNT = 100;
160+const SKILLS_PER_SESSION = 50;
161+const store: Record<string, SessionEntry> = {};
162+for (let i = 0; i < SESSION_COUNT; i += 1) {
163+store[`agent:main:scale:${i}`] = makeEntry(`session-${i}`, makeSnapshot(SKILLS_PER_SESSION));
164+}
165+166+await saveSessionStore(storePath, store, { skipMaintenance: true });
167+168+const stat = await fs.stat(storePath);
169+// Pre-fix: ~SESSION_COUNT * SKILLS_PER_SESSION * ~3KB ≈ 15MB.
170+// Post-fix: only the lightweight `skills` array + prompt per entry.
171+// Conservative budget that comfortably covers metadata growth.
172+expect(stat.size).toBeLessThan(2 * 1024 * 1024);
173+});
174+});
175+176+describe("embedded runner falls back to disk when resolvedSkills is absent", () => {
177+it("signals shouldLoadSkillEntries when the persisted snapshot has no resolvedSkills", () => {
178+const result = resolveEmbeddedRunSkillEntries({
179+workspaceDir: "/nonexistent-workspace-for-test",
180+skillsSnapshot: {
181+prompt: "",
182+skills: [{ name: "x" }],
183+version: 1,
184+// resolvedSkills intentionally omitted — this is the post-fix shape.
185+},
186+});
187+188+expect(result.shouldLoadSkillEntries).toBe(true);
189+});
190+191+it("skips loading when resolvedSkills is present (in-turn cache hot path)", () => {
192+const result = resolveEmbeddedRunSkillEntries({
193+workspaceDir: "/nonexistent-workspace-for-test",
194+skillsSnapshot: {
195+prompt: "",
196+skills: [{ name: "x" }],
197+resolvedSkills: [makeFixtureSkill("x", 100)],
198+version: 1,
199+},
200+});
201+202+expect(result.shouldLoadSkillEntries).toBe(false);
203+expect(result.skillEntries).toEqual([]);
204+});
205+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。