Files
openclaw/src/cli/cold-command-plugin-imports.process.test.ts
T
Peter Steinberger 5c5aeaddc2 test(cli): isolate cold plugin import probes (#118404)
Co-authored-by: Peter Steinberger <steipete@mac-studio-sf2.local>
2026-08-02 22:43:42 -07:00

177 lines
6.4 KiB
TypeScript

// Real CLI processes must keep unrelated plugin runtimes cold on read-only command paths.
import { execFile } from "node:child_process";
import fs from "node:fs";
import { createServer, type Server } from "node:http";
import os from "node:os";
import path from "node:path";
import { promisify } from "node:util";
import { afterAll, beforeAll, expect, it } from "vitest";
import {
createColdPluginFixture,
isColdPluginRuntimeLoaded,
} from "../plugins/test-helpers/cold-plugin-fixtures.js";
const execFileAsync = promisify(execFile);
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-cold-commands-"));
let clawHubServer: Server;
let clawHubUrl: string;
beforeAll(async () => {
clawHubServer = createServer((_request, response) => {
response.writeHead(200, { "content-type": "application/json" });
response.end('{"results":[]}');
});
await new Promise<void>((resolve) => {
clawHubServer.listen(0, "127.0.0.1", resolve);
});
const address = clawHubServer.address();
if (!address || typeof address === "string") {
throw new Error("failed to bind the ClawHub fixture server");
}
clawHubUrl = `http://127.0.0.1:${address.port}`;
});
afterAll(async () => {
await new Promise<void>((resolve, reject) => {
clawHubServer.close((error) => (error ? reject(error) : resolve()));
});
fs.rmSync(tempRoot, { recursive: true, force: true });
});
const cases = [
{ label: "hooks", args: ["hooks", "--json"], needsMemoryOwner: false },
{
label: "skills-search",
args: ["skills", "search", "fixture", "--json"],
needsMemoryOwner: false,
},
{
label: "skills-info",
args: ["skills", "info", "fixture-skill", "--json"],
needsMemoryOwner: false,
},
{
label: "memory-status",
args: ["memory", "status", "--agent", "main", "--json"],
needsMemoryOwner: true,
},
{
label: "memory-search",
args: ["memory", "search", "no-hit", "--agent", "main", "--json"],
needsMemoryOwner: true,
},
] as const;
it.each(cases)(
"keeps an unrelated plugin runtime cold for $label",
async ({ args, label, needsMemoryOwner }) => {
const root = path.join(tempRoot, label);
const stateDir = path.join(root, "state");
const workspaceDir = path.join(root, "workspace");
const pluginDir = path.join(root, "cold-plugin");
const memoryOwnerDir = path.join(root, "memory-owner");
fs.mkdirSync(path.join(workspaceDir, "skills", "fixture-skill"), { recursive: true });
fs.mkdirSync(pluginDir, { recursive: true });
fs.writeFileSync(
path.join(workspaceDir, "skills", "fixture-skill", "SKILL.md"),
"---\nname: fixture-skill\ndescription: Cold path fixture\n---\n",
);
const fixture = createColdPluginFixture({
rootDir: pluginDir,
pluginId: `cold-${label}`,
runtimeMessage: `${label} imported an unrelated plugin runtime`,
manifest: { providers: [], channels: [], providerAuthChoices: [], channelConfigs: {} },
});
if (needsMemoryOwner) {
// Keep this fixture about the ownership boundary: the selected CLI owner must
// execute while the unrelated sentinel stays cold. memory-core behavior has
// its own focused CLI suite, including the stale-search result contract.
fs.mkdirSync(memoryOwnerDir, { recursive: true });
fs.writeFileSync(
path.join(memoryOwnerDir, "package.json"),
JSON.stringify({
name: "@example/openclaw-memory-owner",
version: "1.0.0",
type: "commonjs",
openclaw: { extensions: ["./index.cjs"] },
}),
);
fs.writeFileSync(
path.join(memoryOwnerDir, "openclaw.plugin.json"),
JSON.stringify({
id: "memory-owner",
name: "Memory Owner",
configSchema: { type: "object", additionalProperties: false, properties: {} },
commandAliases: [{ name: "fixture-memory", kind: "runtime-slash", cliCommand: "memory" }],
}),
);
fs.writeFileSync(
path.join(memoryOwnerDir, "index.cjs"),
[
'const fs = require("node:fs");',
"module.exports = {",
' id: "memory-owner",',
" register(api) {",
' fs.writeFileSync(process.env.MEMORY_OWNER_MARKER, "loaded", "utf8");',
" api.registerCli(({ program }) => {",
' const memory = program.command("memory");',
' memory.command("status").option("--agent <id>").option("--json").action(() => console.log("[]"));',
' memory.command("search").argument("<query>").option("--agent <id>").option("--json").action(() => console.log("{\\\"results\\\":[]}"));',
' }, { descriptors: [{ name: "memory", description: "Memory fixture", hasSubcommands: true }] });',
" },",
"};",
"",
].join("\n"),
);
}
fs.mkdirSync(stateDir, { recursive: true });
const configPath = path.join(stateDir, "openclaw.json");
fs.writeFileSync(
configPath,
JSON.stringify({
agents: { defaults: { workspace: workspaceDir } },
plugins: {
load: { paths: [pluginDir, ...(needsMemoryOwner ? [memoryOwnerDir] : [])] },
entries: {
[fixture.pluginId]: { enabled: true },
...(needsMemoryOwner ? { "memory-owner": { enabled: true } } : {}),
},
},
}),
);
const result = await execFileAsync(
process.execPath,
["--import", "tsx", "src/entry.ts", ...args],
{
cwd: path.resolve("."),
env: {
...process.env,
// This fixture owns command imports, not entrypoint respawn or compile-cache behavior.
NODE_DISABLE_COMPILE_CACHE: "1",
NODE_ENV: undefined,
VITEST: undefined,
OPENCLAW_CLAWHUB_URL: clawHubUrl,
OPENCLAW_CONFIG_PATH: configPath,
MEMORY_OWNER_MARKER: path.join(root, "memory-owner-loaded"),
OPENCLAW_DISABLE_BUNDLED_PLUGINS: "1",
OPENCLAW_DEV_SOURCE_ROOT: path.resolve("."),
OPENCLAW_HOME: path.join(root, "home"),
OPENCLAW_NO_RESPAWN: "1",
OPENCLAW_STATE_DIR: stateDir,
},
maxBuffer: 4 * 1024 * 1024,
timeout: 60_000,
},
);
expect(`${result.stdout}\n${result.stderr}`).not.toContain(
`${label} imported an unrelated plugin runtime`,
);
expect(isColdPluginRuntimeLoaded(fixture)).toBe(false);
if (needsMemoryOwner) {
expect(fs.existsSync(path.join(root, "memory-owner-loaded"))).toBe(true);
}
},
150_000,
);