diff --git a/src/cli/program/register.status-health-sessions.test.ts b/src/cli/program/register.status-health-sessions.test.ts index 6e441a713b92..9aa7bad7662a 100644 --- a/src/cli/program/register.status-health-sessions.test.ts +++ b/src/cli/program/register.status-health-sessions.test.ts @@ -2,6 +2,7 @@ import { Command } from "commander"; // Register status/health/session tests cover status-related command registration. import { createRequireRecord } from "openclaw/plugin-sdk/test-fixtures"; import { beforeEach, describe, expect, it, vi } from "vitest"; +import { ExpectedCliError } from "../failure-output.js"; import { registerStatusHealthSessionsCommands } from "./register.status-health-sessions.js"; const mocks = vi.hoisted(() => ({ @@ -14,6 +15,11 @@ const mocks = vi.hoisted(() => ({ sessionsArchiveCommand: vi.fn(), sessionsDeleteCommand: vi.fn(), exportTrajectoryCommand: vi.fn(), + sessionsCleanupModuleLoaded: vi.fn(), + sessionsTailModuleLoaded: vi.fn(), + sessionsCompactModuleLoaded: vi.fn(), + sessionsLifecycleModuleLoaded: vi.fn(), + exportTrajectoryModuleLoaded: vi.fn(), setVerbose: vi.fn(), runtime: { log: vi.fn(), @@ -67,26 +73,33 @@ vi.mock("../../commands/sessions.js", () => ({ sessionsCommand: mocks.sessionsCommand, })); -vi.mock("../../commands/sessions-cleanup.js", () => ({ - sessionsCleanupCommand: mocks.sessionsCleanupCommand, -})); +vi.mock("../../commands/sessions-cleanup.js", () => { + mocks.sessionsCleanupModuleLoaded(); + return { sessionsCleanupCommand: mocks.sessionsCleanupCommand }; +}); -vi.mock("../../commands/sessions-tail.js", () => ({ - sessionsTailCommand: mocks.sessionsTailCommand, -})); +vi.mock("../../commands/sessions-tail.js", () => { + mocks.sessionsTailModuleLoaded(); + return { sessionsTailCommand: mocks.sessionsTailCommand }; +}); -vi.mock("../../commands/sessions-compact.js", () => ({ - sessionsCompactCommand: mocks.sessionsCompactCommand, -})); +vi.mock("../../commands/sessions-compact.js", () => { + mocks.sessionsCompactModuleLoaded(); + return { sessionsCompactCommand: mocks.sessionsCompactCommand }; +}); -vi.mock("../../commands/sessions-lifecycle.js", () => ({ - sessionsArchiveCommand: mocks.sessionsArchiveCommand, - sessionsDeleteCommand: mocks.sessionsDeleteCommand, -})); +vi.mock("../../commands/sessions-lifecycle.js", () => { + mocks.sessionsLifecycleModuleLoaded(); + return { + sessionsArchiveCommand: mocks.sessionsArchiveCommand, + sessionsDeleteCommand: mocks.sessionsDeleteCommand, + }; +}); -vi.mock("../../commands/export-trajectory.js", () => ({ - exportTrajectoryCommand: mocks.exportTrajectoryCommand, -})); +vi.mock("../../commands/export-trajectory.js", () => { + mocks.exportTrajectoryModuleLoaded(); + return { exportTrajectoryCommand: mocks.exportTrajectoryCommand }; +}); vi.mock("../../globals.js", () => ({ setVerbose: mocks.setVerbose, @@ -107,6 +120,19 @@ describe("registerStatusHealthSessionsCommands", () => { await createProgram().parseAsync(args, { from: "user" }); } + async function expectSessionsRegistrationError( + args: string[], + message: string, + owner: typeof sessionsCleanupCommand, + ) { + const execution = runCli(args); + await expect(execution).rejects.toBeInstanceOf(ExpectedCliError); + await expect(execution).rejects.toMatchObject({ message }); + expect(runtime.error).not.toHaveBeenCalled(); + expect(runtime.exit).not.toHaveBeenCalled(); + expect(owner).not.toHaveBeenCalled(); + } + beforeEach(() => { vi.clearAllMocks(); runtime.exit.mockImplementation(() => {}); @@ -121,6 +147,82 @@ describe("registerStatusHealthSessionsCommands", () => { exportTrajectoryCommand.mockResolvedValue(undefined); }); + it.each([ + { + name: "cleanup inherited list filter", + args: ["sessions", "--active", "5", "cleanup", "--json"], + message: + "`sessions cleanup` does not support the parent `sessions` option --active; session-list filters cannot scope session maintenance.", + owner: sessionsCleanupCommand, + }, + { + name: "human-only tail inherited JSON", + args: ["sessions", "--json", "tail"], + message: + "`sessions tail` does not support the parent `sessions` option --json; trajectory tail emits human-readable progress and selects sessions separately.", + owner: sessionsTailCommand, + }, + { + name: "trajectory export inherited all-agent scope", + args: ["sessions", "--all-agents", "export-trajectory", "--json"], + message: + "`sessions export-trajectory` does not support the parent `sessions` option --all-agents; trajectory export targets one session and cannot apply session-list filters.", + owner: exportTrajectoryCommand, + }, + { + name: "archive inherited store", + args: ["sessions", "--store", "/tmp/other.sqlite", "archive", "agent:main:test", "--json"], + message: + "`sessions archive` does not support the parent `sessions` option --store; the gateway resolves target stores from each key and --agent.", + owner: sessionsArchiveCommand, + }, + { + name: "delete inherited all-agent scope", + args: ["sessions", "--all-agents", "delete", "agent:main:test", "--yes", "--json"], + message: + "`sessions delete` does not support the parent `sessions` option --all-agents; the gateway resolves target stores from each key and --agent.", + owner: sessionsDeleteCommand, + }, + { + name: "archive invalid timeout", + args: ["sessions", "--json", "archive", "agent:main:test", "--timeout", "0"], + message: "--timeout must be a positive integer (milliseconds).", + owner: sessionsArchiveCommand, + }, + { + name: "delete invalid timeout", + args: ["sessions", "delete", "agent:main:test", "--timeout", "nope", "--yes", "--json"], + message: "--timeout must be a positive integer (milliseconds).", + owner: sessionsDeleteCommand, + }, + { + name: "compact inherited all-agent scope", + args: ["sessions", "--all-agents", "compact", "agent:main:test", "--json"], + message: + "`sessions compact` does not support the parent `sessions` option --all-agents; the gateway resolves the target store from and --agent.", + owner: sessionsCompactCommand, + }, + { + name: "compact invalid max-lines", + args: ["sessions", "compact", "agent:main:test", "--max-lines", "0", "--json"], + message: "--max-lines must be a positive integer.", + owner: sessionsCompactCommand, + }, + { + name: "compact invalid timeout", + args: ["sessions", "--json", "compact", "agent:main:test", "--timeout", "0"], + message: "--timeout must be a positive integer (milliseconds).", + owner: sessionsCompactCommand, + }, + ])("rejects $name before loading any session owner", async ({ args, message, owner }) => { + await expectSessionsRegistrationError(args, message, owner); + expect(mocks.sessionsCleanupModuleLoaded).not.toHaveBeenCalled(); + expect(mocks.sessionsTailModuleLoaded).not.toHaveBeenCalled(); + expect(mocks.sessionsCompactModuleLoaded).not.toHaveBeenCalled(); + expect(mocks.sessionsLifecycleModuleLoaded).not.toHaveBeenCalled(); + expect(mocks.exportTrajectoryModuleLoaded).not.toHaveBeenCalled(); + }); + it("runs status command with timeout and debug-derived verbose", async () => { await runCli([ "status", @@ -298,28 +400,19 @@ describe("registerStatusHealthSessionsCommands", () => { }); it("rejects an inherited parent --store for compact instead of mutating a different store (regression #91378)", async () => { - await runCli(["sessions", "--store", "/tmp/other-sessions.json", "compact", "agent:work:main"]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--store")); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsCompactCommand).not.toHaveBeenCalled(); + await expectSessionsRegistrationError( + ["sessions", "--store", "/tmp/other-sessions.json", "compact", "agent:work:main"], + "`sessions compact` does not support the parent `sessions` option --store; the gateway resolves the target store from and --agent.", + sessionsCompactCommand, + ); }); it("rejects other unsupported inherited parent list options for compact", async () => { - await runCli([ - "sessions", - "--all-agents", - "--limit", - "25", - "--verbose", - "compact", - "agent:work:main", - ]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--all-agents")); - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--verbose")); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsCompactCommand).not.toHaveBeenCalled(); + await expectSessionsRegistrationError( + ["sessions", "--all-agents", "--limit", "25", "--verbose", "compact", "agent:work:main"], + "`sessions compact` does not support the parent `sessions` options --all-agents, --limit, --verbose; the gateway resolves the target store from and --agent.", + sessionsCompactCommand, + ); }); it("forwards multi-key archive options and inherits parent sessions output options", async () => { @@ -378,29 +471,26 @@ describe("registerStatusHealthSessionsCommands", () => { }); it("rejects inherited session-list filters for lifecycle mutations", async () => { - await runCli([ - "sessions", - "--store", - "/tmp/other-sessions.json", - "--all-agents", - "archive", - "agent:main:scratch-1", - ]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--store")); - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--all-agents")); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsArchiveCommand).not.toHaveBeenCalled(); + await expectSessionsRegistrationError( + [ + "sessions", + "--store", + "/tmp/other-sessions.json", + "--all-agents", + "archive", + "agent:main:scratch-1", + ], + "`sessions archive` does not support the parent `sessions` options --store, --all-agents; the gateway resolves target stores from each key and --agent.", + sessionsArchiveCommand, + ); }); it("rejects invalid lifecycle RPC timeouts", async () => { - await runCli(["sessions", "delete", "agent:main:scratch-1", "--timeout", "0", "--yes"]); - - expect(runtime.error).toHaveBeenCalledWith( + await expectSessionsRegistrationError( + ["sessions", "delete", "agent:main:scratch-1", "--timeout", "0", "--yes"], "--timeout must be a positive integer (milliseconds).", + sessionsDeleteCommand, ); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsDeleteCommand).not.toHaveBeenCalled(); }); it("forwards sessions list-side options", async () => { @@ -471,11 +561,11 @@ describe("registerStatusHealthSessionsCommands", () => { { flag: "--active", value: "5" }, { flag: "--limit", value: "1" }, ])("rejects inherited $flag before running session cleanup", async ({ flag, value }) => { - await runCli(["sessions", flag, value, "cleanup", "--enforce"]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining(flag)); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsCleanupCommand).not.toHaveBeenCalled(); + await expectSessionsRegistrationError( + ["sessions", flag, value, "cleanup", "--enforce"], + `\`sessions cleanup\` does not support the parent \`sessions\` option ${flag}; session-list filters cannot scope session maintenance.`, + sessionsCleanupCommand, + ); }); it("runs sessions tail with forwarded progress options", async () => { @@ -503,14 +593,6 @@ describe("registerStatusHealthSessionsCommands", () => { }); }); - it("rejects inherited JSON mode for human-readable session tail", async () => { - await runCli(["sessions", "--json", "tail"]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--json")); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(sessionsTailCommand).not.toHaveBeenCalled(); - }); - it("runs sessions export-trajectory with owner-routable export options", async () => { await runCli([ "sessions", @@ -551,16 +633,10 @@ describe("registerStatusHealthSessionsCommands", () => { }); it("rejects inherited all-agent scope for single-session trajectory exports", async () => { - await runCli([ - "sessions", - "--all-agents", - "export-trajectory", - "--session-key", - "agent:main:main", - ]); - - expect(runtime.error).toHaveBeenCalledWith(expect.stringContaining("--all-agents")); - expect(runtime.exit).toHaveBeenCalledWith(1); - expect(exportTrajectoryCommand).not.toHaveBeenCalled(); + await expectSessionsRegistrationError( + ["sessions", "--all-agents", "export-trajectory", "--session-key", "agent:main:main"], + "`sessions export-trajectory` does not support the parent `sessions` option --all-agents; trajectory export targets one session and cannot apply session-list filters.", + exportTrajectoryCommand, + ); }); }); diff --git a/src/cli/program/register.status-health-sessions.ts b/src/cli/program/register.status-health-sessions.ts index 43be022f0202..82ec044c4331 100644 --- a/src/cli/program/register.status-health-sessions.ts +++ b/src/cli/program/register.status-health-sessions.ts @@ -6,6 +6,7 @@ import { theme } from "../../../packages/terminal-core/src/theme.js"; import { setVerbose } from "../../globals.js"; import { defaultRuntime } from "../../runtime.js"; import { runCommandWithRuntime } from "../cli-utils.js"; +import { ExpectedCliError } from "../failure-output.js"; import { formatHelpExamples } from "../help-format.js"; import { registerTasksCommand } from "./register.tasks.js"; @@ -33,12 +34,16 @@ const SESSIONS_PARENT_OPTION_FLAGS = { limit: "--limit", } satisfies Record; +function throwSessionsCliError(message: string): never { + throw new ExpectedCliError({ message, humanOutput: message, machineOutput: message }); +} + function rejectUnsupportedSessionsParentOptions( subcommand: string, parentOpts: SessionsListCliOptions | undefined, unsupportedOptions: readonly (keyof SessionsListCliOptions)[], reason: string, -): boolean { +): void { const unsupportedFlags = unsupportedOptions .filter((option) => { const value = parentOpts?.[option]; @@ -46,14 +51,12 @@ function rejectUnsupportedSessionsParentOptions( }) .map((option) => SESSIONS_PARENT_OPTION_FLAGS[option]); if (unsupportedFlags.length === 0) { - return false; + return; } const plural = unsupportedFlags.length > 1 ? "options" : "option"; - defaultRuntime.error( + throwSessionsCliError( `\`sessions ${subcommand}\` does not support the parent \`sessions\` ${plural} ${unsupportedFlags.join(", ")}; ${reason}.`, ); - defaultRuntime.exit(1); - return true; } function addSessionsListOptions(command: Command): Command { @@ -161,21 +164,15 @@ function registerSessionsLifecycleCommand( ) .action(async (keys: string[], opts, actionCommand) => { const parentOpts = actionCommand.parent?.opts() as SessionsListCliOptions | undefined; - if ( - rejectUnsupportedSessionsParentOptions( - operation, - parentOpts, - ["store", "allAgents", "active", "limit", "verbose"], - "the gateway resolves target stores from each key and --agent", - ) - ) { - return; - } + rejectUnsupportedSessionsParentOptions( + operation, + parentOpts, + ["store", "allAgents", "active", "limit", "verbose"], + "the gateway resolves target stores from each key and --agent", + ); const timeoutMs = parseStrictPositiveInteger(opts.timeout); if (opts.timeout !== undefined && timeoutMs === undefined) { - defaultRuntime.error("--timeout must be a positive integer (milliseconds)."); - defaultRuntime.exit(1); - return; + throwSessionsCliError("--timeout must be a positive integer (milliseconds)."); } await runCommandWithRuntime(defaultRuntime, async () => { const lifecycleCommands = await import("../../commands/sessions-lifecycle.js"); @@ -371,16 +368,12 @@ export function registerStatusHealthSessionsCommands(program: Command) { ) .action(async (opts, command) => { const parentOpts = command.parent?.opts() as SessionsListCliOptions | undefined; - if ( - rejectUnsupportedSessionsParentOptions( - "cleanup", - parentOpts, - ["active", "limit", "verbose"], - "session-list filters cannot scope session maintenance", - ) - ) { - return; - } + rejectUnsupportedSessionsParentOptions( + "cleanup", + parentOpts, + ["active", "limit", "verbose"], + "session-list filters cannot scope session maintenance", + ); await runCommandWithRuntime(defaultRuntime, async () => { const { sessionsCleanupCommand } = await import("../../commands/sessions-cleanup.js"); await sessionsCleanupCommand( @@ -411,16 +404,12 @@ export function registerStatusHealthSessionsCommands(program: Command) { .option("--all-agents", "Aggregate sessions across all configured agents", false) .action(async (opts, command) => { const parentOpts = command.parent?.opts() as SessionsListCliOptions | undefined; - if ( - rejectUnsupportedSessionsParentOptions( - "tail", - parentOpts, - ["json", "active", "limit", "verbose"], - "trajectory tail emits human-readable progress and selects sessions separately", - ) - ) { - return; - } + rejectUnsupportedSessionsParentOptions( + "tail", + parentOpts, + ["json", "active", "limit", "verbose"], + "trajectory tail emits human-readable progress and selects sessions separately", + ); await runCommandWithRuntime(defaultRuntime, async () => { const { sessionsTailCommand } = await import("../../commands/sessions-tail.js"); await sessionsTailCommand( @@ -449,16 +438,12 @@ export function registerStatusHealthSessionsCommands(program: Command) { .option("--json", "Output JSON", false) .action(async (opts, command) => { const parentOpts = command.parent?.opts() as SessionsListCliOptions | undefined; - if ( - rejectUnsupportedSessionsParentOptions( - "export-trajectory", - parentOpts, - ["allAgents", "active", "limit", "verbose"], - "trajectory export targets one session and cannot apply session-list filters", - ) - ) { - return; - } + rejectUnsupportedSessionsParentOptions( + "export-trajectory", + parentOpts, + ["allAgents", "active", "limit", "verbose"], + "trajectory export targets one session and cannot apply session-list filters", + ); await runCommandWithRuntime(defaultRuntime, async () => { const { exportTrajectoryCommand } = await import("../../commands/export-trajectory.js"); await exportTrajectoryCommand( @@ -520,27 +505,19 @@ export function registerStatusHealthSessionsCommands(program: Command) { // believe they targeted one store while the gateway compacts another — so // reject any unsupported inherited option instead of ignoring it. const parentOpts = command.parent?.opts() as SessionsListCliOptions | undefined; - if ( - rejectUnsupportedSessionsParentOptions( - "compact", - parentOpts, - ["store", "allAgents", "active", "limit", "verbose"], - "the gateway resolves the target store from and --agent", - ) - ) { - return; - } + rejectUnsupportedSessionsParentOptions( + "compact", + parentOpts, + ["store", "allAgents", "active", "limit", "verbose"], + "the gateway resolves the target store from and --agent", + ); const maxLines = parseStrictPositiveInteger(opts.maxLines); if (opts.maxLines !== undefined && maxLines === undefined) { - defaultRuntime.error("--max-lines must be a positive integer."); - defaultRuntime.exit(1); - return; + throwSessionsCliError("--max-lines must be a positive integer."); } const timeoutMs = parseStrictPositiveInteger(opts.timeout); if (opts.timeout !== undefined && timeoutMs === undefined) { - defaultRuntime.error("--timeout must be a positive integer (milliseconds)."); - defaultRuntime.exit(1); - return; + throwSessionsCliError("--timeout must be a positive integer (milliseconds)."); } await runCommandWithRuntime(defaultRuntime, async () => { const { sessionsCompactCommand } = await import("../../commands/sessions-compact.js"); diff --git a/test/cli-json-stdout.e2e.test.ts b/test/cli-json-stdout.e2e.test.ts index 1622adbe608c..bdad276a44c4 100644 --- a/test/cli-json-stdout.e2e.test.ts +++ b/test/cli-json-stdout.e2e.test.ts @@ -331,6 +331,143 @@ describe("cli json stdout contract", () => { ); }); + it.each([ + { + name: "cleanup with an inherited filter in human mode", + args: ["sessions", "--active", "5", "cleanup"], + message: + "`sessions cleanup` does not support the parent `sessions` option --active; session-list filters cannot scope session maintenance.", + human: true, + }, + { + name: "cleanup inherited filter with leaf JSON", + args: ["sessions", "--active", "5", "cleanup", "--json"], + message: + "`sessions cleanup` does not support the parent `sessions` option --active; session-list filters cannot scope session maintenance.", + }, + { + name: "cleanup inherited limit with parent JSON", + args: ["sessions", "--json", "--limit", "1", "cleanup"], + message: + "`sessions cleanup` does not support the parent `sessions` option --limit; session-list filters cannot scope session maintenance.", + }, + { + name: "trajectory export inherited all-agent scope", + args: [ + "sessions", + "--all-agents", + "export-trajectory", + "--session-key", + "agent:main:main", + "--json", + ], + message: + "`sessions export-trajectory` does not support the parent `sessions` option --all-agents; trajectory export targets one session and cannot apply session-list filters.", + }, + { + name: "archive inherited store with leaf JSON", + args: ["sessions", "--store", "/tmp/other.sqlite", "archive", "agent:main:test", "--json"], + message: + "`sessions archive` does not support the parent `sessions` option --store; the gateway resolves target stores from each key and --agent.", + }, + { + name: "archive invalid timeout with parent JSON", + args: ["sessions", "--json", "archive", "agent:main:test", "--timeout", "0"], + message: "--timeout must be a positive integer (milliseconds).", + }, + { + name: "delete inherited all-agent scope", + args: ["sessions", "--all-agents", "delete", "agent:main:test", "--yes", "--json"], + message: + "`sessions delete` does not support the parent `sessions` option --all-agents; the gateway resolves target stores from each key and --agent.", + }, + { + name: "delete invalid timeout with leaf JSON", + args: ["sessions", "delete", "agent:main:test", "--timeout", "nope", "--yes", "--json"], + message: "--timeout must be a positive integer (milliseconds).", + }, + { + name: "compact inherited all-agent scope", + args: ["sessions", "--all-agents", "compact", "agent:main:test", "--json"], + message: + "`sessions compact` does not support the parent `sessions` option --all-agents; the gateway resolves the target store from and --agent.", + }, + { + name: "compact invalid max-lines with leaf JSON", + args: ["sessions", "compact", "agent:main:test", "--max-lines", "0", "--json"], + message: "--max-lines must be a positive integer.", + }, + { + name: "compact invalid timeout with parent JSON", + args: ["sessions", "--json", "compact", "agent:main:test", "--timeout", "0"], + message: "--timeout must be a positive integer (milliseconds).", + }, + { + name: "human-only tail rejecting inherited JSON", + args: ["sessions", "--json", "tail"], + message: + "`sessions tail` does not support the parent `sessions` option --json; trajectory tail emits human-readable progress and selects sessions separately.", + }, + { + name: "cleanup inherited filter through forced Commander", + args: ["sessions", "--active", "5", "cleanup", "--json"], + message: + "`sessions cleanup` does not support the parent `sessions` option --active; session-list filters cannot scope session maintenance.", + commander: true, + }, + { + name: "compact invalid max-lines through dual-TTY finalization", + args: ["sessions", "compact", "agent:main:test", "--max-lines", "0", "--json"], + message: "--max-lines must be a positive integer.", + tty: true, + }, + ])("renders sessions registration validation failures for $name", async (testCase) => { + await withTempHome( + async (tempHome) => { + const preload = Buffer.from( + [ + 'import net from "node:net";', + 'net.Socket.prototype.connect = function () { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };', + 'globalThis.fetch = async () => { throw new Error("AUTOQA_NETWORK_FORBIDDEN"); };', + ...("tty" in testCase + ? [ + 'Object.defineProperty(process.stdout, "isTTY", { value: true, configurable: true });', + 'Object.defineProperty(process.stderr, "isTTY", { value: true, configurable: true });', + ] + : []), + ].join("\n"), + ).toString("base64"); + const result = runBuiltCli(tempHome, testCase.args, { + NODE_OPTIONS: `--import=data:text/javascript;base64,${preload}`, + OPENCLAW_CONFIG_PATH: path.join(tempHome, "missing-openclaw.json"), + OPENCLAW_GATEWAY_PORT: "29791", + OPENCLAW_STATE_DIR: path.join(tempHome, "isolated-state"), + ...("commander" in testCase ? { OPENCLAW_DISABLE_ROUTE_FIRST: "1" } : {}), + ...("tty" in testCase ? { FORCE_COLOR: "1" } : {}), + }); + + expect(result.status, result.stderr).toBe(1); + expect(result.stdout, result.stderr).not.toContain("\u001B"); + expect(result.stdout, result.stderr).not.toContain("\u0007"); + if ("human" in testCase) { + expect(result.stdout).toBe(""); + } else { + expect(JSON.parse(result.stdout)).toEqual({ + ok: false, + error: { type: "cli_error", message: testCase.message }, + }); + } + expect(result.stderr).toContain(testCase.message); + expect(result.stderr.split(testCase.message)).toHaveLength(2); + expect(result.stderr).not.toContain("AUTOQA_NETWORK_FORBIDDEN"); + if ("tty" in testCase) { + expect(result.stderr).toContain("\u001B[?25h"); + } + }, + { prefix: "openclaw-sessions-registration-json-failure-e2e-" }, + ); + }); + it.each([ { name: "account validation in human mode",