mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-27 04:47:03 -06:00
fix(cli): render session validation JSON failures (#128224)
This commit is contained in:
committed by
GitHub
parent
4bb0c47b28
commit
7d154d32c1
@@ -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");
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user