






















@@ -0,0 +1,172 @@
1+import fs from "node:fs";
2+import path from "node:path";
3+import { afterEach, describe, expect, it, vi } from "vitest";
4+import type { PluginCandidate } from "../../../plugins/discovery.js";
5+import { readPersistedInstalledPluginIndex } from "../../../plugins/installed-plugin-index-store.js";
6+import {
7+cleanupTrackedTempDirs,
8+makeTrackedTempDir,
9+} from "../../../plugins/test-helpers/fs-fixtures.js";
10+import {
11+FORCE_PLUGIN_REGISTRY_MIGRATION_ENV,
12+migratePluginRegistryForInstall,
13+preflightPluginRegistryInstallMigration,
14+} from "./plugin-registry-migration.js";
15+16+const tempDirs: string[] = [];
17+18+afterEach(() => {
19+cleanupTrackedTempDirs(tempDirs);
20+});
21+22+function makeTempDir() {
23+return makeTrackedTempDir("openclaw-plugin-registry-migration", tempDirs);
24+}
25+26+function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
27+return {
28+OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
29+OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
30+OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
31+OPENCLAW_VERSION: "2026.4.25",
32+VITEST: "true",
33+ ...overrides,
34+};
35+}
36+37+function createCandidate(rootDir: string): PluginCandidate {
38+fs.writeFileSync(
39+path.join(rootDir, "index.ts"),
40+"throw new Error('runtime entry should not load while migrating plugin registry');\n",
41+"utf8",
42+);
43+fs.writeFileSync(
44+path.join(rootDir, "openclaw.plugin.json"),
45+JSON.stringify({
46+id: "demo",
47+name: "Demo",
48+configSchema: { type: "object" },
49+providers: ["demo"],
50+}),
51+"utf8",
52+);
53+return {
54+idHint: "demo",
55+source: path.join(rootDir, "index.ts"),
56+ rootDir,
57+origin: "global",
58+};
59+}
60+61+describe("plugin registry install migration", () => {
62+it("short-circuits when a registry file already exists", async () => {
63+const stateDir = makeTempDir();
64+const filePath = path.join(stateDir, "plugins", "installed-index.json");
65+fs.mkdirSync(path.dirname(filePath), { recursive: true });
66+fs.writeFileSync(filePath, "{}\n", "utf8");
67+const readConfig = vi.fn(async () => ({}));
68+69+await expect(
70+migratePluginRegistryForInstall({
71+ stateDir,
72+ readConfig,
73+env: hermeticEnv(),
74+}),
75+).resolves.toMatchObject({
76+status: "skip-existing",
77+migrated: false,
78+preflight: {
79+action: "skip-existing",
80+ filePath,
81+},
82+});
83+expect(readConfig).not.toHaveBeenCalled();
84+});
85+86+it("supports dry-run preflight without reading config or writing the registry", async () => {
87+const stateDir = makeTempDir();
88+const readConfig = vi.fn(async () => ({}));
89+90+await expect(
91+migratePluginRegistryForInstall({
92+ stateDir,
93+dryRun: true,
94+ readConfig,
95+env: hermeticEnv(),
96+}),
97+).resolves.toMatchObject({
98+status: "dry-run",
99+migrated: false,
100+preflight: {
101+action: "migrate",
102+},
103+});
104+expect(readConfig).not.toHaveBeenCalled();
105+expect(fs.existsSync(path.join(stateDir, "plugins", "installed-index.json"))).toBe(false);
106+});
107+108+it("migrates missing registry state from legacy discovery and config inputs", async () => {
109+const stateDir = makeTempDir();
110+const pluginDir = path.join(stateDir, "plugins", "demo");
111+fs.mkdirSync(pluginDir, { recursive: true });
112+const candidate = createCandidate(pluginDir);
113+114+await expect(
115+migratePluginRegistryForInstall({
116+ stateDir,
117+candidates: [candidate],
118+readConfig: async () => ({
119+plugins: {
120+installs: {
121+demo: {
122+source: "npm",
123+resolvedName: "@vendor/demo",
124+resolvedVersion: "1.0.0",
125+},
126+},
127+},
128+}),
129+env: hermeticEnv(),
130+}),
131+).resolves.toMatchObject({
132+status: "migrated",
133+migrated: true,
134+current: {
135+refreshReason: "migration",
136+migrationVersion: 1,
137+plugins: [
138+expect.objectContaining({
139+pluginId: "demo",
140+installRecord: expect.objectContaining({
141+source: "npm",
142+resolvedName: "@vendor/demo",
143+resolvedVersion: "1.0.0",
144+}),
145+}),
146+],
147+},
148+});
149+150+await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
151+refreshReason: "migration",
152+plugins: [expect.objectContaining({ pluginId: "demo" })],
153+});
154+});
155+156+it("marks force migration env as deprecated break-glass", () => {
157+expect(
158+preflightPluginRegistryInstallMigration({
159+stateDir: makeTempDir(),
160+env: hermeticEnv({
161+[FORCE_PLUGIN_REGISTRY_MIGRATION_ENV]: "1",
162+}),
163+}),
164+).toMatchObject({
165+action: "migrate",
166+force: true,
167+deprecationWarnings: [
168+expect.stringContaining(`${FORCE_PLUGIN_REGISTRY_MIGRATION_ENV} is deprecated`),
169+],
170+});
171+});
172+});
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。