diff --git a/packages/cli/__tests__/commands/spawn.test.ts b/packages/cli/__tests__/commands/spawn.test.ts index 594f2de86..a422bb06c 100644 --- a/packages/cli/__tests__/commands/spawn.test.ts +++ b/packages/cli/__tests__/commands/spawn.test.ts @@ -234,6 +234,62 @@ describe("spawn command", () => { expect(output).toContain("8474d6f29887-app-7"); }); + it("passes --agent flag to sessionManager.spawn()", async () => { + const fakeSession: Session = { + id: "app-1", + projectId: "my-app", + status: "spawning", + activity: null, + branch: null, + issueId: null, + pr: null, + workspacePath: "/tmp/wt", + runtimeHandle: { id: "hash-app-1", runtimeName: "tmux", data: {} }, + agentInfo: null, + createdAt: new Date(), + lastActivityAt: new Date(), + metadata: {}, + }; + + mockSessionManager.spawn.mockResolvedValue(fakeSession); + + await program.parseAsync(["node", "test", "spawn", "my-app", "--agent", "codex"]); + + expect(mockSessionManager.spawn).toHaveBeenCalledWith({ + projectId: "my-app", + issueId: undefined, + agent: "codex", + }); + }); + + it("passes --agent flag with issue ID", async () => { + const fakeSession: Session = { + id: "app-1", + projectId: "my-app", + status: "spawning", + activity: null, + branch: "feat/INT-42", + issueId: "INT-42", + pr: null, + workspacePath: "/tmp/wt", + runtimeHandle: { id: "hash-app-1", runtimeName: "tmux", data: {} }, + agentInfo: null, + createdAt: new Date(), + lastActivityAt: new Date(), + metadata: {}, + }; + + mockSessionManager.spawn.mockResolvedValue(fakeSession); + + await program.parseAsync(["node", "test", "spawn", "my-app", "INT-42", "--agent", "codex"]); + + expect(mockSessionManager.spawn).toHaveBeenCalledWith({ + projectId: "my-app", + issueId: "INT-42", + agent: "codex", + }); + }); + it("rejects unknown project ID", async () => { await expect( program.parseAsync(["node", "test", "spawn", "nonexistent"]), diff --git a/packages/core/src/__tests__/plugin-registry.test.ts b/packages/core/src/__tests__/plugin-registry.test.ts index 0fc85c619..e0132c4eb 100644 --- a/packages/core/src/__tests__/plugin-registry.test.ts +++ b/packages/core/src/__tests__/plugin-registry.test.ts @@ -140,6 +140,26 @@ describe("loadBuiltins", () => { // In the test environment, most are not resolvable — should not throw. await expect(registry.loadBuiltins()).resolves.toBeUndefined(); }); + + it("registers multiple agent plugins from importFn", async () => { + const registry = createPluginRegistry(); + + const fakeClaudeCode = makePlugin("agent", "claude-code"); + const fakeCodex = makePlugin("agent", "codex"); + + await registry.loadBuiltins(undefined, async (pkg: string) => { + if (pkg === "@composio/ao-plugin-agent-claude-code") return fakeClaudeCode; + if (pkg === "@composio/ao-plugin-agent-codex") return fakeCodex; + throw new Error(`Not found: ${pkg}`); + }); + + const agents = registry.list("agent"); + expect(agents).toContainEqual(expect.objectContaining({ name: "claude-code", slot: "agent" })); + expect(agents).toContainEqual(expect.objectContaining({ name: "codex", slot: "agent" })); + + expect(registry.get("agent", "codex")).not.toBeNull(); + expect(registry.get("agent", "claude-code")).not.toBeNull(); + }); }); describe("extractPluginConfig (via register with config)", () => { diff --git a/packages/core/src/__tests__/session-manager.test.ts b/packages/core/src/__tests__/session-manager.test.ts index 96d56edff..424404bcf 100644 --- a/packages/core/src/__tests__/session-manager.test.ts +++ b/packages/core/src/__tests__/session-manager.test.ts @@ -227,6 +227,64 @@ describe("spawn", () => { await expect(sm.spawn({ projectId: "my-app" })).rejects.toThrow("not found"); }); + describe("agent override", () => { + let mockCodexAgent: Agent; + let registryWithMultipleAgents: PluginRegistry; + + beforeEach(() => { + mockCodexAgent = { + name: "codex", + processName: "codex", + getLaunchCommand: vi.fn().mockReturnValue("codex --start"), + getEnvironment: vi.fn().mockReturnValue({ CODEX_VAR: "1" }), + detectActivity: vi.fn().mockReturnValue("active"), + getActivityState: vi.fn().mockResolvedValue(null), + isProcessRunning: vi.fn().mockResolvedValue(true), + getSessionInfo: vi.fn().mockResolvedValue(null), + }; + + registryWithMultipleAgents = { + ...mockRegistry, + get: vi.fn().mockImplementation((slot: string, name: string) => { + if (slot === "runtime") return mockRuntime; + if (slot === "agent") { + if (name === "mock-agent") return mockAgent; + if (name === "codex") return mockCodexAgent; + return null; + } + if (slot === "workspace") return mockWorkspace; + return null; + }), + }; + }); + + it("uses overridden agent when spawnConfig.agent is provided", async () => { + const sm = createSessionManager({ config, registry: registryWithMultipleAgents }); + + await sm.spawn({ projectId: "my-app", agent: "codex" }); + + expect(mockCodexAgent.getLaunchCommand).toHaveBeenCalled(); + expect(mockAgent.getLaunchCommand).not.toHaveBeenCalled(); + }); + + it("throws when agent override plugin is not found", async () => { + const sm = createSessionManager({ config, registry: registryWithMultipleAgents }); + + await expect( + sm.spawn({ projectId: "my-app", agent: "nonexistent" }), + ).rejects.toThrow("Agent plugin 'nonexistent' not found"); + }); + + it("uses default agent when no override specified", async () => { + const sm = createSessionManager({ config, registry: registryWithMultipleAgents }); + + await sm.spawn({ projectId: "my-app" }); + + expect(mockAgent.getLaunchCommand).toHaveBeenCalled(); + expect(mockCodexAgent.getLaunchCommand).not.toHaveBeenCalled(); + }); + }); + it("validates issue exists when issueId provided", async () => { const mockTracker: Tracker = { name: "mock-tracker",