





















@@ -79,6 +79,69 @@ function createCandidate(rootDir: string, options: { id?: string } = {}): Plugin
7979};
8080}
818182+function requirePersisted(index: InstalledPluginIndex | null): InstalledPluginIndex {
83+expect(index).not.toBeNull();
84+if (!index) {
85+throw new Error("Expected persisted installed plugin index");
86+}
87+return index;
88+}
89+90+function expectPluginIds(index: InstalledPluginIndex, expected: string[]) {
91+expect(index.plugins.map((plugin) => plugin.pluginId)).toEqual(expected);
92+}
93+94+function expectPluginFields(
95+index: InstalledPluginIndex,
96+pluginId: string,
97+expected: Record<string, unknown>,
98+) {
99+const plugin = index.plugins.find((candidate) => candidate.pluginId === pluginId);
100+expect(plugin, pluginId).toBeDefined();
101+if (!plugin) {
102+throw new Error(`Missing plugin ${pluginId}`);
103+}
104+for (const [key, value] of Object.entries(expected)) {
105+expect(plugin[key as keyof typeof plugin], key).toEqual(value);
106+}
107+}
108+109+function expectInstallRecord(
110+index: InstalledPluginIndex,
111+pluginId: string,
112+expected: Record<string, unknown>,
113+) {
114+const record = index.installRecords[pluginId];
115+expect(record, pluginId).toBeDefined();
116+if (!record) {
117+throw new Error(`Missing install record ${pluginId}`);
118+}
119+for (const [key, value] of Object.entries(expected)) {
120+expect(record[key as keyof typeof record], key).toEqual(value);
121+}
122+}
123+124+async function expectPersistedIndex(
125+stateDir: string,
126+expected: {
127+refreshReason?: string;
128+pluginIds?: string[];
129+installRecords?: Record<string, Record<string, unknown>>;
130+},
131+) {
132+const persisted = requirePersisted(await readPersistedInstalledPluginIndex({ stateDir }));
133+if (expected.refreshReason !== undefined) {
134+expect(persisted.refreshReason).toBe(expected.refreshReason);
135+}
136+if (expected.pluginIds) {
137+expectPluginIds(persisted, expected.pluginIds);
138+}
139+for (const [pluginId, fields] of Object.entries(expected.installRecords ?? {})) {
140+expectInstallRecord(persisted, pluginId, fields);
141+}
142+return persisted;
143+}
144+82145describe("installed plugin index persistence", () => {
83146it("resolves the persisted index path under the state plugins directory", () => {
84147const stateDir = makeTempDir();
@@ -101,7 +164,10 @@ describe("installed plugin index persistence", () => {
101164if (process.platform !== "win32") {
102165expect(fs.statSync(filePath).mode & 0o777).toBe(0o600);
103166}
104-await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject(index);
167+const persisted = requirePersisted(await readPersistedInstalledPluginIndex({ stateDir }));
168+expect(persisted.version).toBe(index.version);
169+expect(persisted.policyHash).toBe(index.policyHash);
170+expectPluginIds(persisted, ["demo"]);
105171});
106172107173it("does not preserve prototype poison keys from persisted index JSON", async () => {
@@ -128,12 +194,9 @@ describe("installed plugin index persistence", () => {
128194129195const persisted = await readPersistedInstalledPluginIndex({ stateDir });
130196131-expect(persisted).toMatchObject({
132-plugins: [expect.objectContaining({ pluginId: "demo" })],
133-installRecords: {
134-demo: expect.objectContaining({ source: "npm" }),
135-},
136-});
197+const persistedIndex = requirePersisted(persisted);
198+expectPluginIds(persistedIndex, ["demo"]);
199+expectInstallRecord(persistedIndex, "demo", { source: "npm" });
137200expect(Object.prototype.hasOwnProperty.call(persisted as object, "__proto__")).toBe(false);
138201expect(Object.prototype.hasOwnProperty.call(persisted?.installRecords ?? {}, "__proto__")).toBe(
139202false,
@@ -174,16 +237,15 @@ describe("installed plugin index persistence", () => {
174237VITEST: "true",
175238};
176239177-await expect(
178-inspectPersistedInstalledPluginIndex({ stateDir, candidates: [candidate], env }),
179-).resolves.toMatchObject({
180-state: "missing",
181-refreshReasons: ["missing"],
182-persisted: null,
183-current: {
184-plugins: [expect.objectContaining({ pluginId: "demo" })],
185-},
240+const missingInspect = await inspectPersistedInstalledPluginIndex({
241+ stateDir,
242+candidates: [candidate],
243+ env,
186244});
245+expect(missingInspect.state).toBe("missing");
246+expect(missingInspect.refreshReasons).toEqual(["missing"]);
247+expect(missingInspect.persisted).toBeNull();
248+expectPluginIds(missingInspect.current, ["demo"]);
187249188250const current = await refreshPersistedInstalledPluginIndex({
189251reason: "manual",
@@ -192,40 +254,34 @@ describe("installed plugin index persistence", () => {
192254 env,
193255});
194256195-await expect(
196-inspectPersistedInstalledPluginIndex({ stateDir, candidates: [candidate], env }),
197-).resolves.toMatchObject({
198-state: "fresh",
199-refreshReasons: [],
200-persisted: current,
201-current: {
202-plugins: [expect.objectContaining({ pluginId: "demo", enabled: true })],
203-},
257+const freshInspect = await inspectPersistedInstalledPluginIndex({
258+ stateDir,
259+candidates: [candidate],
260+ env,
204261});
262+expect(freshInspect.state).toBe("fresh");
263+expect(freshInspect.refreshReasons).toEqual([]);
264+expect(freshInspect.persisted).toEqual(current);
265+expectPluginFields(freshInspect.current, "demo", { enabled: true });
205266206-await expect(
207-inspectPersistedInstalledPluginIndex({
208- stateDir,
209-candidates: [candidate],
210-config: {
211-plugins: {
212-entries: {
213-demo: {
214-enabled: false,
215-},
267+const policyInspect = await inspectPersistedInstalledPluginIndex({
268+ stateDir,
269+candidates: [candidate],
270+config: {
271+plugins: {
272+entries: {
273+demo: {
274+enabled: false,
216275},
217276},
218277},
219- env,
220-}),
221-).resolves.toMatchObject({
222-state: "stale",
223-refreshReasons: ["policy-changed"],
224-persisted: current,
225-current: {
226-plugins: [expect.objectContaining({ pluginId: "demo", enabled: false })],
227278},
279+ env,
228280});
281+expect(policyInspect.state).toBe("stale");
282+expect(policyInspect.refreshReasons).toEqual(["policy-changed"]);
283+expect(policyInspect.persisted).toEqual(current);
284+expectPluginFields(policyInspect.current, "demo", { enabled: false });
229285230286fs.writeFileSync(
231287path.join(pluginDir, "openclaw.plugin.json"),
@@ -238,20 +294,15 @@ describe("installed plugin index persistence", () => {
238294"utf8",
239295);
240296241-await expect(
242-inspectPersistedInstalledPluginIndex({ stateDir, candidates: [candidate], env }),
243-).resolves.toMatchObject({
244-state: "stale",
245-refreshReasons: ["stale-manifest"],
246-persisted: current,
247-current: {
248-plugins: [
249-expect.objectContaining({
250-pluginId: "demo",
251-}),
252-],
253-},
297+const staleManifestInspect = await inspectPersistedInstalledPluginIndex({
298+ stateDir,
299+candidates: [candidate],
300+ env,
254301});
302+expect(staleManifestInspect.state).toBe("stale");
303+expect(staleManifestInspect.refreshReasons).toEqual(["stale-manifest"]);
304+expect(staleManifestInspect.persisted).toEqual(current);
305+expectPluginIds(staleManifestInspect.current, ["demo"]);
255306});
256307257308it("refreshes and persists a rebuilt index without loading plugin runtime", async () => {
@@ -273,9 +324,9 @@ describe("installed plugin index persistence", () => {
273324274325expect(index.refreshReason).toBe("manual");
275326expect(index.plugins.map((plugin) => plugin.pluginId)).toEqual(["demo"]);
276-await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
327+await expectPersistedIndex(stateDir, {
277328refreshReason: "manual",
278-plugins: [expect.objectContaining({ pluginId: "demo" })],
329+pluginIds: ["demo"],
279330});
280331});
281332@@ -324,7 +375,7 @@ describe("installed plugin index persistence", () => {
324375});
325376326377expect(refreshed.plugins).toHaveLength(initial.plugins.length);
327-expect(refreshed.plugins[0]).toMatchObject({
378+expectPluginFields(refreshed, "demo", {
328379pluginId: "demo",
329380enabled: false,
330381manifestHash: initial.plugins[0]?.manifestHash,
@@ -399,25 +450,21 @@ describe("installed plugin index persistence", () => {
399450},
400451});
401452402-expect(index).toMatchObject({
403-installRecords: {
404-missing: {
405-source: "npm",
406-spec: "missing-plugin@1.0.0",
407-installPath: path.join(stateDir, "plugins", "missing"),
408-},
409-},
410-plugins: [],
453+expectInstallRecord(index, "missing", {
454+source: "npm",
455+spec: "missing-plugin@1.0.0",
456+installPath: path.join(stateDir, "plugins", "missing"),
411457});
412-await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
458+expectPluginIds(index, []);
459+await expectPersistedIndex(stateDir, {
460+pluginIds: [],
413461installRecords: {
414462missing: {
415463source: "npm",
416464spec: "missing-plugin@1.0.0",
417465installPath: path.join(stateDir, "plugins", "missing"),
418466},
419467},
420-plugins: [],
421468});
422469});
423470@@ -466,34 +513,34 @@ describe("installed plugin index persistence", () => {
466513},
467514});
468515469-const expected = {
516+const expectedRecord = {
517+source: "clawhub",
518+spec: "clawhub:clawpack-demo@2026.5.1-beta.2",
519+ installPath,
520+version: "2026.5.1-beta.2",
521+integrity: "sha256-archive",
522+resolvedAt: "2026-05-01T00:00:00.000Z",
523+clawhubUrl: "https://clawhub.ai",
524+clawhubPackage: "clawpack-demo",
525+clawhubFamily: "code-plugin",
526+clawhubChannel: "official",
527+artifactKind: "npm-pack",
528+artifactFormat: "tgz",
529+npmIntegrity: "sha512-clawpack",
530+npmShasum: "1".repeat(40),
531+npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
532+clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
533+clawpackSpecVersion: 1,
534+clawpackManifestSha256: "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
535+clawpackSize: 4096,
536+};
537+expectInstallRecord(index, "clawpack-demo", expectedRecord);
538+expectPluginIds(index, []);
539+await expectPersistedIndex(stateDir, {
540+pluginIds: [],
470541installRecords: {
471-"clawpack-demo": {
472-source: "clawhub",
473-spec: "clawhub:clawpack-demo@2026.5.1-beta.2",
474- installPath,
475-version: "2026.5.1-beta.2",
476-integrity: "sha256-archive",
477-resolvedAt: "2026-05-01T00:00:00.000Z",
478-clawhubUrl: "https://clawhub.ai",
479-clawhubPackage: "clawpack-demo",
480-clawhubFamily: "code-plugin",
481-clawhubChannel: "official",
482-artifactKind: "npm-pack",
483-artifactFormat: "tgz",
484-npmIntegrity: "sha512-clawpack",
485-npmShasum: "1".repeat(40),
486-npmTarballName: "clawpack-demo-2026.5.1-beta.2.tgz",
487-clawpackSha256: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
488-clawpackSpecVersion: 1,
489-clawpackManifestSha256:
490-"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
491-clawpackSize: 4096,
492-},
542+"clawpack-demo": expectedRecord,
493543},
494-plugins: [],
495-};
496-expect(index).toMatchObject(expected);
497-await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject(expected);
544+});
498545});
499546});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。