Files
openclaw/src/cli/help-exit.process.test.ts
Peter Steinberger 223235044a fix(cli): exit cleanly on subcommand-group --help (lazy register handle leak) (#111433)
Lazy command-group registration must import the real command tree to render complete help. On macOS with NODE_USE_SYSTEM_CA=1, those imports start Node's system CA loader worker; natural shutdown can block in CleanupCachedRootCertificates while joining LoadSystemCACertificates.

Request the existing stream-flushed one-shot exit immediately after successful Commander help parsing, covering both CommanderError and normal-return plugin help without affecting leaf command execution.
2026-07-19 08:10:56 -07:00

112 lines
3.9 KiB
TypeScript

// Process coverage for help rendering without loading live Gateway transports.
import { execFile } from "node:child_process";
import fs from "node:fs/promises";
import path from "node:path";
import { pathToFileURL } from "node:url";
import { promisify } from "node:util";
import { afterEach, describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../../test/helpers/temp-dir.js";
const execFileAsync = promisify(execFile);
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
const CHILD_PROCESS_TIMEOUT_MS = 30_000;
const LAZY_GROUP_HELP_CASES = [
{ group: "backup", usageCommand: "backup" },
{ group: "capability", usageCommand: "infer|capability" },
{ group: "channels", usageCommand: "channels" },
{ group: "clawbot", usageCommand: "clawbot" },
{ group: "daemon", usageCommand: "daemon" },
{ group: "hooks", usageCommand: "hooks" },
{ group: "infer", usageCommand: "infer|capability" },
{ group: "migrate", usageCommand: "migrate" },
{ group: "node", usageCommand: "node" },
{ group: "security", usageCommand: "security" },
{ group: "update", usageCommand: "update" },
] as const;
async function createHelpProcessFixture() {
const root = tempDirs.make("openclaw-help-exit-");
const stateDir = path.join(root, "state");
const configPath = path.join(stateDir, "openclaw.json");
const tlsImportGuardPath = path.join(root, "forbid-tls-import.mjs");
const keepAlivePath = path.join(root, "keep-alive.mjs");
await fs.mkdir(stateDir, { recursive: true });
await fs.writeFile(
configPath,
JSON.stringify({ plugins: { entries: { "oc-path": { enabled: true } } } }),
);
await fs.writeFile(
tlsImportGuardPath,
`import { registerHooks } from "node:module";
registerHooks({
resolve(specifier, context, nextResolve) {
if (specifier === "node:tls" || specifier === "tls") {
throw new Error(\`CLI help imported TLS from \${context.parentURL ?? "unknown"}\`);
}
return nextResolve(specifier, context);
},
});
`,
);
await fs.writeFile(keepAlivePath, "setInterval(() => {}, 60_000);\n");
return { root, stateDir, configPath, tlsImportGuardPath, keepAlivePath };
}
async function runHelpProcess(params: {
args: string[];
forbidTlsImport?: boolean;
keepAlive?: boolean;
}) {
const fixture = await createHelpProcessFixture();
return await execFileAsync(
process.execPath,
[
...(params.forbidTlsImport
? ["--import", pathToFileURL(fixture.tlsImportGuardPath).href]
: []),
...(params.keepAlive ? ["--import", pathToFileURL(fixture.keepAlivePath).href] : []),
"--import",
"tsx",
"src/entry.ts",
...params.args,
],
{
cwd: path.resolve("."),
encoding: "utf8",
env: {
...process.env,
HOME: fixture.root,
NODE_ENV: undefined,
NODE_OPTIONS: undefined,
NODE_USE_SYSTEM_CA: "1",
OPENCLAW_CONFIG_PATH: fixture.configPath,
OPENCLAW_NO_RESPAWN: "1",
OPENCLAW_STATE_DIR: fixture.stateDir,
VITEST: undefined,
},
killSignal: "SIGKILL",
timeout: CHILD_PROCESS_TIMEOUT_MS,
},
);
}
describe("CLI help process exit", () => {
it.each([
{ args: ["--help"], usage: "Usage: openclaw [options] [command]" },
{ args: ["path", "--help"], usage: "Usage: openclaw path [options] [command]" },
])("exits promptly after $args", async ({ args, usage }) => {
const result = await runHelpProcess({ args, forbidTlsImport: true });
expect(result.stderr).toBe("");
expect(result.stdout).toContain(usage);
});
it.each(LAZY_GROUP_HELP_CASES)("exits promptly after $group --help", async (testCase) => {
const { group, usageCommand } = testCase;
const result = await runHelpProcess({ args: [group, "--help"], keepAlive: true });
expect(result.stderr).toBe("");
expect(result.stdout).toContain(`Usage: openclaw ${usageCommand} [options] [command]`);
});
});