Files
openclaw/src/cli/plugins-location-bridges.test.ts
T
Vincent Koc 767e8280ac fix(cli): harden official plugin recovery (#93325)
* fix(cli): harden official plugin recovery

* fix(config): preserve include write context

* fix(config): reject external include mutations

* fix(config): bind snapshots to config paths

* fix(config): preserve write ownership

* fix(cli): preflight plugin config mutations

* chore(plugin-sdk): refresh api baseline

* test(config): prove install env policy mutations

* fix(cli): preflight plugin updates

* fix(cli): preflight non-npm id migrations

* chore(plugin-sdk): refresh api baseline

* fix(cli): satisfy plugin recovery checks
2026-06-15 23:07:29 +08:00

236 lines
7.8 KiB
TypeScript

// Plugin location bridge tests cover CLI plugin path bridging between install surfaces.
import { beforeEach, describe, expect, it, vi } from "vitest";
import type { InstalledPluginIndex } from "../plugins/installed-plugin-index.js";
import type { InstalledPluginStartupInfo } from "../plugins/installed-plugin-index.js";
import type { PluginManifestRegistry } from "../plugins/manifest-registry.js";
const readPersistedInstalledPluginIndexMock = vi.fn();
const loadPluginManifestRegistryForInstalledIndexMock = vi.fn();
const startupInfo: InstalledPluginStartupInfo = {
sidecar: false,
memory: false,
deferConfiguredChannelFullLoadUntilAfterListen: false,
agentHarnesses: [],
};
vi.mock("../plugins/installed-plugin-index-store.js", () => ({
readPersistedInstalledPluginIndex: (...args: unknown[]) =>
readPersistedInstalledPluginIndexMock(...args),
}));
vi.mock("../plugins/manifest-registry-installed.js", () => ({
loadPluginManifestRegistryForInstalledIndex: (...args: unknown[]) =>
loadPluginManifestRegistryForInstalledIndexMock(...args),
}));
const { listPersistedBundledPluginLocationBridges, listPersistedBundledPluginRecoveryLocations } =
await import("./plugins-location-bridges.js");
function makeIndex(record: InstalledPluginIndex["plugins"][number]): InstalledPluginIndex {
return {
version: 1,
hostContractVersion: "2026.5.2",
compatRegistryVersion: "test",
migrationVersion: 1,
policyHash: "test",
generatedAtMs: 1,
refreshReason: "manual",
installRecords: {},
plugins: [record],
diagnostics: [],
};
}
function makeRegistry(pluginId: string): PluginManifestRegistry {
return {
plugins: [
{
id: pluginId,
name: pluginId,
rootDir: `/app/dist/extensions/${pluginId}`,
source: `/app/dist/extensions/${pluginId}/index.js`,
origin: "bundled",
channels: [pluginId],
providers: [],
cliBackends: [],
syntheticAuthRefs: [],
nonSecretAuthMarkers: [],
skills: [],
settingsFiles: [],
hooks: [],
configContracts: [],
activation: {},
startup: {},
packageInstall: {
clawhubSpec: `clawhub:@openclaw/${pluginId}`,
npmSpec: `@openclaw/${pluginId}`,
defaultChoice: "clawhub",
},
},
],
diagnostics: [],
} as unknown as PluginManifestRegistry;
}
describe("listPersistedBundledPluginLocationBridges", () => {
beforeEach(() => {
readPersistedInstalledPluginIndexMock.mockReset();
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
});
it("keeps persisted bundled relocations npm-first for launch", async () => {
readPersistedInstalledPluginIndexMock.mockResolvedValue(
makeIndex({
pluginId: "diagnostics-otel",
manifestPath: "/app/dist/extensions/diagnostics-otel/openclaw.plugin.json",
manifestHash: "hash",
source: "/app/dist/extensions/diagnostics-otel/index.js",
rootDir: "/app/dist/extensions/diagnostics-otel",
origin: "bundled",
enabled: true,
startup: startupInfo,
compat: [],
packageInstall: {
defaultChoice: "clawhub",
clawhub: {
spec: "clawhub:@openclaw/diagnostics-otel",
packageName: "@openclaw/diagnostics-otel",
exactVersion: false,
},
npm: {
spec: "@openclaw/diagnostics-otel",
packageName: "@openclaw/diagnostics-otel",
selectorKind: "none",
exactVersion: false,
pinState: "floating-without-integrity",
},
warnings: [],
},
}),
);
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue(
makeRegistry("diagnostics-otel"),
);
await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([
{
bundledPluginId: "diagnostics-otel",
pluginId: "diagnostics-otel",
preferredSource: "npm",
npmSpec: "@openclaw/diagnostics-otel",
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
channelIds: ["diagnostics-otel"],
},
]);
});
it("uses official external catalog metadata when the persisted bundled row lacks npm metadata", async () => {
readPersistedInstalledPluginIndexMock.mockResolvedValue(
makeIndex({
pluginId: "diagnostics-otel",
manifestPath: "/app/dist/extensions/diagnostics-otel/openclaw.plugin.json",
manifestHash: "hash",
source: "/app/dist/extensions/diagnostics-otel/index.js",
rootDir: "/app/dist/extensions/diagnostics-otel",
origin: "bundled",
enabled: true,
startup: startupInfo,
compat: [],
packageInstall: {
defaultChoice: "clawhub",
clawhub: {
spec: "clawhub:@openclaw/diagnostics-otel",
packageName: "@openclaw/diagnostics-otel",
exactVersion: false,
},
warnings: [],
},
}),
);
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue(
makeRegistry("diagnostics-otel"),
);
await expect(listPersistedBundledPluginLocationBridges({})).resolves.toEqual([
{
bundledPluginId: "diagnostics-otel",
pluginId: "diagnostics-otel",
preferredSource: "npm",
npmSpec: "@openclaw/diagnostics-otel",
clawhubSpec: "clawhub:@openclaw/diagnostics-otel",
channelIds: ["diagnostics-otel"],
},
]);
});
it("does not create a relocation bridge without persisted or official install metadata", async () => {
readPersistedInstalledPluginIndexMock.mockResolvedValue(
makeIndex({
pluginId: "local-only",
manifestPath: "/app/dist/extensions/local-only/openclaw.plugin.json",
manifestHash: "hash",
source: "/app/dist/extensions/local-only/index.js",
rootDir: "/app/dist/extensions/local-only",
origin: "bundled",
enabled: true,
startup: startupInfo,
compat: [],
packageInstall: {
warnings: [],
},
}),
);
loadPluginManifestRegistryForInstalledIndexMock.mockReturnValue(makeRegistry("local-only"));
await expect(listPersistedBundledPluginLocationBridges({})).resolves.toStrictEqual([]);
});
});
describe("listPersistedBundledPluginRecoveryLocations", () => {
beforeEach(() => {
readPersistedInstalledPluginIndexMock.mockReset();
loadPluginManifestRegistryForInstalledIndexMock.mockReset();
});
it("includes exact packaged and legacy paths for disabled bundled records", async () => {
readPersistedInstalledPluginIndexMock.mockResolvedValue(
makeIndex({
pluginId: "diagnostics-otel",
manifestPath: "/app/dist/extensions/diagnostics-otel/openclaw.plugin.json",
manifestHash: "hash",
source: "/app/dist/extensions/diagnostics-otel/index.js",
rootDir: "/app/dist/extensions/diagnostics-otel",
origin: "bundled",
enabled: false,
startup: startupInfo,
compat: [],
}),
);
await expect(listPersistedBundledPluginRecoveryLocations({})).resolves.toEqual([
{
pluginId: "diagnostics-otel",
loadPaths: ["/app/dist/extensions/diagnostics-otel", "/app/extensions/diagnostics-otel"],
},
]);
});
it("does not use a relative persisted bundled root as ownership proof", async () => {
readPersistedInstalledPluginIndexMock.mockResolvedValue(
makeIndex({
pluginId: "diagnostics-otel",
manifestPath: "extensions/diagnostics-otel/openclaw.plugin.json",
manifestHash: "hash",
source: "extensions/diagnostics-otel/index.js",
rootDir: "extensions/diagnostics-otel",
origin: "bundled",
enabled: true,
startup: startupInfo,
compat: [],
}),
);
await expect(listPersistedBundledPluginRecoveryLocations({})).resolves.toStrictEqual([]);
});
});