// Exercise Commander, auth dispatch, and actual owner resolution with local I/O seams. import { Command } from "commander"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { ChannelPluginCatalogEntry } from "../channels/plugins/catalog.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { registerChannelsCli } from "./channels-cli.js"; const fixture = vi.hoisted(() => ({ config: {} as OpenClawConfig, registered: true, login: vi.fn(), logout: vi.fn(async () => ({ cleared: false })), catalog: vi.fn<(...args: unknown[]) => ChannelPluginCatalogEntry[]>(() => []), loadScoped: vi.fn(), runtime: { log: vi.fn(), error: vi.fn(), exit: vi.fn() }, })); vi.mock("../config/config.js", () => ({ getRuntimeConfig: () => fixture.config })); vi.mock("../commands/config-validation.js", () => ({ requireValidConfigForWrite: async () => ({ snapshot: { sourceConfig: fixture.config, hash: "fixture" }, writeOptions: {}, }), })); vi.mock("../config/plugin-auto-enable.js", () => ({ applyPluginAutoEnable: ({ config }: { config: OpenClawConfig }) => ({ config, changes: [] }), })); vi.mock("../channels/plugins/catalog.js", () => ({ listRawChannelPluginCatalogEntries: fixture.catalog, getChannelPluginCatalogEntry: () => undefined, })); vi.mock("../channels/plugins/index.js", () => ({ normalizeChannelId: (id: string) => id, getLoadedChannelPlugin: () => (fixture.registered ? plugin : undefined), listChannelPlugins: () => [plugin], })); vi.mock("../commands/channel-setup/plugin-install.js", () => ({ loadChannelSetupPluginRegistrySnapshotForChannel: fixture.loadScoped, ensureChannelSetupPluginInstalled: vi.fn(), })); vi.mock("../gateway/call.js", () => ({ callGateway: async () => { throw new Error("isolated fixture has no Gateway"); }, })); vi.mock("../runtime.js", () => ({ defaultRuntime: fixture.runtime, ExitError: class extends Error {}, })); const plugin = { id: "fixture-chat", auth: { login: fixture.login }, gateway: { logoutAccount: fixture.logout }, config: { listAccountIds: () => ["work"], resolveAccount: () => ({ accountId: "work" }), }, }; async function runAuth(mode: string, parent: string[] = [], leaf: string[] = []) { const args = ["channels", ...parent, mode, ...leaf, "--channel", plugin.id, "--account", "work"]; const program = new Command() .name("openclaw") .enablePositionalOptions() .exitOverride() .configureOutput({ writeErr: () => undefined }); await registerChannelsCli(program, ["node", "openclaw", ...args]); await program.parseAsync(args, { from: "user" }); } describe.each(["login", "logout"])("channels %s owner", (mode) => { beforeEach(() => { vi.clearAllMocks(); fixture.registered = true; fixture.config = { agents: { ownership: "explicit", entries: { research: { workspace: "/tmp/research-workspace" }, ops: { workspace: "/tmp/ops-workspace" }, }, }, }; fixture.catalog.mockReturnValue([ { id: plugin.id, pluginId: plugin.id, origin: "bundled", meta: { id: plugin.id, label: "Fixture", selectionLabel: "Fixture", docsPath: "", blurb: "", }, install: { npmSpec: "fixture-chat" }, }, ]); fixture.loadScoped.mockReturnValue({ channels: [{ plugin }], channelSetups: [] }); }); it.each([ { name: "parent", parent: ["--agent", "ops"], leaf: [], owner: "ops" }, { name: "leaf", parent: [], leaf: ["--agent", "ops"], owner: "ops" }, { name: "leaf overrides parent", parent: ["--agent", "research"], leaf: ["--agent", "ops"], owner: "ops", }, { name: "System Agent", parent: [], leaf: [], owner: "research" }, { name: "trimmed explicit", parent: ["--agent", " ops "], leaf: [], owner: "ops" }, { name: "explicit before System Agent", parent: ["--agent", "ops"], leaf: [], owner: "ops" }, ])("retains the $name owner through plugin discovery", async ({ parent, leaf, owner, name }) => { if (name === "System Agent") { fixture.config.agents!.defaults = { systemAgent: { agentId: "research" } }; } else if (name === "explicit before System Agent") { fixture.config.agents!.defaults = { systemAgent: { agentId: "???" } }; } fixture.registered = false; await runAuth(mode, parent, leaf); expect(fixture.runtime.error).not.toHaveBeenCalled(); expect(fixture.loadScoped).toHaveBeenCalledWith( expect.objectContaining({ workspaceDir: `/tmp/${owner}-workspace` }), ); expect(mode === "login" ? fixture.login : fixture.logout).toHaveBeenCalledWith( expect.objectContaining({ accountId: "work" }), ); }); it("does not require discovery to reload an already registered plugin", async () => { await runAuth(mode, ["--agent", "ops"]); expect(fixture.runtime.error).not.toHaveBeenCalled(); expect(fixture.catalog).toHaveBeenCalledWith( expect.objectContaining({ workspaceDir: "/tmp/ops-workspace" }), ); expect(fixture.loadScoped).not.toHaveBeenCalled(); expect(mode === "login" ? fixture.login : fixture.logout).toHaveBeenCalled(); }); it.each([undefined, "missing"])( "keeps an ownerless fleet from auth with designation %s", async (agentId) => { fixture.config.agents!.defaults = { systemAgent: { agentId } }; // Agent selection is an expected CLI condition rendered by the root failure // owner; the command rethrows instead of printing its own copy. await expect(runAuth(mode)).rejects.toMatchObject({ name: "AgentSelectionRequiredError", message: expect.stringContaining("no explicit owner"), }); expect(fixture.runtime.error).not.toHaveBeenCalled(); expect(fixture.runtime.exit).not.toHaveBeenCalled(); expect(fixture.catalog).not.toHaveBeenCalled(); expect(fixture.loadScoped).not.toHaveBeenCalled(); expect(fixture.login).not.toHaveBeenCalled(); expect(fixture.logout).not.toHaveBeenCalled(); }, ); it.each([ { agent: "missing", error: 'Unknown agent id "missing"' }, { agent: " ", error: "--agent must not be blank" }, ])("rejects invalid explicit agent '$agent' before discovery", async ({ agent, error }) => { fixture.config.agents!.defaults = { systemAgent: { agentId: "research" } }; await runAuth(mode, ["--agent", agent]); expect(fixture.runtime.error).toHaveBeenCalledWith(expect.stringContaining(error)); expect(fixture.catalog).not.toHaveBeenCalled(); expect(fixture.login).not.toHaveBeenCalled(); expect(fixture.logout).not.toHaveBeenCalled(); }); it.each(["???", "OPS"])( "rejects explicit owner %s instead of normalizing it into another agent", async (agent) => { fixture.config.agents!.entries = { main: { workspace: "/tmp/main-workspace" }, ops: { workspace: "/tmp/ops-workspace" }, }; await runAuth(mode, ["--agent", agent]); expect(fixture.runtime.error).toHaveBeenCalledWith( expect.stringContaining(`Unknown agent id "${agent}"`), ); expect(fixture.runtime.exit).toHaveBeenCalledWith(1); expect(fixture.catalog).not.toHaveBeenCalled(); expect(fixture.login).not.toHaveBeenCalled(); expect(fixture.logout).not.toHaveBeenCalled(); }, ); it("keeps the legacy owner when the configured System Agent is unused", async () => { fixture.config.agents!.ownership = undefined; fixture.config.agents!.entries!.research!.default = true; fixture.config.agents!.defaults = { systemAgent: { agentId: "???" } }; await runAuth(mode); expect(fixture.runtime.error).not.toHaveBeenCalled(); expect(fixture.catalog).toHaveBeenCalledWith( expect.objectContaining({ workspaceDir: "/tmp/research-workspace" }), ); expect(mode === "login" ? fixture.login : fixture.logout).toHaveBeenCalled(); }); });