mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-19 17:11:42 -06:00
767e8280ac
* 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
236 lines
7.8 KiB
TypeScript
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([]);
|
|
});
|
|
});
|