diff --git a/openclaw.mjs b/openclaw.mjs index 05f8c1c207e..77f48f78df3 100755 --- a/openclaw.mjs +++ b/openclaw.mjs @@ -371,22 +371,120 @@ const buildMissingEntryErrorMessage = async () => { const isBareRootHelpInvocation = (argv) => argv.length === 3 && (argv[2] === "--help" || argv[2] === "-h"); -const resolvePrecomputedCommandHelp = (argv) => { - if (argv.length !== 4 || (argv[3] !== "--help" && argv[3] !== "-h")) { - return null; +const LAUNCHER_HELP_FLAGS = new Set(["-h", "--help"]); +const LAUNCHER_ROOT_BOOLEAN_FLAGS = new Set(["--dev", "--no-color"]); +const LAUNCHER_ROOT_VALUE_FLAGS = new Set(["--profile", "--log-level", "--container"]); +const LAUNCHER_PRECOMPUTED_COMMAND_HELP = { + browser: { command: "browser", metadataKey: "browserHelpText" }, + secrets: { command: "secrets", metadataKey: "secretsHelpText" }, + nodes: { command: "nodes", metadataKey: "nodesHelpText" }, +}; +const LAUNCHER_PRECOMPUTED_SUBCOMMAND_HELP = new Set([ + "doctor", + "gateway", + "models", + "plugins", + "sessions", + "tasks", +]); + +const isLauncherRootOptionValueToken = (arg) => { + if (!arg || arg === "--") { + return false; } - if (argv[2] === "browser") { - return { command: "browser", metadataKey: "browserHelpText" }; + if (!arg.startsWith("-")) { + return true; } - if (argv[2] === "secrets") { - return { command: "secrets", metadataKey: "secretsHelpText" }; + return /^-\d+(?:\.\d+)?$/.test(arg); +}; + +const consumeLauncherRootOptionToken = (args, index) => { + const arg = args[index]; + if (!arg) { + return 0; } - if (argv[2] === "nodes") { - return { command: "nodes", metadataKey: "nodesHelpText" }; + if (LAUNCHER_ROOT_BOOLEAN_FLAGS.has(arg)) { + return 1; + } + if ( + arg.startsWith("--profile=") || + arg.startsWith("--log-level=") || + arg.startsWith("--container=") + ) { + return 1; + } + if (LAUNCHER_ROOT_VALUE_FLAGS.has(arg)) { + return isLauncherRootOptionValueToken(args[index + 1]) ? 2 : 1; + } + return 0; +}; + +const hasLauncherContainerTarget = (argv) => { + if (normalizeLauncherMetadataValue(process.env.OPENCLAW_CONTAINER)) { + return true; + } + const args = argv.slice(2); + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg || arg === "--") { + return false; + } + if (arg === "--container" || arg.startsWith("--container=")) { + return true; + } + } + return false; +}; + +const resolvePrecomputedCommandHelpByName = (commandName) => { + if (Object.hasOwn(LAUNCHER_PRECOMPUTED_COMMAND_HELP, commandName)) { + return LAUNCHER_PRECOMPUTED_COMMAND_HELP[commandName]; + } + if (LAUNCHER_PRECOMPUTED_SUBCOMMAND_HELP.has(commandName)) { + return { + command: commandName, + metadataKey: "subcommandHelpText", + subcommandKey: commandName, + }; } return null; }; +const resolvePrecomputedCommandHelp = (argv) => { + const args = argv.slice(2); + let commandHelp = null; + let sawHelp = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg || arg === "--") { + return null; + } + if (!commandHelp) { + const consumed = consumeLauncherRootOptionToken(args, index); + if (consumed > 0) { + index += consumed - 1; + continue; + } + if (arg.startsWith("-")) { + return null; + } + commandHelp = resolvePrecomputedCommandHelpByName(arg); + if (!commandHelp) { + return null; + } + continue; + } + if (LAUNCHER_HELP_FLAGS.has(arg)) { + sawHelp = true; + continue; + } + return null; + } + + return commandHelp && sawHelp ? commandHelp : null; +}; + const isHelpFastPathDisabled = () => process.env.OPENCLAW_DISABLE_CLI_STARTUP_HELP_FAST_PATH === "1"; @@ -456,11 +554,11 @@ const shouldDeferRootHelpToRuntimeEntry = () => { return false; }; -const loadPrecomputedHelpText = (key) => { +const loadPrecomputedHelpText = (key, subkey) => { try { const raw = readFileSync(new URL("./dist/cli-startup-metadata.json", import.meta.url), "utf8"); const parsed = JSON.parse(raw); - const value = parsed?.[key]; + const value = subkey ? parsed?.[key]?.[subkey] : parsed?.[key]; return typeof value === "string" && value.length > 0 ? value : null; } catch { return null; @@ -650,10 +748,13 @@ const tryOutputPrecomputedCommandHelp = () => { if (!commandHelp) { return false; } + if (hasLauncherContainerTarget(process.argv)) { + return false; + } if (commandHelp.command === "nodes" && shouldDeferRootHelpToRuntimeEntry()) { return false; } - const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey); + const precomputed = loadPrecomputedHelpText(commandHelp.metadataKey, commandHelp.subcommandKey); if (!precomputed) { return false; } diff --git a/scripts/bench-cli-startup.ts b/scripts/bench-cli-startup.ts index cbec863e38d..1d5b8adb345 100644 --- a/scripts/bench-cli-startup.ts +++ b/scripts/bench-cli-startup.ts @@ -663,6 +663,10 @@ function parseMaxRssMb(stderr: string): number | null { return Number(lastMatch[1]) / 1024; } +function nodeImportSpecifierForPath(filePath: string): string { + return pathToFileURL(filePath).href; +} + function buildCpuOrHeapFlags(options: { cpuProfDir?: string; heapProfDir?: string }): string[] { const flags: string[] = []; if (options.cpuProfDir) { @@ -697,7 +701,7 @@ async function runSample(params: { } const nodeArgs = [ "--import", - params.rssHookPath, + nodeImportSpecifierForPath(params.rssHookPath), ...buildCpuOrHeapFlags({ cpuProfDir: params.cpuProfDir, heapProfDir: params.heapProfDir, @@ -1268,6 +1272,7 @@ async function main(): Promise { export const testing = { buildConfigFixture, collectFailedSamples, + nodeImportSpecifierForPath, parseGatewayPortEnv, parseNonNegativeInt, parsePositiveInt, diff --git a/scripts/check-cli-startup-memory.mjs b/scripts/check-cli-startup-memory.mjs index 1b9cf84ad65..7c2a1942404 100644 --- a/scripts/check-cli-startup-memory.mjs +++ b/scripts/check-cli-startup-memory.mjs @@ -173,6 +173,10 @@ function formatCaseCommand(testCase) { return `node ${testCase.args.join(" ")}`; } +function nodeImportSpecifierForPath(filePath) { + return pathToFileURL(filePath).href; +} + function buildBenchEnv() { if (!tmpHome) { throw new Error("temporary home is not initialized"); @@ -218,14 +222,18 @@ function runCase(testCase, params = {}) { const env = buildBenchEnv(); const spawn = params.spawnSync ?? defaultSpawnSync; const timeoutMs = params.timeoutMs ?? COMMAND_TIMEOUT_MS; - const result = spawn(process.execPath, ["--import", rssHookPath, ...testCase.args], { - cwd: repoRoot, - env, - encoding: "utf8", - maxBuffer: 20 * 1024 * 1024, - timeout: timeoutMs, - killSignal: "SIGKILL", - }); + const result = spawn( + process.execPath, + ["--import", nodeImportSpecifierForPath(rssHookPath), ...testCase.args], + { + cwd: repoRoot, + env, + encoding: "utf8", + maxBuffer: 20 * 1024 * 1024, + timeout: timeoutMs, + killSignal: "SIGKILL", + }, + ); const stderr = result.stderr ?? ""; const maxRssMb = parseMaxRssMb(stderr); const matrixBootstrapWarning = /matrix: crypto runtime bootstrap failed/i.test(stderr); @@ -373,6 +381,7 @@ function runStartupMemoryCheck(argv = process.argv.slice(2), params = {}) { */ export const testing = { cases, + nodeImportSpecifierForPath, parseArgs, readPositiveIntEnv, readPositiveNumberEnv, diff --git a/scripts/write-cli-startup-metadata.ts b/scripts/write-cli-startup-metadata.ts index a032a45de10..55f4f62aca2 100644 --- a/scripts/write-cli-startup-metadata.ts +++ b/scripts/write-cli-startup-metadata.ts @@ -33,7 +33,14 @@ const COMMAND_HELP_RENDER_TIMEOUT_MS = 120_000; const COMMAND_HELP_RENDER_MAX_OUTPUT_BYTES = 16 * 1024 * 1024; const COMMAND_HELP_RENDER_KILL_GRACE_MS = 5_000; const COMMAND_HELP_RENDER_CONCURRENCY = 2; -const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = ["doctor", "gateway", "models", "plugins"] as const; +const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = [ + "doctor", + "gateway", + "models", + "plugins", + "sessions", + "tasks", +] as const; const CORE_CHANNEL_ORDER = [ "telegram", "whatsapp", @@ -249,6 +256,7 @@ function resolveSubcommandHelpSourceSignature(sourceRootDir: string = rootDir): path.join(sourceRootDir, "src/cli/help-format.ts"), path.join(sourceRootDir, "src/cli/daemon-cli/register-service-commands.ts"), path.join(sourceRootDir, "src/cli/program/register.maintenance.ts"), + path.join(sourceRootDir, "src/cli/program/register.status-health-sessions.ts"), path.join(sourceRootDir, "src/cli/gateway-cli.ts"), path.join(sourceRootDir, "src/cli/gateway-cli/register.ts"), path.join(sourceRootDir, "src/cli/gateway-cli/run-command.ts"), diff --git a/src/cli/root-help-metadata.ts b/src/cli/root-help-metadata.ts index 241958d34a9..db8ce0ba4c8 100644 --- a/src/cli/root-help-metadata.ts +++ b/src/cli/root-help-metadata.ts @@ -1,7 +1,13 @@ // Cached startup metadata readers for precomputed root and subcommand help text. import { readCliStartupMetadata } from "./startup-metadata.js"; -export type PrecomputedSubcommandHelpName = "doctor" | "gateway" | "models" | "plugins"; +export type PrecomputedSubcommandHelpName = + | "doctor" + | "gateway" + | "models" + | "plugins" + | "sessions" + | "tasks"; let precomputedRootHelpText: string | null | undefined; let precomputedBrowserHelpText: string | null | undefined; @@ -139,7 +145,9 @@ function isPrecomputedSubcommandHelpName( commandName === "doctor" || commandName === "gateway" || commandName === "models" || - commandName === "plugins" + commandName === "plugins" || + commandName === "sessions" || + commandName === "tasks" ); } diff --git a/src/cli/run-main-policy.ts b/src/cli/run-main-policy.ts index 0b7f63bf2ff..aa7764e4650 100644 --- a/src/cli/run-main-policy.ts +++ b/src/cli/run-main-policy.ts @@ -24,7 +24,14 @@ import { getSubCliParentDefaultHelpCommands } from "./program/subcli-descriptors const ROOT_HELP_ALIASES = new Set(["tools"]); const SETUP_ONBOARD_CONFIGURE_HELP_COMMANDS = new Set(["setup", "onboard", "configure"]); -const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = new Set(["doctor", "gateway", "models", "plugins"]); +const PRECOMPUTED_SUBCOMMAND_HELP_COMMANDS = new Set([ + "doctor", + "gateway", + "models", + "plugins", + "sessions", + "tasks", +]); const HELP_FLAGS = new Set(["-h", "--help"]); const VERSION_FLAGS = new Set(["-V", "--version"]); const BARE_PARENT_DEFAULT_HELP_COMMANDS = new Set([ diff --git a/src/cli/run-main.test.ts b/src/cli/run-main.test.ts index c24169e5d29..7f9616ca27a 100644 --- a/src/cli/run-main.test.ts +++ b/src/cli/run-main.test.ts @@ -319,6 +319,12 @@ describe("resolvePrecomputedSubcommandHelpFastPath", () => { expect( resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "plugins", "--help"]), ).toBe("plugins"); + expect( + resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "sessions", "--help"]), + ).toBe("sessions"); + expect(resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "tasks", "-h"])).toBe( + "tasks", + ); expect( resolvePrecomputedSubcommandHelpFastPath(["node", "openclaw", "doctor", "--version"]), ).toBeNull(); diff --git a/src/entry.test.ts b/src/entry.test.ts index 8af9dc2aa93..68bf93057ac 100644 --- a/src/entry.test.ts +++ b/src/entry.test.ts @@ -155,6 +155,68 @@ describe("entry precomputed command help fast path", () => { expect(outputPrecomputedNodesHelpTextCalls).toBe(1); }); + it.each(["doctor", "sessions", "tasks"])( + "renders precomputed %s help from startup metadata without importing the full program", + async (commandName) => { + const outputPrecomputedSubcommandHelpTextCalls: string[] = []; + + const handled = await tryHandlePrecomputedCommandHelpFastPath( + ["node", "openclaw", commandName, "--help"], + { + env: {}, + outputPrecomputedSubcommandHelpText: (requestedCommandName) => { + outputPrecomputedSubcommandHelpTextCalls.push(requestedCommandName); + return true; + }, + }, + ); + + expect(handled).toBe(true); + expect(outputPrecomputedSubcommandHelpTextCalls).toEqual([commandName]); + }, + ); + + it("renders precomputed subcommand help with leading root options", async () => { + const outputPrecomputedSubcommandHelpTextCalls: string[] = []; + + const handled = await tryHandlePrecomputedCommandHelpFastPath( + ["node", "openclaw", "--profile", "work", "--no-color", "models", "-h"], + { + env: {}, + outputPrecomputedSubcommandHelpText: (commandName) => { + outputPrecomputedSubcommandHelpTextCalls.push(commandName); + return true; + }, + }, + ); + + expect(handled).toBe(true); + expect(outputPrecomputedSubcommandHelpTextCalls).toEqual(["models"]); + }); + + it("keeps subcommand help fast path strict for extra or mixed flags", async () => { + const invocations = [ + ["node", "openclaw", "doctor", "--help", "--bogus"], + ["node", "openclaw", "doctor", "--help", "extra"], + ["node", "openclaw", "doctor", "--version", "-h"], + ["node", "openclaw", "--bogus", "doctor", "--help"], + ]; + let outputPrecomputedSubcommandHelpTextCalls = 0; + + for (const argv of invocations) { + const handled = await tryHandlePrecomputedCommandHelpFastPath(argv, { + env: {}, + outputPrecomputedSubcommandHelpText: () => { + outputPrecomputedSubcommandHelpTextCalls += 1; + return true; + }, + }); + + expect(handled).toBe(false); + } + expect(outputPrecomputedSubcommandHelpTextCalls).toBe(0); + }); + it("defers nodes help when plugin config can change command metadata", async () => { let outputPrecomputedNodesHelpTextCalls = 0; let liveConfigChecks = 0; @@ -288,4 +350,22 @@ describe("entry precomputed command help fast path", () => { expect(handled).toBe(false); expect(outputPrecomputedSecretsHelpTextCalls).toBe(0); }); + + it("skips the host command help fast path when a container target comes from env", async () => { + let outputPrecomputedBrowserHelpTextCalls = 0; + + const handled = await tryHandlePrecomputedCommandHelpFastPath( + ["node", "openclaw", "browser", "--help"], + { + env: { OPENCLAW_CONTAINER: "demo" }, + outputPrecomputedBrowserHelpText: () => { + outputPrecomputedBrowserHelpTextCalls += 1; + return true; + }, + }, + ); + + expect(handled).toBe(false); + expect(outputPrecomputedBrowserHelpTextCalls).toBe(0); + }); }); diff --git a/src/entry.ts b/src/entry.ts index e1eb7dbf115..421fae88b66 100644 --- a/src/entry.ts +++ b/src/entry.ts @@ -15,6 +15,7 @@ import { } from "./entry.compile-cache.js"; import { buildCliRespawnPlan, runCliRespawnPlan } from "./entry.respawn.js"; import { tryHandleRootVersionFastPath } from "./entry.version-fast-path.js"; +import { consumeRootOptionToken } from "./infra/cli-root-options.js"; import { isTruthyEnvValue, normalizeEnv } from "./infra/env.js"; import { isMainModule } from "./infra/is-main.js"; import { ensureOpenClawExecMarkerOnProcess } from "./infra/openclaw-exec-env.js"; @@ -26,8 +27,18 @@ const ENTRY_WRAPPER_PAIRS = [ ] as const; type PrecomputedCommandHelpName = "browser" | "secrets" | "nodes"; +type PrecomputedSubcommandHelpName = + | "doctor" + | "gateway" + | "models" + | "plugins" + | "sessions" + | "tasks"; type OutputPrecomputedHelpText = () => boolean; +const HELP_FLAGS = new Set(["-h", "--help"]); +const VERSION_FLAGS = new Set(["-V", "--version"]); + const loadRootHelpLiveConfigModule = async () => await import("./cli/root-help-live-config.js"); const loadRootHelpMetadataModule = async () => await import("./cli/root-help-metadata.js"); @@ -227,12 +238,69 @@ function resolvePrecomputedCommandHelpName(argv: string[]): PrecomputedCommandHe return null; } +function resolvePrecomputedSubcommandHelpName( + argv: string[], +): PrecomputedSubcommandHelpName | null { + const args = argv.slice(2); + let commandName: PrecomputedSubcommandHelpName | null = null; + let sawHelp = false; + + for (let index = 0; index < args.length; index += 1) { + const arg = args[index]; + if (!arg || arg === "--") { + return null; + } + if (VERSION_FLAGS.has(arg)) { + return null; + } + if (!commandName) { + const consumed = consumeRootOptionToken(args, index); + if (consumed > 0) { + index += consumed - 1; + continue; + } + if (arg.startsWith("-")) { + return null; + } + commandName = resolvePrecomputedSubcommandName(arg); + if (!commandName) { + return null; + } + continue; + } + if (HELP_FLAGS.has(arg)) { + sawHelp = true; + continue; + } + return null; + } + + return commandName && sawHelp ? commandName : null; +} + +function resolvePrecomputedSubcommandName( + commandName: string, +): PrecomputedSubcommandHelpName | null { + if ( + commandName === "doctor" || + commandName === "gateway" || + commandName === "models" || + commandName === "plugins" || + commandName === "sessions" || + commandName === "tasks" + ) { + return commandName; + } + return null; +} + export async function tryHandlePrecomputedCommandHelpFastPath( argv: string[], deps: { outputPrecomputedBrowserHelpText?: OutputPrecomputedHelpText; outputPrecomputedSecretsHelpText?: OutputPrecomputedHelpText; outputPrecomputedNodesHelpText?: OutputPrecomputedHelpText; + outputPrecomputedSubcommandHelpText?: (commandName: string) => boolean; loadRootHelpRenderOptionsForConfigSensitivePlugins?: ( env?: NodeJS.ProcessEnv, ) => Promise; @@ -247,11 +315,18 @@ export async function tryHandlePrecomputedCommandHelpFastPath( return false; } const commandName = resolvePrecomputedCommandHelpName(argv); - if (!commandName) { + const subcommandName = commandName ? null : resolvePrecomputedSubcommandHelpName(argv); + if (!commandName && !subcommandName) { return false; } try { + if (subcommandName) { + const outputPrecomputedSubcommandHelpText = + deps.outputPrecomputedSubcommandHelpText ?? + (await loadRootHelpMetadataModule()).outputPrecomputedSubcommandHelpText; + return outputPrecomputedSubcommandHelpText(subcommandName); + } if (commandName === "nodes") { const loadRootHelpRenderOptionsForConfigSensitivePlugins = deps.loadRootHelpRenderOptionsForConfigSensitivePlugins ?? diff --git a/test/openclaw-launcher.e2e.test.ts b/test/openclaw-launcher.e2e.test.ts index 28792f4ef66..1d8052b8c83 100644 --- a/test/openclaw-launcher.e2e.test.ts +++ b/test/openclaw-launcher.e2e.test.ts @@ -480,6 +480,125 @@ describe("openclaw launcher", () => { expect(result.stdout).toBe(`PRECOMPUTED ${params.command} help\n`); }); + it.each(["doctor", "gateway", "models", "plugins", "sessions", "tasks"])( + "uses precomputed %s help before loading the runtime entry", + async (command) => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await fs.writeFile( + path.join(fixtureRoot, "dist", "cli-startup-metadata.json"), + JSON.stringify({ subcommandHelpText: { [command]: `PRECOMPUTED ${command} help\n` } }), + "utf8", + ); + + const result = spawnSync( + process.execPath, + [path.join(fixtureRoot, "openclaw.mjs"), command, "--help"], + { + cwd: fixtureRoot, + env: launcherEnv(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe(`PRECOMPUTED ${command} help\n`); + }, + ); + + it("uses precomputed subcommand help with leading root options", async () => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await fs.writeFile( + path.join(fixtureRoot, "dist", "cli-startup-metadata.json"), + JSON.stringify({ subcommandHelpText: { models: "PRECOMPUTED models help\n" } }), + "utf8", + ); + + const result = spawnSync( + process.execPath, + [path.join(fixtureRoot, "openclaw.mjs"), "--profile", "work", "--no-color", "models", "-h"], + { + cwd: fixtureRoot, + env: launcherEnv(), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("PRECOMPUTED models help\n"); + }); + + it("defers precomputed subcommand help to the runtime entry when container env is set", async () => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await fs.writeFile( + path.join(fixtureRoot, "dist", "cli-startup-metadata.json"), + JSON.stringify({ subcommandHelpText: { models: "PRECOMPUTED models help\n" } }), + "utf8", + ); + await fs.writeFile( + path.join(fixtureRoot, "dist", "entry.js"), + "process.stdout.write('RUNTIME ENTRY\\n');\n", + "utf8", + ); + + const result = spawnSync( + process.execPath, + [path.join(fixtureRoot, "openclaw.mjs"), "models", "--help"], + { + cwd: fixtureRoot, + env: launcherEnv({ OPENCLAW_CONTAINER: "demo" }), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("RUNTIME ENTRY\n"); + expect(result.stdout).not.toContain("PRECOMPUTED"); + }); + + it.each([ + { + name: "container env", + args: ["browser", "--help"], + env: { OPENCLAW_CONTAINER: "demo" }, + }, + { + name: "root --container flag", + args: ["--container", "demo", "browser", "--help"], + env: {}, + }, + { + name: "root --container=value flag", + args: ["--container=demo", "browser", "--help"], + env: {}, + }, + ])("defers precomputed command help to the runtime entry with $name", async (params) => { + const fixtureRoot = await makeLauncherFixture(fixtureRoots); + await fs.writeFile( + path.join(fixtureRoot, "dist", "cli-startup-metadata.json"), + JSON.stringify({ browserHelpText: "PRECOMPUTED browser help\n" }), + "utf8", + ); + await fs.writeFile( + path.join(fixtureRoot, "dist", "entry.js"), + "process.stdout.write('RUNTIME ENTRY\\n');\n", + "utf8", + ); + + const result = spawnSync( + process.execPath, + [path.join(fixtureRoot, "openclaw.mjs"), ...params.args], + { + cwd: fixtureRoot, + env: launcherEnv(params.env), + encoding: "utf8", + }, + ); + + expect(result.status).toBe(0); + expect(result.stdout).toBe("RUNTIME ENTRY\n"); + expect(result.stdout).not.toContain("PRECOMPUTED"); + }); + it("defers root help to the runtime entry when plugin config can change help", async () => { const fixtureRoot = await makeLauncherFixture(fixtureRoots); const configPath = path.join(fixtureRoot, "openclaw.json"); diff --git a/test/scripts/bench-cli-startup.test.ts b/test/scripts/bench-cli-startup.test.ts index 0db3312edd3..10a8cc24ae4 100644 --- a/test/scripts/bench-cli-startup.test.ts +++ b/test/scripts/bench-cli-startup.test.ts @@ -1,7 +1,8 @@ // Bench Cli Startup tests cover bench cli startup script behavior. import { spawnSync } from "node:child_process"; import { existsSync, readFileSync, writeFileSync } from "node:fs"; -import { join } from "node:path"; +import { join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { testing } from "../../scripts/bench-cli-startup.ts"; import { withEnv } from "../../src/test-utils/env.js"; @@ -187,6 +188,12 @@ describe("bench-cli-startup", () => { } }); + it("passes generated import hook paths as file URL specifiers", () => { + const hookPath = resolve("measure-rss.mjs"); + + expect(testing.nodeImportSpecifierForPath(hookPath)).toBe(pathToFileURL(hookPath).href); + }); + it("fails reports with no measured samples", () => { expect( testing.collectFailedSamples({ diff --git a/test/scripts/check-cli-startup-memory.test.ts b/test/scripts/check-cli-startup-memory.test.ts index 8d0c093417c..182296c75cb 100644 --- a/test/scripts/check-cli-startup-memory.test.ts +++ b/test/scripts/check-cli-startup-memory.test.ts @@ -218,4 +218,42 @@ describe("check-cli-startup-memory", () => { ), ).toThrow("--help did not report max RSS"); }); + + it("passes the generated RSS hook as a Node import URL", () => { + if (process.platform !== "darwin" && process.platform !== "linux") { + return; + } + + const tempRoot = makeTempRoot(); + const seenArgs: string[][] = []; + + const result = testing.runStartupMemoryCheck( + [ + "--json", + path.join(tempRoot, "startup-memory.json"), + "--summary", + path.join(tempRoot, "summary.md"), + ], + { + platform: "linux", + spawnSync: (_command: string, args: string[]) => { + seenArgs.push(args); + return { + error: null, + signal: null, + status: 0, + stderr: "__OPENCLAW_MAX_RSS_KB__=1024\n", + stdout: "", + }; + }, + }, + ); + + expect(result.skipped).toBe(false); + expect(seenArgs).toHaveLength(testing.cases.length); + for (const args of seenArgs) { + expect(args[0]).toBe("--import"); + expect(args[1]).toMatch(/^file:/u); + } + }); }); diff --git a/test/scripts/write-cli-startup-metadata.test.ts b/test/scripts/write-cli-startup-metadata.test.ts index ea33dd3349e..0780dd87cc5 100644 --- a/test/scripts/write-cli-startup-metadata.test.ts +++ b/test/scripts/write-cli-startup-metadata.test.ts @@ -34,6 +34,10 @@ function writeStartupMetadataSourceSignatureFixture(rootDir: string): void { ["src/cli/models-cli.ts", "export const modelsHelp = 'models';\n"], ["src/cli/nodes-cli/register.ts", "export const nodesHelp = 'nodes';\n"], ["src/cli/program/register.maintenance.ts", "export const maintenanceHelp = 'maintenance';\n"], + [ + "src/cli/program/register.status-health-sessions.ts", + "export const statusHealthSessionsHelp = 'sessions';\n", + ], ["src/cli/program/context.ts", "export const context = 'context';\n"], ["src/cli/program/help.ts", "export const help = 'help';\n"], ["src/cli/plugins-cli.ts", "export const pluginsHelp = 'plugins';\n"], @@ -364,6 +368,8 @@ describe("write-cli-startup-metadata", () => { gateway: "Usage: openclaw gateway\n", models: "Usage: openclaw models\n", plugins: "Usage: openclaw plugins\n", + sessions: "Usage: openclaw sessions\n", + tasks: "Usage: openclaw tasks\n", }), }); @@ -379,6 +385,8 @@ describe("write-cli-startup-metadata", () => { gateway: string; models: string; plugins: string; + sessions: string; + tasks: string; }; }; expect(written.channelOptions).toContain("matrix"); @@ -395,6 +403,8 @@ describe("write-cli-startup-metadata", () => { expect(written.subcommandHelpText.gateway).toContain("openclaw gateway"); expect(written.subcommandHelpText.models).toContain("openclaw models"); expect(written.subcommandHelpText.plugins).toContain("openclaw plugins"); + expect(written.subcommandHelpText.sessions).toContain("openclaw sessions"); + expect(written.subcommandHelpText.tasks).toContain("openclaw tasks"); }); it("renders independent startup help snapshots concurrently", async () => { @@ -452,6 +462,8 @@ describe("write-cli-startup-metadata", () => { gateway: "Usage: openclaw gateway\n", models: "Usage: openclaw models\n", plugins: "Usage: openclaw plugins\n", + sessions: "Usage: openclaw sessions\n", + tasks: "Usage: openclaw tasks\n", }; }, }); @@ -500,6 +512,8 @@ describe("write-cli-startup-metadata", () => { gateway: "Usage: openclaw gateway\n", models: "Usage: openclaw models\n", plugins: "Usage: openclaw plugins\n", + sessions: "Usage: openclaw sessions\n", + tasks: "Usage: openclaw tasks\n", }), }); };