mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 06:20:43 +00:00
fix(plugins): preflight registry install migration
This commit is contained in:
@@ -32,7 +32,6 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||
const DEFAULT_EXTENSIONS_DIR = join(__dirname, "..", "dist", "extensions");
|
||||
const DEFAULT_PACKAGE_ROOT = join(__dirname, "..");
|
||||
const DISABLE_POSTINSTALL_ENV = "OPENCLAW_DISABLE_BUNDLED_PLUGIN_POSTINSTALL";
|
||||
const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
|
||||
const EAGER_BUNDLED_PLUGIN_DEPS_ENV = "OPENCLAW_EAGER_BUNDLED_PLUGIN_DEPS";
|
||||
const DIST_INVENTORY_PATH = "dist/postinstall-inventory.json";
|
||||
const LEGACY_QA_CHANNEL_DIR = ["qa", "channel"].join("-");
|
||||
@@ -662,45 +661,34 @@ async function importInstalledDistModule(params, distPath) {
|
||||
}
|
||||
|
||||
export async function runPluginRegistryPostinstallMigration(params = {}) {
|
||||
const env = params.env ?? process.env;
|
||||
const log = params.log ?? console;
|
||||
const packageRoot = params.packageRoot ?? DEFAULT_PACKAGE_ROOT;
|
||||
if (env?.[DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV]?.trim()) {
|
||||
return { status: "disabled" };
|
||||
}
|
||||
|
||||
try {
|
||||
const [configModule, registryModule] = await Promise.all([
|
||||
importInstalledDistModule(params, "dist/config/config.js"),
|
||||
importInstalledDistModule(params, "dist/plugins/plugin-registry.js"),
|
||||
]);
|
||||
if (!configModule || !registryModule) {
|
||||
const migrationModule = await importInstalledDistModule(
|
||||
params,
|
||||
"dist/commands/doctor/shared/plugin-registry-migration.js",
|
||||
);
|
||||
if (!migrationModule) {
|
||||
return { status: "skipped", reason: "missing-dist-entry" };
|
||||
}
|
||||
const readConfig =
|
||||
typeof configModule.readBestEffortConfig === "function"
|
||||
? configModule.readBestEffortConfig
|
||||
: configModule.loadConfig;
|
||||
if (
|
||||
typeof readConfig !== "function" ||
|
||||
typeof registryModule.ensurePluginRegistryMigrated !== "function"
|
||||
) {
|
||||
if (typeof migrationModule.migratePluginRegistryForInstall !== "function") {
|
||||
return { status: "skipped", reason: "missing-dist-contract" };
|
||||
}
|
||||
|
||||
const config = await readConfig();
|
||||
const inspection = await registryModule.ensurePluginRegistryMigrated({
|
||||
config,
|
||||
env,
|
||||
const result = await migrationModule.migratePluginRegistryForInstall({
|
||||
env: params.env ?? process.env,
|
||||
packageRoot,
|
||||
});
|
||||
if (inspection.migrated) {
|
||||
log.log(
|
||||
`[postinstall] migrated plugin registry: ${inspection.current.plugins.length} plugin(s) indexed`,
|
||||
);
|
||||
return { status: "migrated", inspection };
|
||||
for (const warning of result.preflight?.deprecationWarnings ?? []) {
|
||||
log.warn(`[postinstall] ${warning}`);
|
||||
}
|
||||
return { status: "fresh", inspection };
|
||||
if (result.migrated) {
|
||||
log.log(
|
||||
`[postinstall] migrated plugin registry: ${result.current.plugins.length} plugin(s) indexed`,
|
||||
);
|
||||
}
|
||||
return result;
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
log.warn(`[postinstall] could not migrate plugin registry: ${message}`);
|
||||
|
||||
172
src/commands/doctor/shared/plugin-registry-migration.test.ts
Normal file
172
src/commands/doctor/shared/plugin-registry-migration.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
import type { PluginCandidate } from "../../../plugins/discovery.js";
|
||||
import { readPersistedInstalledPluginIndex } from "../../../plugins/installed-plugin-index-store.js";
|
||||
import {
|
||||
cleanupTrackedTempDirs,
|
||||
makeTrackedTempDir,
|
||||
} from "../../../plugins/test-helpers/fs-fixtures.js";
|
||||
import {
|
||||
FORCE_PLUGIN_REGISTRY_MIGRATION_ENV,
|
||||
migratePluginRegistryForInstall,
|
||||
preflightPluginRegistryInstallMigration,
|
||||
} from "./plugin-registry-migration.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
afterEach(() => {
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
function makeTempDir() {
|
||||
return makeTrackedTempDir("openclaw-plugin-registry-migration", tempDirs);
|
||||
}
|
||||
|
||||
function hermeticEnv(overrides: NodeJS.ProcessEnv = {}): NodeJS.ProcessEnv {
|
||||
return {
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
function createCandidate(rootDir: string): PluginCandidate {
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "index.ts"),
|
||||
"throw new Error('runtime entry should not load while migrating plugin registry');\n",
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
id: "demo",
|
||||
name: "Demo",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["demo"],
|
||||
}),
|
||||
"utf8",
|
||||
);
|
||||
return {
|
||||
idHint: "demo",
|
||||
source: path.join(rootDir, "index.ts"),
|
||||
rootDir,
|
||||
origin: "global",
|
||||
};
|
||||
}
|
||||
|
||||
describe("plugin registry install migration", () => {
|
||||
it("short-circuits when a registry file already exists", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const filePath = path.join(stateDir, "plugins", "installed-index.json");
|
||||
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
||||
fs.writeFileSync(filePath, "{}\n", "utf8");
|
||||
const readConfig = vi.fn(async () => ({}));
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
readConfig,
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "skip-existing",
|
||||
migrated: false,
|
||||
preflight: {
|
||||
action: "skip-existing",
|
||||
filePath,
|
||||
},
|
||||
});
|
||||
expect(readConfig).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("supports dry-run preflight without reading config or writing the registry", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const readConfig = vi.fn(async () => ({}));
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
dryRun: true,
|
||||
readConfig,
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "dry-run",
|
||||
migrated: false,
|
||||
preflight: {
|
||||
action: "migrate",
|
||||
},
|
||||
});
|
||||
expect(readConfig).not.toHaveBeenCalled();
|
||||
expect(fs.existsSync(path.join(stateDir, "plugins", "installed-index.json"))).toBe(false);
|
||||
});
|
||||
|
||||
it("migrates missing registry state from legacy discovery and config inputs", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const candidate = createCandidate(pluginDir);
|
||||
|
||||
await expect(
|
||||
migratePluginRegistryForInstall({
|
||||
stateDir,
|
||||
candidates: [candidate],
|
||||
readConfig: async () => ({
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
env: hermeticEnv(),
|
||||
}),
|
||||
).resolves.toMatchObject({
|
||||
status: "migrated",
|
||||
migrated: true,
|
||||
current: {
|
||||
refreshReason: "migration",
|
||||
migrationVersion: 1,
|
||||
plugins: [
|
||||
expect.objectContaining({
|
||||
pluginId: "demo",
|
||||
installRecord: expect.objectContaining({
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
}),
|
||||
}),
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(readPersistedInstalledPluginIndex({ stateDir })).resolves.toMatchObject({
|
||||
refreshReason: "migration",
|
||||
plugins: [expect.objectContaining({ pluginId: "demo" })],
|
||||
});
|
||||
});
|
||||
|
||||
it("marks force migration env as deprecated break-glass", () => {
|
||||
expect(
|
||||
preflightPluginRegistryInstallMigration({
|
||||
stateDir: makeTempDir(),
|
||||
env: hermeticEnv({
|
||||
[FORCE_PLUGIN_REGISTRY_MIGRATION_ENV]: "1",
|
||||
}),
|
||||
}),
|
||||
).toMatchObject({
|
||||
action: "migrate",
|
||||
force: true,
|
||||
deprecationWarnings: [
|
||||
expect.stringContaining(`${FORCE_PLUGIN_REGISTRY_MIGRATION_ENV} is deprecated`),
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
135
src/commands/doctor/shared/plugin-registry-migration.ts
Normal file
135
src/commands/doctor/shared/plugin-registry-migration.ts
Normal file
@@ -0,0 +1,135 @@
|
||||
import fs from "node:fs";
|
||||
import type { OpenClawConfig } from "../../../config/types.openclaw.js";
|
||||
import {
|
||||
inspectPersistedInstalledPluginIndex,
|
||||
refreshPersistedInstalledPluginIndex,
|
||||
resolveInstalledPluginIndexStorePath,
|
||||
type InstalledPluginIndexStoreInspection,
|
||||
type InstalledPluginIndexStoreOptions,
|
||||
} from "../../../plugins/installed-plugin-index-store.js";
|
||||
import type {
|
||||
InstalledPluginIndex,
|
||||
LoadInstalledPluginIndexParams,
|
||||
} from "../../../plugins/installed-plugin-index.js";
|
||||
|
||||
export const DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION";
|
||||
export const FORCE_PLUGIN_REGISTRY_MIGRATION_ENV = "OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION";
|
||||
|
||||
export type PluginRegistryInstallMigrationPreflightAction =
|
||||
| "disabled"
|
||||
| "skip-existing"
|
||||
| "migrate";
|
||||
|
||||
export type PluginRegistryInstallMigrationPreflight = {
|
||||
action: PluginRegistryInstallMigrationPreflightAction;
|
||||
filePath: string;
|
||||
force: boolean;
|
||||
deprecationWarnings: readonly string[];
|
||||
};
|
||||
|
||||
export type PluginRegistryInstallMigrationResult =
|
||||
| {
|
||||
status: "disabled" | "skip-existing" | "dry-run";
|
||||
migrated: false;
|
||||
preflight: PluginRegistryInstallMigrationPreflight;
|
||||
}
|
||||
| {
|
||||
status: "migrated";
|
||||
migrated: true;
|
||||
preflight: PluginRegistryInstallMigrationPreflight;
|
||||
inspection: InstalledPluginIndexStoreInspection;
|
||||
current: InstalledPluginIndex;
|
||||
};
|
||||
|
||||
export type PluginRegistryInstallMigrationParams = LoadInstalledPluginIndexParams &
|
||||
InstalledPluginIndexStoreOptions & {
|
||||
dryRun?: boolean;
|
||||
existsSync?: (path: string) => boolean;
|
||||
readConfig?: () => Promise<OpenClawConfig> | OpenClawConfig;
|
||||
};
|
||||
|
||||
function hasEnvFlag(env: NodeJS.ProcessEnv | undefined, key: string): boolean {
|
||||
return Boolean(env?.[key]?.trim());
|
||||
}
|
||||
|
||||
function forceDeprecationWarning(): string {
|
||||
return `${FORCE_PLUGIN_REGISTRY_MIGRATION_ENV} is deprecated and will be removed after the plugin registry migration rollout; use doctor registry repair once available.`;
|
||||
}
|
||||
|
||||
export function preflightPluginRegistryInstallMigration(
|
||||
params: PluginRegistryInstallMigrationParams = {},
|
||||
): PluginRegistryInstallMigrationPreflight {
|
||||
const env = params.env ?? process.env;
|
||||
const filePath = resolveInstalledPluginIndexStorePath(params);
|
||||
const force = hasEnvFlag(env, FORCE_PLUGIN_REGISTRY_MIGRATION_ENV);
|
||||
const deprecationWarnings = force ? [forceDeprecationWarning()] : [];
|
||||
if (hasEnvFlag(env, DISABLE_PLUGIN_REGISTRY_MIGRATION_ENV)) {
|
||||
return {
|
||||
action: "disabled",
|
||||
filePath,
|
||||
force,
|
||||
deprecationWarnings,
|
||||
};
|
||||
}
|
||||
const pathExists = params.existsSync ?? fs.existsSync;
|
||||
if (!force && pathExists(filePath)) {
|
||||
return {
|
||||
action: "skip-existing",
|
||||
filePath,
|
||||
force,
|
||||
deprecationWarnings,
|
||||
};
|
||||
}
|
||||
return {
|
||||
action: "migrate",
|
||||
filePath,
|
||||
force,
|
||||
deprecationWarnings,
|
||||
};
|
||||
}
|
||||
|
||||
async function readMigrationConfig(
|
||||
params: PluginRegistryInstallMigrationParams,
|
||||
): Promise<OpenClawConfig> {
|
||||
if (params.config) {
|
||||
return params.config;
|
||||
}
|
||||
if (params.readConfig) {
|
||||
return await params.readConfig();
|
||||
}
|
||||
const configModule = await import("../../../config/config.js");
|
||||
return await configModule.readBestEffortConfig();
|
||||
}
|
||||
|
||||
export async function migratePluginRegistryForInstall(
|
||||
params: PluginRegistryInstallMigrationParams = {},
|
||||
): Promise<PluginRegistryInstallMigrationResult> {
|
||||
const preflight = preflightPluginRegistryInstallMigration(params);
|
||||
if (preflight.action === "disabled") {
|
||||
return { status: "disabled", migrated: false, preflight };
|
||||
}
|
||||
if (preflight.action === "skip-existing") {
|
||||
return { status: "skip-existing", migrated: false, preflight };
|
||||
}
|
||||
if (params.dryRun) {
|
||||
return { status: "dry-run", migrated: false, preflight };
|
||||
}
|
||||
|
||||
const config = await readMigrationConfig(params);
|
||||
const migrationParams = {
|
||||
...params,
|
||||
config,
|
||||
};
|
||||
const inspection = await inspectPersistedInstalledPluginIndex(migrationParams);
|
||||
const current = await refreshPersistedInstalledPluginIndex({
|
||||
...migrationParams,
|
||||
reason: "migration",
|
||||
});
|
||||
return {
|
||||
status: "migrated",
|
||||
migrated: true,
|
||||
preflight,
|
||||
inspection,
|
||||
current,
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,6 @@ import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { PluginCandidate } from "./discovery.js";
|
||||
import {
|
||||
getPluginRecord,
|
||||
ensurePluginRegistryMigrated,
|
||||
inspectPluginRegistry,
|
||||
isPluginEnabled,
|
||||
listPluginContributionIds,
|
||||
@@ -187,36 +186,4 @@ describe("plugin registry facade", () => {
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("migrates missing persisted registry state from legacy discovery inputs", async () => {
|
||||
const stateDir = makeTempDir();
|
||||
const pluginDir = path.join(stateDir, "plugins", "demo");
|
||||
fs.mkdirSync(pluginDir, { recursive: true });
|
||||
const candidate = createCandidate(pluginDir);
|
||||
const env = hermeticEnv();
|
||||
|
||||
await expect(
|
||||
ensurePluginRegistryMigrated({ stateDir, candidates: [candidate], env }),
|
||||
).resolves.toMatchObject({
|
||||
state: "missing",
|
||||
refreshReasons: ["missing"],
|
||||
migrated: true,
|
||||
current: {
|
||||
refreshReason: "migration",
|
||||
migrationVersion: 1,
|
||||
plugins: [expect.objectContaining({ pluginId: "demo", enabled: true })],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
inspectPluginRegistry({ stateDir, candidates: [candidate], env }),
|
||||
).resolves.toMatchObject({
|
||||
state: "fresh",
|
||||
refreshReasons: [],
|
||||
persisted: {
|
||||
refreshReason: "migration",
|
||||
plugins: [expect.objectContaining({ pluginId: "demo" })],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -20,9 +20,6 @@ import {
|
||||
export type PluginRegistrySnapshot = InstalledPluginIndex;
|
||||
export type PluginRegistryRecord = InstalledPluginIndexRecord;
|
||||
export type PluginRegistryInspection = InstalledPluginIndexStoreInspection;
|
||||
export type PluginRegistryMigrationInspection = PluginRegistryInspection & {
|
||||
migrated: boolean;
|
||||
};
|
||||
|
||||
export type LoadPluginRegistryParams = LoadInstalledPluginIndexParams & {
|
||||
index?: PluginRegistrySnapshot;
|
||||
@@ -177,31 +174,3 @@ export function refreshPluginRegistry(
|
||||
store.refreshPersistedInstalledPluginIndex(params),
|
||||
);
|
||||
}
|
||||
|
||||
function resolveMigrationRefreshReason(
|
||||
reasons: readonly RefreshInstalledPluginIndexParams["reason"][],
|
||||
): RefreshInstalledPluginIndexParams["reason"] {
|
||||
return reasons.includes("missing") || reasons.includes("migration") ? "migration" : "manual";
|
||||
}
|
||||
|
||||
export async function ensurePluginRegistryMigrated(
|
||||
params: LoadInstalledPluginIndexParams & InstalledPluginIndexStoreOptions = {},
|
||||
): Promise<PluginRegistryMigrationInspection> {
|
||||
const store = await import("./installed-plugin-index-store.js");
|
||||
const inspection = await store.inspectPersistedInstalledPluginIndex(params);
|
||||
if (inspection.state === "fresh") {
|
||||
return {
|
||||
...inspection,
|
||||
migrated: false,
|
||||
};
|
||||
}
|
||||
const current = await store.refreshPersistedInstalledPluginIndex({
|
||||
...params,
|
||||
reason: resolveMigrationRefreshReason(inspection.refreshReasons),
|
||||
});
|
||||
return {
|
||||
...inspection,
|
||||
current,
|
||||
migrated: true,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -251,39 +251,29 @@ describe("bundled plugin postinstall", () => {
|
||||
it("migrates the plugin registry during postinstall from built dist contracts", async () => {
|
||||
const packageRoot = await createTempDirAsync("openclaw-postinstall-registry-");
|
||||
const log = { log: vi.fn(), warn: vi.fn() };
|
||||
const readBestEffortConfig = vi.fn(async () => ({
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}));
|
||||
const ensurePluginRegistryMigrated = vi.fn(async () => ({
|
||||
const migratePluginRegistryForInstall = vi.fn(async () => ({
|
||||
status: "migrated",
|
||||
migrated: true,
|
||||
preflight: {
|
||||
deprecationWarnings: [],
|
||||
},
|
||||
current: {
|
||||
plugins: [{ pluginId: "demo" }],
|
||||
},
|
||||
}));
|
||||
const importModule = vi.fn(async (specifier: string) => {
|
||||
if (specifier.endsWith("/dist/config/config.js")) {
|
||||
return { readBestEffortConfig };
|
||||
}
|
||||
if (specifier.endsWith("/dist/plugins/plugin-registry.js")) {
|
||||
return { ensurePluginRegistryMigrated };
|
||||
if (specifier.endsWith("/dist/commands/doctor/shared/plugin-registry-migration.js")) {
|
||||
return { migratePluginRegistryForInstall };
|
||||
}
|
||||
throw new Error(`unexpected import: ${specifier}`);
|
||||
});
|
||||
|
||||
const result = await runPluginRegistryPostinstallMigration({
|
||||
packageRoot,
|
||||
existsSync: vi.fn(
|
||||
(filePath: string) =>
|
||||
filePath.endsWith(path.join("dist", "config", "config.js")) ||
|
||||
filePath.endsWith(path.join("dist", "plugins", "plugin-registry.js")),
|
||||
existsSync: vi.fn((filePath: string) =>
|
||||
filePath.endsWith(
|
||||
path.join("dist", "commands", "doctor", "shared", "plugin-registry-migration.js"),
|
||||
),
|
||||
),
|
||||
importModule,
|
||||
env: { OPENCLAW_HOME: "/tmp/home" },
|
||||
@@ -291,19 +281,7 @@ describe("bundled plugin postinstall", () => {
|
||||
});
|
||||
|
||||
expect(result).toMatchObject({ status: "migrated" });
|
||||
expect(readBestEffortConfig).toHaveBeenCalled();
|
||||
expect(ensurePluginRegistryMigrated).toHaveBeenCalledWith({
|
||||
config: {
|
||||
plugins: {
|
||||
installs: {
|
||||
demo: {
|
||||
source: "npm",
|
||||
resolvedName: "@vendor/demo",
|
||||
resolvedVersion: "1.0.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expect(migratePluginRegistryForInstall).toHaveBeenCalledWith({
|
||||
env: { OPENCLAW_HOME: "/tmp/home" },
|
||||
packageRoot,
|
||||
});
|
||||
@@ -312,6 +290,29 @@ describe("bundled plugin postinstall", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("surfaces deprecated plugin registry migration break-glass warnings", async () => {
|
||||
const warn = vi.fn();
|
||||
const migratePluginRegistryForInstall = vi.fn(async () => ({
|
||||
status: "skip-existing",
|
||||
migrated: false,
|
||||
preflight: {
|
||||
deprecationWarnings: ["OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION is deprecated"],
|
||||
},
|
||||
}));
|
||||
const importModule = vi.fn(async () => ({ migratePluginRegistryForInstall }));
|
||||
|
||||
await runPluginRegistryPostinstallMigration({
|
||||
packageRoot: "/pkg",
|
||||
existsSync: vi.fn(() => true),
|
||||
importModule,
|
||||
log: { log: vi.fn(), warn },
|
||||
});
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"[postinstall] OPENCLAW_FORCE_PLUGIN_REGISTRY_MIGRATION is deprecated",
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps plugin registry postinstall migration non-fatal when dist entries are unavailable", async () => {
|
||||
const warn = vi.fn();
|
||||
|
||||
@@ -329,12 +330,22 @@ describe("bundled plugin postinstall", () => {
|
||||
});
|
||||
|
||||
it("honors plugin registry postinstall migration disable env", async () => {
|
||||
const migratePluginRegistryForInstall = vi.fn(async () => ({
|
||||
status: "disabled",
|
||||
migrated: false,
|
||||
preflight: {
|
||||
deprecationWarnings: [],
|
||||
},
|
||||
}));
|
||||
await expect(
|
||||
runPluginRegistryPostinstallMigration({
|
||||
packageRoot: "/pkg",
|
||||
env: { OPENCLAW_DISABLE_PLUGIN_REGISTRY_MIGRATION: "1" },
|
||||
existsSync: vi.fn(() => true),
|
||||
importModule: vi.fn(async () => ({ migratePluginRegistryForInstall })),
|
||||
log: { log: vi.fn(), warn: vi.fn() },
|
||||
}),
|
||||
).resolves.toEqual({ status: "disabled" });
|
||||
).resolves.toMatchObject({ status: "disabled" });
|
||||
});
|
||||
|
||||
it("prunes stale dist files from packaged installs", async () => {
|
||||
|
||||
@@ -214,10 +214,10 @@ function buildCoreDistEntries(): Record<string, string> {
|
||||
"agents/models-config.runtime": "src/agents/models-config.runtime.ts",
|
||||
"subagent-registry.runtime": "src/agents/subagent-registry.runtime.ts",
|
||||
"agents/pi-model-discovery-runtime": "src/agents/pi-model-discovery-runtime.ts",
|
||||
"commands/doctor/shared/plugin-registry-migration":
|
||||
"src/commands/doctor/shared/plugin-registry-migration.ts",
|
||||
"commands/status.summary.runtime": "src/commands/status.summary.runtime.ts",
|
||||
"config/config": "src/config/config.ts",
|
||||
"infra/boundary-file-read": "src/infra/boundary-file-read.ts",
|
||||
"plugins/plugin-registry": "src/plugins/plugin-registry.ts",
|
||||
"plugins/provider-discovery.runtime": "src/plugins/provider-discovery.runtime.ts",
|
||||
"plugins/provider-runtime.runtime": "src/plugins/provider-runtime.runtime.ts",
|
||||
"plugins/public-surface-runtime": "src/plugins/public-surface-runtime.ts",
|
||||
|
||||
Reference in New Issue
Block a user