mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 07:10:43 +00:00
test(plugins): guard cold status snapshots
Add a reusable cold plugin fixture and status snapshot guard proving read-only plugin metadata paths do not import plugin runtime entries.
This commit is contained in:
@@ -1,11 +1,14 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { clearPluginDiscoveryCache } from "../plugins/discovery.js";
|
||||
import { clearPluginManifestRegistryCache } from "../plugins/manifest-registry.js";
|
||||
import { refreshPluginRegistry } from "../plugins/plugin-registry.js";
|
||||
import {
|
||||
createColdPluginConfig,
|
||||
createColdPluginFixture,
|
||||
createColdPluginHermeticEnv,
|
||||
isColdPluginRuntimeLoaded,
|
||||
} from "../plugins/test-helpers/cold-plugin-fixtures.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "../plugins/test-helpers/fs-fixtures.js";
|
||||
import { buildAuthChoiceOptions, formatAuthChoiceChoicesForCli } from "./auth-choice-options.js";
|
||||
import { listManifestInstalledChannelIds } from "./channel-setup/discovery.js";
|
||||
import { resolveProviderCatalogPluginIdsForFilter } from "./models/list.provider-catalog.js";
|
||||
@@ -13,111 +16,21 @@ import { resolveProviderCatalogPluginIdsForFilter } from "./models/list.provider
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-command-cold-imports-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function hermeticEnv(
|
||||
homeDir: string,
|
||||
options: { disablePersistedRegistry?: boolean } = {},
|
||||
): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
OPENCLAW_HOME: path.join(homeDir, "home"),
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: undefined,
|
||||
OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY:
|
||||
options.disablePersistedRegistry === false ? undefined : "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
};
|
||||
}
|
||||
|
||||
function createColdControlPlanePlugin() {
|
||||
const rootDir = makeTempDir();
|
||||
const runtimeMarker = path.join(rootDir, "runtime-loaded.txt");
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@example/openclaw-cold-control-plane",
|
||||
version: "1.0.0",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "cold-control-plane",
|
||||
name: "Cold Control Plane",
|
||||
configSchema: { type: "object" },
|
||||
providers: ["cold-model-provider"],
|
||||
channels: ["cold-channel"],
|
||||
channelConfigs: {
|
||||
"cold-channel": {
|
||||
schema: { type: "object" },
|
||||
},
|
||||
},
|
||||
providerAuthChoices: [
|
||||
{
|
||||
provider: "cold-model-provider",
|
||||
method: "api-key",
|
||||
choiceId: "cold-provider-api-key",
|
||||
choiceLabel: "Cold Provider API key",
|
||||
groupId: "cold-model-provider",
|
||||
groupLabel: "Cold Provider",
|
||||
optionKey: "coldProviderApiKey",
|
||||
cliFlag: "--cold-provider-api-key",
|
||||
cliOption: "--cold-provider-api-key <key>",
|
||||
onboardingScopes: ["text-inference"],
|
||||
},
|
||||
],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf8");\nthrow new Error("runtime entry should not load for command control-plane discovery");\n`,
|
||||
"utf8",
|
||||
);
|
||||
return { rootDir, runtimeMarker };
|
||||
}
|
||||
|
||||
function createColdConfig(pluginDir: string): OpenClawConfig {
|
||||
return {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
entries: {
|
||||
"cold-control-plane": { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
return makeTrackedTempDir("openclaw-command-cold-imports", tempDirs);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("command control-plane plugin discovery", () => {
|
||||
it("resolves channel setup metadata without importing plugin runtime", () => {
|
||||
const plugin = createColdControlPlanePlugin();
|
||||
const plugin = createColdPluginFixture({ rootDir: makeTempDir() });
|
||||
const workspaceDir = makeTempDir();
|
||||
const cfg = createColdConfig(plugin.rootDir);
|
||||
const env = hermeticEnv(workspaceDir);
|
||||
const cfg = createColdPluginConfig(plugin.rootDir, plugin.pluginId);
|
||||
const env = createColdPluginHermeticEnv(workspaceDir);
|
||||
|
||||
expect(
|
||||
listManifestInstalledChannelIds({
|
||||
@@ -125,15 +38,15 @@ describe("command control-plane plugin discovery", () => {
|
||||
workspaceDir,
|
||||
env,
|
||||
}),
|
||||
).toContain("cold-channel");
|
||||
expect(fs.existsSync(plugin.runtimeMarker)).toBe(false);
|
||||
).toContain(plugin.channelId);
|
||||
expect(isColdPluginRuntimeLoaded(plugin)).toBe(false);
|
||||
});
|
||||
|
||||
it("builds onboarding auth choices from manifest metadata without importing plugin runtime", () => {
|
||||
const plugin = createColdControlPlanePlugin();
|
||||
const plugin = createColdPluginFixture({ rootDir: makeTempDir() });
|
||||
const workspaceDir = makeTempDir();
|
||||
const cfg = createColdConfig(plugin.rootDir);
|
||||
const env = hermeticEnv(workspaceDir);
|
||||
const cfg = createColdPluginConfig(plugin.rootDir, plugin.pluginId);
|
||||
const env = createColdPluginHermeticEnv(workspaceDir);
|
||||
|
||||
expect(
|
||||
buildAuthChoiceOptions({
|
||||
@@ -145,9 +58,9 @@ describe("command control-plane plugin discovery", () => {
|
||||
}),
|
||||
).toContainEqual(
|
||||
expect.objectContaining({
|
||||
value: "cold-provider-api-key",
|
||||
value: plugin.authChoiceId,
|
||||
label: "Cold Provider API key",
|
||||
groupId: "cold-model-provider",
|
||||
groupId: plugin.providerId,
|
||||
}),
|
||||
);
|
||||
expect(
|
||||
@@ -156,15 +69,15 @@ describe("command control-plane plugin discovery", () => {
|
||||
workspaceDir,
|
||||
env,
|
||||
}).split("|"),
|
||||
).toContain("cold-provider-api-key");
|
||||
expect(fs.existsSync(plugin.runtimeMarker)).toBe(false);
|
||||
).toContain(plugin.authChoiceId);
|
||||
expect(isColdPluginRuntimeLoaded(plugin)).toBe(false);
|
||||
});
|
||||
|
||||
it("resolves models-list provider ownership without importing plugin runtime", async () => {
|
||||
const plugin = createColdControlPlanePlugin();
|
||||
const plugin = createColdPluginFixture({ rootDir: makeTempDir() });
|
||||
const workspaceDir = makeTempDir();
|
||||
const cfg = createColdConfig(plugin.rootDir);
|
||||
const env = hermeticEnv(workspaceDir, { disablePersistedRegistry: false });
|
||||
const cfg = createColdPluginConfig(plugin.rootDir, plugin.pluginId);
|
||||
const env = createColdPluginHermeticEnv(workspaceDir, { disablePersistedRegistry: false });
|
||||
|
||||
await refreshPluginRegistry({
|
||||
config: cfg,
|
||||
@@ -172,15 +85,15 @@ describe("command control-plane plugin discovery", () => {
|
||||
env,
|
||||
reason: "manual",
|
||||
});
|
||||
expect(fs.existsSync(plugin.runtimeMarker)).toBe(false);
|
||||
expect(isColdPluginRuntimeLoaded(plugin)).toBe(false);
|
||||
|
||||
await expect(
|
||||
resolveProviderCatalogPluginIdsForFilter({
|
||||
cfg,
|
||||
env,
|
||||
providerFilter: "cold-model-provider",
|
||||
providerFilter: plugin.providerId,
|
||||
}),
|
||||
).resolves.toEqual(["cold-control-plane"]);
|
||||
expect(fs.existsSync(plugin.runtimeMarker)).toBe(false);
|
||||
).resolves.toEqual([plugin.pluginId]);
|
||||
expect(isColdPluginRuntimeLoaded(plugin)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,43 +1,36 @@
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { clearPluginDiscoveryCache } from "./discovery.js";
|
||||
import { clearPluginManifestRegistryCache } from "./manifest-registry.js";
|
||||
import { buildPluginRegistrySnapshotReport } from "./status.js";
|
||||
import { buildPluginRegistrySnapshotReport, buildPluginSnapshotReport } from "./status.js";
|
||||
import {
|
||||
createColdPluginConfig,
|
||||
createColdPluginFixture,
|
||||
createColdPluginHermeticEnv,
|
||||
isColdPluginRuntimeLoaded,
|
||||
} from "./test-helpers/cold-plugin-fixtures.js";
|
||||
import { cleanupTrackedTempDirs, makeTrackedTempDir } from "./test-helpers/fs-fixtures.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-plugin-status-"));
|
||||
tempDirs.push(dir);
|
||||
return dir;
|
||||
return makeTrackedTempDir("openclaw-plugin-status", tempDirs);
|
||||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
for (const dir of tempDirs.splice(0)) {
|
||||
fs.rmSync(dir, { recursive: true, force: true });
|
||||
}
|
||||
cleanupTrackedTempDirs(tempDirs);
|
||||
});
|
||||
|
||||
describe("buildPluginRegistrySnapshotReport", () => {
|
||||
it("reconstructs list metadata from indexed manifests without importing plugin runtime", () => {
|
||||
const pluginDir = makeTempDir();
|
||||
const runtimeMarker = path.join(pluginDir, "runtime-loaded.txt");
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "@example/openclaw-indexed-demo",
|
||||
version: "9.8.7",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify({
|
||||
const fixture = createColdPluginFixture({
|
||||
rootDir: makeTempDir(),
|
||||
pluginId: "indexed-demo",
|
||||
packageName: "@example/openclaw-indexed-demo",
|
||||
packageVersion: "9.8.7",
|
||||
manifest: {
|
||||
id: "indexed-demo",
|
||||
name: "Indexed Demo",
|
||||
description: "Manifest-backed list metadata",
|
||||
@@ -49,19 +42,13 @@ describe("buildPluginRegistrySnapshotReport", () => {
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
},
|
||||
}),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");\nmodule.exports = { id: "indexed-demo", register() {} };\n`,
|
||||
"utf-8",
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
const report = buildPluginRegistrySnapshotReport({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
load: { paths: [fixture.rootDir] },
|
||||
},
|
||||
},
|
||||
});
|
||||
@@ -75,9 +62,45 @@ describe("buildPluginRegistrySnapshotReport", () => {
|
||||
format: "openclaw",
|
||||
providerIds: ["indexed-provider"],
|
||||
commands: ["indexed-demo"],
|
||||
source: fs.realpathSync(path.join(pluginDir, "index.cjs")),
|
||||
source: fs.realpathSync(fixture.runtimeSource),
|
||||
status: "loaded",
|
||||
});
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(false);
|
||||
expect(isColdPluginRuntimeLoaded(fixture)).toBe(false);
|
||||
});
|
||||
|
||||
it("builds read-only plugin status snapshots without importing plugin runtime", () => {
|
||||
const fixture = createColdPluginFixture({
|
||||
rootDir: makeTempDir(),
|
||||
pluginId: "snapshot-demo",
|
||||
manifest: {
|
||||
id: "snapshot-demo",
|
||||
name: "Snapshot Demo",
|
||||
description: "Status metadata",
|
||||
providers: ["snapshot-provider"],
|
||||
},
|
||||
providerId: "snapshot-provider",
|
||||
runtimeMessage: "runtime entry should not load for plugin status snapshot report",
|
||||
});
|
||||
const workspaceDir = makeTempDir();
|
||||
const report = buildPluginSnapshotReport({
|
||||
config: createColdPluginConfig(fixture.rootDir, fixture.pluginId),
|
||||
workspaceDir,
|
||||
env: createColdPluginHermeticEnv(workspaceDir, {
|
||||
bundledPluginsDir: makeTempDir(),
|
||||
}),
|
||||
});
|
||||
|
||||
expect(report.plugins).toEqual(
|
||||
expect.arrayContaining([
|
||||
expect.objectContaining({
|
||||
id: "snapshot-demo",
|
||||
name: "Snapshot Demo",
|
||||
source: fs.realpathSync(fixture.runtimeSource),
|
||||
status: "loaded",
|
||||
imported: false,
|
||||
}),
|
||||
]),
|
||||
);
|
||||
expect(isColdPluginRuntimeLoaded(fixture)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
128
src/plugins/test-helpers/cold-plugin-fixtures.ts
Normal file
128
src/plugins/test-helpers/cold-plugin-fixtures.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import type { OpenClawConfig } from "../../config/types.openclaw.js";
|
||||
|
||||
export type ColdPluginFixture = {
|
||||
authChoiceId: string;
|
||||
channelId: string;
|
||||
pluginId: string;
|
||||
providerId: string;
|
||||
rootDir: string;
|
||||
runtimeMarker: string;
|
||||
runtimeSource: string;
|
||||
};
|
||||
|
||||
type ColdPluginFixtureOptions = {
|
||||
rootDir: string;
|
||||
pluginId?: string;
|
||||
packageName?: string;
|
||||
packageVersion?: string;
|
||||
providerId?: string;
|
||||
channelId?: string;
|
||||
authChoiceId?: string;
|
||||
runtimeMessage?: string;
|
||||
manifest?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export function createColdPluginFixture(options: ColdPluginFixtureOptions): ColdPluginFixture {
|
||||
const pluginId = options.pluginId ?? "cold-control-plane";
|
||||
const providerId = options.providerId ?? "cold-model-provider";
|
||||
const channelId = options.channelId ?? "cold-channel";
|
||||
const authChoiceId = options.authChoiceId ?? "cold-provider-api-key";
|
||||
const runtimeSource = path.join(options.rootDir, "index.cjs");
|
||||
const runtimeMarker = path.join(options.rootDir, "runtime-loaded.txt");
|
||||
fs.writeFileSync(
|
||||
path.join(options.rootDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: options.packageName ?? "@example/openclaw-cold-control-plane",
|
||||
version: options.packageVersion ?? "1.0.0",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(options.rootDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: pluginId,
|
||||
name: "Cold Control Plane",
|
||||
configSchema: { type: "object" },
|
||||
providers: [providerId],
|
||||
channels: [channelId],
|
||||
channelConfigs: {
|
||||
[channelId]: {
|
||||
schema: { type: "object" },
|
||||
},
|
||||
},
|
||||
providerAuthChoices: [
|
||||
{
|
||||
provider: providerId,
|
||||
method: "api-key",
|
||||
choiceId: authChoiceId,
|
||||
choiceLabel: "Cold Provider API key",
|
||||
groupId: providerId,
|
||||
groupLabel: "Cold Provider",
|
||||
optionKey: "coldProviderApiKey",
|
||||
cliFlag: "--cold-provider-api-key",
|
||||
cliOption: "--cold-provider-api-key <key>",
|
||||
onboardingScopes: ["text-inference"],
|
||||
},
|
||||
],
|
||||
...options.manifest,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
runtimeSource,
|
||||
`require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf8");\nthrow new Error(${JSON.stringify(options.runtimeMessage ?? "runtime entry should not load for cold plugin metadata discovery")});\n`,
|
||||
"utf8",
|
||||
);
|
||||
return {
|
||||
authChoiceId,
|
||||
channelId,
|
||||
pluginId,
|
||||
providerId,
|
||||
rootDir: options.rootDir,
|
||||
runtimeMarker,
|
||||
runtimeSource,
|
||||
};
|
||||
}
|
||||
|
||||
export function createColdPluginConfig(pluginDir: string, pluginId: string): OpenClawConfig {
|
||||
return {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
entries: {
|
||||
[pluginId]: { enabled: true },
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function createColdPluginHermeticEnv(
|
||||
homeDir: string,
|
||||
options: { bundledPluginsDir?: string; disablePersistedRegistry?: boolean } = {},
|
||||
): NodeJS.ProcessEnv {
|
||||
return {
|
||||
...process.env,
|
||||
OPENCLAW_HOME: path.join(homeDir, "home"),
|
||||
OPENCLAW_BUNDLED_PLUGINS_DIR: options.bundledPluginsDir,
|
||||
OPENCLAW_DISABLE_PERSISTED_PLUGIN_REGISTRY:
|
||||
options.disablePersistedRegistry === false ? undefined : "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_DISCOVERY_CACHE: "1",
|
||||
OPENCLAW_DISABLE_PLUGIN_MANIFEST_CACHE: "1",
|
||||
OPENCLAW_VERSION: "2026.4.25",
|
||||
VITEST: "true",
|
||||
};
|
||||
}
|
||||
|
||||
export function isColdPluginRuntimeLoaded(fixture: Pick<ColdPluginFixture, "runtimeMarker">) {
|
||||
return fs.existsSync(fixture.runtimeMarker);
|
||||
}
|
||||
Reference in New Issue
Block a user