// Register agent tests cover agent command registration and option wiring. import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { registerAgentTurnCommand } from "./register.agent-turn.js"; import { registerAgentsCommands } from "./register.agent.js"; const mocks = vi.hoisted(() => ({ agentCliCommandMock: vi.fn(), agentExecCommandMock: vi.fn(), agentsAddCommandMock: vi.fn(), agentsTeamCreateCommandMock: vi.fn(), agentsBindingsCommandMock: vi.fn(), agentsBindCommandMock: vi.fn(), agentsDeleteCommandMock: vi.fn(), agentsListCommandMock: vi.fn(), agentsSetIdentityCommandMock: vi.fn(), agentsUnbindCommandMock: vi.fn(), requestExitAfterOneShotOutputMock: vi.fn(), setVerboseMock: vi.fn(), runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn(), }, })); const agentCliCommandMock = mocks.agentCliCommandMock; const agentExecCommandMock = mocks.agentExecCommandMock; const agentsAddCommandMock = mocks.agentsAddCommandMock; const agentsBindingsCommandMock = mocks.agentsBindingsCommandMock; const agentsBindCommandMock = mocks.agentsBindCommandMock; const agentsDeleteCommandMock = mocks.agentsDeleteCommandMock; const agentsListCommandMock = mocks.agentsListCommandMock; const agentsSetIdentityCommandMock = mocks.agentsSetIdentityCommandMock; const agentsUnbindCommandMock = mocks.agentsUnbindCommandMock; const requestExitAfterOneShotOutputMock = mocks.requestExitAfterOneShotOutputMock; const setVerboseMock = mocks.setVerboseMock; const runtime = mocks.runtime; vi.mock("../../commands/agent-via-gateway.js", () => ({ agentCliCommand: mocks.agentCliCommandMock, })); vi.mock("../../commands/agent-exec.js", () => ({ agentExecCommand: mocks.agentExecCommandMock, })); vi.mock("../../commands/agents.commands.add.js", () => ({ agentsAddCommand: mocks.agentsAddCommandMock, })); vi.mock("../../commands/agents.commands.team.js", () => ({ agentsTeamCreateCommand: mocks.agentsTeamCreateCommandMock, })); vi.mock("../../commands/agents.commands.bind.js", () => ({ agentsBindingsCommand: mocks.agentsBindingsCommandMock, agentsBindCommand: mocks.agentsBindCommandMock, agentsUnbindCommand: mocks.agentsUnbindCommandMock, })); vi.mock("../../commands/agents.commands.delete.js", () => ({ agentsDeleteCommand: mocks.agentsDeleteCommandMock, })); vi.mock("../../commands/agents.commands.identity.js", () => ({ agentsSetIdentityCommand: mocks.agentsSetIdentityCommandMock, })); vi.mock("../../commands/agents.commands.list.js", () => ({ agentsListCommand: mocks.agentsListCommandMock, })); vi.mock("../../global-state.js", () => ({ setVerbose: mocks.setVerboseMock, })); vi.mock("../../runtime.js", async (importOriginal) => ({ ...(await importOriginal()), defaultRuntime: mocks.runtime, })); vi.mock("../one-shot-exit.js", () => ({ requestExitAfterOneShotOutput: mocks.requestExitAfterOneShotOutputMock, })); describe("agent command registration", () => { async function runCli(args: string[]) { const program = new Command().enablePositionalOptions(); registerAgentTurnCommand(program, { agentChannelOptions: "last|telegram|discord" }); registerAgentsCommands(program); await program.parseAsync(args, { from: "user" }); } beforeEach(() => { vi.clearAllMocks(); runtime.exit.mockImplementation(() => {}); agentCliCommandMock.mockResolvedValue(undefined); agentExecCommandMock.mockResolvedValue({ exitCode: 0 }); agentsAddCommandMock.mockResolvedValue(undefined); agentsBindingsCommandMock.mockResolvedValue(undefined); agentsBindCommandMock.mockResolvedValue(undefined); agentsDeleteCommandMock.mockResolvedValue(undefined); agentsListCommandMock.mockResolvedValue(undefined); agentsSetIdentityCommandMock.mockResolvedValue(undefined); agentsUnbindCommandMock.mockResolvedValue(undefined); }); function commandCall(mock: { mock: { calls: unknown[][] } }, index = 0): unknown[] { const call = mock.mock.calls[index]; if (!call) { throw new Error(`expected command call ${index + 1}`); } return call; } it("keeps agent help aligned with supported thinking levels and auth sources", () => { const program = new Command(); registerAgentTurnCommand(program, { agentChannelOptions: "last|telegram|discord" }); const agent = program.commands.find((command) => command.name() === "agent"); const exec = agent?.commands.find((command) => command.name() === "exec"); expect(agent?.options.find((option) => option.long === "--thinking")?.description).toContain( "ultra", ); expect(exec?.options.find((option) => option.long === "--thinking")?.description).toContain( "ultra", ); expect(agent?.options.find((option) => option.long === "--local")?.description).toContain( "configured provider credentials or local CLI logins", ); }); it("runs agent command with verbose enabled for --verbose on", async () => { await runCli(["agent", "--message", "hi", "--verbose", "ON", "--json"]); expect(setVerboseMock).toHaveBeenCalledWith(true); const [options, callRuntime, deps] = commandCall(agentCliCommandMock); expect((options as { message?: string }).message).toBe("hi"); expect((options as { verbose?: string }).verbose).toBe("ON"); expect((options as { json?: boolean }).json).toBe(true); expect(callRuntime).toBe(runtime); expect(deps).toBeUndefined(); }); it("runs agent command with verbose disabled for --verbose off", async () => { await runCli(["agent", "--message", "hi", "--verbose", "off"]); expect(setVerboseMock).toHaveBeenCalledWith(false); const [options, callRuntime, deps] = commandCall(agentCliCommandMock); expect((options as { message?: string }).message).toBe("hi"); expect((options as { verbose?: string }).verbose).toBe("off"); expect(callRuntime).toBe(runtime); expect(deps).toBeUndefined(); }); it("forwards a message file to the agent command", async () => { await runCli(["agent", "--message-file", "task.md", "--agent", "ops"]); const [options, callRuntime, deps] = commandCall(agentCliCommandMock); expect((options as { message?: string }).message).toBeUndefined(); expect((options as { messageFile?: string }).messageFile).toBe("task.md"); expect((options as { agent?: string }).agent).toBe("ops"); expect(callRuntime).toBe(runtime); expect(deps).toBeUndefined(); }); it("accepts a model override for one-shot agent runs", async () => { await runCli(["agent", "--message", "hi", "--agent", "ops", "--model", "openai/gpt-5.4"]); const [options, callRuntime, deps] = commandCall(agentCliCommandMock); expect((options as { message?: string }).message).toBe("hi"); expect((options as { agent?: string }).agent).toBe("ops"); expect((options as { model?: string }).model).toBe("openai/gpt-5.4"); expect(callRuntime).toBe(runtime); expect(deps).toBeUndefined(); }); it("forwards an explicit session key to the agent command", async () => { await runCli(["agent", "--message", "hi", "--session-key", "agent:ops:incident-42"]); const [options, callRuntime, deps] = commandCall(agentCliCommandMock); expect((options as { message?: string }).message).toBe("hi"); expect((options as { sessionKey?: string }).sessionKey).toBe("agent:ops:incident-42"); expect(callRuntime).toBe(runtime); expect(deps).toBeUndefined(); }); it.each([0, 1])("keeps bare agent on the parent action with exit code %i", async (exitCode) => { const previousExitCode = process.exitCode; agentCliCommandMock.mockImplementationOnce(async () => { process.exitCode = exitCode; }); try { await runCli(["agent", "--message", "hi", "--agent", "ops"]); expect(agentCliCommandMock).toHaveBeenCalledTimes(1); expect(agentExecCommandMock).not.toHaveBeenCalled(); expect(requestExitAfterOneShotOutputMock).toHaveBeenCalledWith(runtime); expect(process.exitCode).toBe(exitCode); } finally { process.exitCode = previousExitCode; } }); it("keeps an exec-valued parent message on the existing parent action", async () => { await runCli(["agent", "--message", "exec", "--agent", "ops"]); expect(agentCliCommandMock).toHaveBeenCalledTimes(1); expect(agentExecCommandMock).not.toHaveBeenCalled(); }); it("runs the nested headless exec command with repeatable fallbacks", async () => { await runCli([ "agent", "exec", "fix it", "--cwd", "/tmp/project", "--model", "openai/gpt-5.6-luna", "--code-mode", "code", "--local-model-lean", "--fallback", "anthropic/claude-sonnet-4-6", "--fallback", "google/gemini-3.1-pro-preview", "--json", ]); expect(agentCliCommandMock).not.toHaveBeenCalled(); expect(agentExecCommandMock).toHaveBeenCalledWith( "fix it", expect.objectContaining({ cwd: "/tmp/project", model: "openai/gpt-5.6-luna", codeMode: "code", localModelLean: true, fallback: ["anthropic/claude-sonnet-4-6", "google/gemini-3.1-pro-preview"], // Stored credentials are the default so exec reaches the same logins as // the rest of the CLI; --auth-env-only is the opt-in restriction. authEnvOnly: false, isolated: false, timeout: "600", json: true, }), runtime, ); }); it("restricts credentials and config to the process environment with --auth-env-only", async () => { await runCli(["agent", "exec", "fix it", "--auth-env-only"]); expect(agentExecCommandMock).toHaveBeenCalledWith( "fix it", expect.objectContaining({ authEnvOnly: true }), runtime, ); }); it("forwards the pinned-config and isolated run flags", async () => { await runCli(["agent", "exec", "fix it", "--config", "/tmp/ci.json", "--isolated"]); expect(agentExecCommandMock).toHaveBeenCalledWith( "fix it", expect.objectContaining({ config: "/tmp/ci.json", isolated: true }), runtime, ); }); it("accepts parent options before the nested exec command", async () => { await runCli(["agent", "--model", "openai/gpt-5.6-luna", "exec", "fix it", "--json"]); expect(agentCliCommandMock).not.toHaveBeenCalled(); expect(agentExecCommandMock).toHaveBeenCalledWith( "fix it", expect.objectContaining({ model: "openai/gpt-5.6-luna", json: true }), runtime, ); }); it("resolves nested exec --timeout from the explicit leaf, then the parent, then the default", async () => { await runCli(["agent", "exec", "fix it", "--timeout", "120"]); expect(agentExecCommandMock).toHaveBeenLastCalledWith( "fix it", expect.objectContaining({ timeout: "120" }), runtime, ); await runCli(["agent", "--timeout", "30", "exec", "fix it"]); expect(agentExecCommandMock).toHaveBeenLastCalledWith( "fix it", expect.objectContaining({ timeout: "30" }), runtime, ); await runCli(["agent", "--timeout", "30", "exec", "fix it", "--timeout", "120"]); expect(agentExecCommandMock).toHaveBeenLastCalledWith( "fix it", expect.objectContaining({ timeout: "120" }), runtime, ); }); it("runs agents add and detects explicit automation options", async () => { await runCli(["agents", "add", "alpha"]); const [alphaOptions, alphaRuntime, alphaFlags] = commandCall(agentsAddCommandMock, 0); expect((alphaOptions as { name?: string }).name).toBe("alpha"); expect((alphaOptions as { workspace?: string }).workspace).toBeUndefined(); expect((alphaOptions as { bind?: string[] }).bind).toEqual([]); expect(alphaRuntime).toBe(runtime); expect(alphaFlags).toEqual({ hasAutomationFlags: false }); await runCli([ "agents", "add", "beta", "--workspace", "/tmp/ws", "--bind", "telegram", "--bind", "discord:acct", "--non-interactive", "--json", ]); const [betaOptions, betaRuntime, betaFlags] = commandCall(agentsAddCommandMock, 1); expect((betaOptions as { name?: string }).name).toBe("beta"); expect((betaOptions as { workspace?: string }).workspace).toBe("/tmp/ws"); expect((betaOptions as { bind?: string[] }).bind).toEqual(["telegram", "discord:acct"]); expect((betaOptions as { nonInteractive?: boolean }).nonInteractive).toBe(true); expect((betaOptions as { json?: boolean }).json).toBe(true); expect(betaRuntime).toBe(runtime); expect(betaFlags).toEqual({ hasAutomationFlags: true }); }); it("keeps JSON-only agent creation in wizard mode", async () => { await runCli(["agents", "add", "alpha", "--json"]); const [options, callRuntime, flags] = commandCall(agentsAddCommandMock); expect(options).toEqual( expect.objectContaining({ name: "alpha", json: true, nonInteractive: false }), ); expect(callRuntime).toBe(runtime); expect(flags).toEqual({ hasAutomationFlags: false }); }); it("keeps role creation interactive unless automation flags are explicit", async () => { await runCli(["agents", "add", "editor", "--role", "writer", "--json"]); expect(agentsAddCommandMock).toHaveBeenLastCalledWith( expect.objectContaining({ name: "editor", role: "writer", json: true, nonInteractive: false, }), runtime, { hasAutomationFlags: false }, ); await runCli(["agents", "add", "editor", "--role", "writer", "--non-interactive"]); expect(agentsAddCommandMock).toHaveBeenLastCalledWith( expect.objectContaining({ role: "writer", nonInteractive: true }), runtime, { hasAutomationFlags: true }, ); }); it("parses team creation and forwards explicit coordinator and workspace choices", async () => { await runCli([ "agents", "team", "create", "--preset", "team", "--coordinator", "lead", "--prefix", "docs", "--workspace-root", "/tmp/team", "--non-interactive", "--json", ]); expect(mocks.agentsTeamCreateCommandMock).toHaveBeenCalledWith( { preset: "team", coordinator: "lead", prefix: "docs", workspaceRoot: "/tmp/team", nonInteractive: true, json: true, }, runtime, ); expect(agentsListCommandMock).not.toHaveBeenCalled(); }); it("runs agents list when root agents command is invoked", async () => { await runCli(["agents"]); expect(agentsListCommandMock).toHaveBeenCalledWith({}, runtime); }); it("forwards agents list options", async () => { await runCli(["agents", "list", "--json", "--bindings", "--tree"]); expect(agentsListCommandMock).toHaveBeenCalledWith( { json: true, bindings: true, tree: true, }, runtime, ); }); it("forwards agents bindings options", async () => { await runCli(["agents", "bindings", "--agent", "ops", "--json"]); expect(agentsBindingsCommandMock).toHaveBeenCalledWith( { agent: "ops", json: true, }, runtime, ); }); it("forwards agents bind options", async () => { await runCli([ "agents", "bind", "--agent", "ops", "--bind", "matrix:ops", "--bind", "telegram", "--json", ]); expect(agentsBindCommandMock).toHaveBeenCalledWith( { agent: "ops", bind: ["matrix:ops", "telegram"], json: true, }, runtime, ); }); it("documents set-identity --workspace as a locator", () => { const program = new Command(); registerAgentsCommands(program); const agents = program.commands.find((command) => command.name() === "agents"); const setIdentity = agents?.commands.find((command) => command.name() === "set-identity"); const help = setIdentity?.helpInformation() ?? ""; expect(help.replace(/\s+/g, " ")).toContain("does not change the stored workspace"); }); it("documents bind accountId resolution behavior in help text", () => { const program = new Command(); registerAgentsCommands(program); const agents = program.commands.find((command) => command.name() === "agents"); const bind = agents?.commands.find((command) => command.name() === "bind"); const help = bind?.helpInformation() ?? ""; expect(help).toContain("accountId is resolved by channel defaults/hooks"); }); it("forwards agents unbind options", async () => { await runCli(["agents", "unbind", "--agent", "ops", "--all", "--json"]); expect(agentsUnbindCommandMock).toHaveBeenCalledWith( { agent: "ops", bind: [], all: true, json: true, }, runtime, ); }); it("forwards agents delete options", async () => { await runCli(["agents", "delete", "worker-a", "--force", "--json"]); const [options, callRuntime] = commandCall(agentsDeleteCommandMock); expect((options as { id?: string }).id).toBe("worker-a"); expect((options as { force?: boolean }).force).toBe(true); expect((options as { json?: boolean }).json).toBe(true); expect(callRuntime).toBe(runtime); }); it("forwards set-identity options", async () => { await runCli([ "agents", "set-identity", "--agent", "main", "--workspace", "/tmp/ws", "--identity-file", "/tmp/ws/IDENTITY.md", "--from-identity", "--name", "OpenClaw", "--theme", "ops", "--emoji", ":lobster:", "--avatar", "https://example.com/openclaw.png", "--json", ]); expect(agentsSetIdentityCommandMock).toHaveBeenCalledWith( { agent: "main", workspace: "/tmp/ws", identityFile: "/tmp/ws/IDENTITY.md", fromIdentity: true, name: "OpenClaw", theme: "ops", emoji: ":lobster:", avatar: "https://example.com/openclaw.png", json: true, }, runtime, ); }); it("reports errors via runtime when a command fails", async () => { agentsListCommandMock.mockRejectedValueOnce(new Error("list failed")); await runCli(["agents"]); expect(runtime.error).toHaveBeenCalledWith("list failed"); expect(runtime.exit).toHaveBeenCalledWith(1); }); it.each([ { label: "human", args: ["agent", "--message", "hello"] }, { label: "JSON", args: ["agent", "--message", "hello", "--json"] }, ])( "renders gateway request errors without internal class names in $label mode", async ({ args }) => { const message = "The selected model was not found by the provider. Check the model id or choose a different model."; const error = Object.assign(new Error(message), { name: "GatewayClientRequestError", code: "UNAVAILABLE", gatewayCode: "UNAVAILABLE", details: { reason: "model_not_found" }, }); agentCliCommandMock.mockRejectedValueOnce(error); await runCli(args); expect(runtime.error).toHaveBeenCalledWith(message); expect(runtime.error).not.toHaveBeenCalledWith(expect.stringContaining("Error:")); expect(runtime.exit).toHaveBeenCalledWith(1); }, ); });