From 7cecbe1002c017a12e2333bcf3773282289d0cb6 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 26 Apr 2026 17:15:39 -0700 Subject: [PATCH] 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. --- .../plugin-control-plane-cold-imports.test.ts | 143 ++++-------------- src/plugins/status.registry-snapshot.test.ts | 91 ++++++----- .../test-helpers/cold-plugin-fixtures.ts | 128 ++++++++++++++++ 3 files changed, 213 insertions(+), 149 deletions(-) create mode 100644 src/plugins/test-helpers/cold-plugin-fixtures.ts diff --git a/src/commands/plugin-control-plane-cold-imports.test.ts b/src/commands/plugin-control-plane-cold-imports.test.ts index 8572af6130d..3f52603d548 100644 --- a/src/commands/plugin-control-plane-cold-imports.test.ts +++ b/src/commands/plugin-control-plane-cold-imports.test.ts @@ -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 ", - 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); }); }); diff --git a/src/plugins/status.registry-snapshot.test.ts b/src/plugins/status.registry-snapshot.test.ts index 245e0fd9c8f..474fb27a69b 100644 --- a/src/plugins/status.registry-snapshot.test.ts +++ b/src/plugins/status.registry-snapshot.test.ts @@ -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); }); }); diff --git a/src/plugins/test-helpers/cold-plugin-fixtures.ts b/src/plugins/test-helpers/cold-plugin-fixtures.ts new file mode 100644 index 00000000000..3fa98d5fc3d --- /dev/null +++ b/src/plugins/test-helpers/cold-plugin-fixtures.ts @@ -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; +}; + +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 ", + 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) { + return fs.existsSync(fixture.runtimeMarker); +}