





















@@ -0,0 +1,211 @@
1+import fs from "node:fs";
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 { InstalledPluginIndex } from "./installed-plugin-index.js";
6+import type { PluginManifestRecord, PluginManifestRegistry } from "./manifest-registry.js";
7+import {
8+clearLoadPluginMetadataSnapshotMemo,
9+loadPluginMetadataSnapshot,
10+} from "./plugin-metadata-snapshot.js";
11+12+const loadPluginRegistrySnapshotWithMetadata = vi.hoisted(() => vi.fn());
13+const loadPluginManifestRegistryForInstalledIndex = vi.hoisted(() => vi.fn());
14+15+vi.mock("./plugin-registry.js", async (importOriginal) => {
16+const actual = await importOriginal<typeof import("./plugin-registry.js")>();
17+return {
18+ ...actual,
19+loadPluginRegistrySnapshotWithMetadata: (params: unknown) =>
20+loadPluginRegistrySnapshotWithMetadata(params),
21+};
22+});
23+24+vi.mock("./manifest-registry-installed.js", async (importOriginal) => {
25+const actual = await importOriginal<typeof import("./manifest-registry-installed.js")>();
26+return {
27+ ...actual,
28+loadPluginManifestRegistryForInstalledIndex: (params: unknown) =>
29+loadPluginManifestRegistryForInstalledIndex(params),
30+};
31+});
32+33+const tempDirs: string[] = [];
34+35+function tempStateDir(): string {
36+const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-metadata-memo-"));
37+tempDirs.push(dir);
38+return dir;
39+}
40+41+function touchPersistedIndex(stateDir: string, value = 1): void {
42+const indexPath = path.join(stateDir, "plugins", "installs.json");
43+fs.mkdirSync(path.dirname(indexPath), { recursive: true });
44+fs.writeFileSync(indexPath, JSON.stringify({ value }));
45+}
46+47+function makeIndex(pluginId = "demo"): InstalledPluginIndex {
48+return {
49+version: 1,
50+hostContractVersion: "test",
51+compatRegistryVersion: "test",
52+migrationVersion: 1,
53+policyHash: "test",
54+generatedAtMs: 1,
55+installRecords: {},
56+diagnostics: [],
57+plugins: [
58+{
59+ pluginId,
60+manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
61+manifestHash: `${pluginId}-manifest`,
62+rootDir: `/plugins/${pluginId}`,
63+origin: "global",
64+enabled: true,
65+startup: {
66+sidecar: false,
67+memory: false,
68+deferConfiguredChannelFullLoadUntilAfterListen: false,
69+agentHarnesses: [],
70+},
71+compat: [],
72+},
73+],
74+};
75+}
76+77+function makeManifestRegistry(pluginId = "demo"): PluginManifestRegistry {
78+const plugin: PluginManifestRecord = {
79+id: pluginId,
80+name: pluginId,
81+channels: [],
82+providers: [pluginId],
83+cliBackends: [],
84+skills: [],
85+hooks: [],
86+commandAliases: [{ name: `${pluginId}-command` }],
87+rootDir: `/plugins/${pluginId}`,
88+source: `/plugins/${pluginId}/index.js`,
89+manifestPath: `/plugins/${pluginId}/openclaw.plugin.json`,
90+origin: "global",
91+};
92+return { plugins: [plugin], diagnostics: [] };
93+}
94+95+function firstPlugin(snapshot: ReturnType<typeof loadPluginMetadataSnapshot>): PluginManifestRecord {
96+const plugin = snapshot.plugins[0];
97+if (!plugin) {
98+throw new Error("expected memo test fixture plugin");
99+}
100+return plugin;
101+}
102+103+function firstCommandAlias(
104+plugin: PluginManifestRecord,
105+): NonNullable<PluginManifestRecord["commandAliases"]>[number] {
106+const commandAlias = plugin.commandAliases?.[0];
107+if (!commandAlias) {
108+throw new Error("expected memo test fixture command alias");
109+}
110+return commandAlias;
111+}
112+113+describe("loadPluginMetadataSnapshot process memo", () => {
114+beforeEach(() => {
115+clearLoadPluginMetadataSnapshotMemo();
116+loadPluginRegistrySnapshotWithMetadata.mockReset();
117+loadPluginManifestRegistryForInstalledIndex.mockReset();
118+loadPluginManifestRegistryForInstalledIndex.mockReturnValue(makeManifestRegistry());
119+});
120+121+afterEach(() => {
122+clearLoadPluginMetadataSnapshotMemo();
123+for (const dir of tempDirs.splice(0)) {
124+fs.rmSync(dir, { recursive: true, force: true });
125+}
126+});
127+128+it("reuses persisted metadata snapshots for repeated process lookups", () => {
129+const stateDir = tempStateDir();
130+touchPersistedIndex(stateDir);
131+loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
132+source: "persisted",
133+snapshot: makeIndex(),
134+diagnostics: [],
135+});
136+137+const first = loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
138+const firstRecord = firstPlugin(first);
139+firstRecord.providers.push("first-mutated");
140+firstCommandAlias(firstRecord).name = "first-command-mutated";
141+const second = loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
142+const secondRecord = firstPlugin(second);
143+secondRecord.providers.push("second-mutated");
144+firstCommandAlias(secondRecord).name = "second-command-mutated";
145+const third = loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
146+147+expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledOnce();
148+expect(loadPluginManifestRegistryForInstalledIndex).toHaveBeenCalledOnce();
149+expect(third.plugins[0]?.providers).toEqual(["demo"]);
150+expect(third.plugins[0]?.commandAliases?.[0]?.name).toBe("demo-command");
151+expect(second.manifestRegistry.plugins[0]).toBe(second.plugins[0]);
152+expect(second.byPluginId.get("demo")).toBe(second.plugins[0]);
153+});
154+155+it("memoizes policy-stale derived snapshots used by validation callers", () => {
156+const stateDir = tempStateDir();
157+touchPersistedIndex(stateDir);
158+loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
159+source: "derived",
160+snapshot: makeIndex(),
161+diagnostics: [
162+{
163+level: "warn",
164+code: "persisted-registry-stale-policy",
165+message: "policy changed",
166+},
167+],
168+});
169+170+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
171+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
172+173+expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledOnce();
174+});
175+176+it.each([
177+["persisted-registry-missing", undefined],
178+["persisted-registry-stale-source", undefined],
179+["persisted-registry-disabled", undefined],
180+[undefined, { preferPersisted: false }],
181+])("does not memoize derived snapshots for %s diagnostics", (code, options) => {
182+const stateDir = tempStateDir();
183+touchPersistedIndex(stateDir);
184+loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
185+source: "derived",
186+snapshot: makeIndex(),
187+diagnostics: code ? [{ level: "warn", code, message: "registry not reusable" }] : [],
188+});
189+190+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir, ...options });
191+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir, ...options });
192+193+expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
194+});
195+196+it("refreshes when the persisted registry file changes", () => {
197+const stateDir = tempStateDir();
198+touchPersistedIndex(stateDir, 1);
199+loadPluginRegistrySnapshotWithMetadata.mockReturnValue({
200+source: "persisted",
201+snapshot: makeIndex(),
202+diagnostics: [],
203+});
204+205+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
206+touchPersistedIndex(stateDir, 22);
207+loadPluginMetadataSnapshot({ config: {}, env: {}, stateDir });
208+209+expect(loadPluginRegistrySnapshotWithMetadata).toHaveBeenCalledTimes(2);
210+});
211+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。