fix(cli): render session validation JSON failures (#128224)

This commit is contained in:
Peter Steinberger
2026-08-23 09:43:52 -07:00
committed by GitHub
parent 4bb0c47b28
commit 7d154d32c1
3 changed files with 331 additions and 141 deletions
@@ -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 <key> 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 <key> 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 <key> 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,
);
});
});
@@ -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<keyof SessionsListCliOptions, string>;
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 <key> and --agent",
)
) {
return;
}
rejectUnsupportedSessionsParentOptions(
"compact",
parentOpts,
["store", "allAgents", "active", "limit", "verbose"],
"the gateway resolves the target store from <key> 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");
+137
View File
@@ -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 <key> 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",