Files
kandev/apps/web/app/settings/agents/custom-tui-mcp-mount.test.tsx
T
João Manso f2d52f1826 feat(agents): let a custom agent speak ACP (#3863)
Co-authored-by: Joao Manso <X363081@bcpcorp.net>
Co-authored-by: Carlos Florencio <carlosmflorencio@gmail.com>
2026-09-23 11:01:32 +01:00

155 lines
5.2 KiB
TypeScript

import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { afterEach, describe, expect, it, vi } from "vitest";
import { StateProvider } from "@/components/state-provider";
import { ToastProvider } from "@/components/toast-provider";
import { ApiError, INTERIM_SETTINGS_INTERLOCK_ERROR_CODE } from "@/lib/api/client";
import type { Agent } from "@/lib/types/http-agents";
const { updateMcpStrategy, toast } = vi.hoisted(() => ({
updateMcpStrategy: vi.fn(),
toast: vi.fn(),
}));
// The strategy list is fetched on mount; the mount assertion below is about
// whether the control is reachable at all, not about the option list.
vi.mock("@/lib/api/domains/settings-api", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
listMCPStrategies: vi.fn(async () => [{ key: "claude", description: "a config file" }]),
updateCustomTUIAgentMCPStrategy: updateMcpStrategy,
}));
vi.mock("@/components/toast-provider", async (importOriginal) => ({
...(await importOriginal<Record<string, unknown>>()),
useToast: () => ({ toast }),
}));
const { CustomTUIMcpCard } = await import("@/components/settings/custom-tui-mcp-card");
const { InstalledAgentCard } = await import("@/components/settings/installed-agent-card");
const TIMESTAMP = "2026-08-13T10:00:00Z";
const STRATEGY_SELECT = "mcp-strategy-select";
function tuiAgent(overrides: Partial<Agent> = {}): Agent {
return {
id: "agent-1",
name: "fuel-claude",
supports_mcp: false,
tui_config: {
command: "fuelclaude",
display_name: "Fuel Claude",
wait_for_terminal: true,
},
profiles: [],
created_at: TIMESTAMP,
updated_at: TIMESTAMP,
...overrides,
} as Agent;
}
function discovery(name: string) {
return {
name,
supports_mcp: false,
mcp_config_path: null,
installation_paths: [],
available: true,
matched_path: "/usr/bin/fuelclaude",
};
}
/**
* Composes the card exactly as the agents index page does: inside
* InstalledAgentCard, keyed off the saved agent record.
*
* The point of asserting through this composition rather than rendering
* CustomTUIMcpCard alone is that the control was previously mounted on the
* agent detail route, which redirects saved agents back to the index before the
* form mounts. Testing the leaf in isolation passed while the control was
* reachable from nowhere.
*/
function renderIndexComposition(saved: Agent | undefined, name = "fuel-claude") {
return render(
<StateProvider>
<ToastProvider>
<InstalledAgentCard
agent={discovery(name) as never}
savedAgent={saved}
displayName="Fuel Claude"
>
<CustomTUIMcpCard agent={saved} />
</InstalledAgentCard>
</ToastProvider>
</StateProvider>,
);
}
describe("custom TUI MCP control on the agents index", () => {
afterEach(cleanup);
afterEach(() => vi.clearAllMocks());
it("renders for a saved custom TUI agent", async () => {
renderIndexComposition(tuiAgent());
expect(await screen.findByTestId("agent-mcp-strategy-fuel-claude")).toBeTruthy();
expect(screen.getByTestId(STRATEGY_SELECT)).toBeTruthy();
});
it("shows the agent's stored strategy as the current value", async () => {
renderIndexComposition(
tuiAgent({
supports_mcp: true,
tui_config: {
command: "fuelclaude",
display_name: "Fuel Claude",
wait_for_terminal: true,
mcp_strategy: "claude",
},
}),
);
const trigger = await screen.findByTestId(STRATEGY_SELECT);
expect(trigger.textContent).toContain("claude");
});
it("renders nothing for a built-in (non-TUI) agent", () => {
renderIndexComposition(tuiAgent({ tui_config: null }), "claude-acp");
expect(screen.queryByTestId(STRATEGY_SELECT)).toBeNull();
});
// An ACP custom agent receives kandev's MCP servers in session/new. Offering
// a passthrough config-file strategy would present a choice the backend
// rejects, because there is no passthrough command to write a config for.
it("renders nothing for a custom agent registered as ACP", () => {
renderIndexComposition(
tuiAgent({
tui_config: {
command: "fuelclaude --acp",
display_name: "Fuel Claude",
wait_for_terminal: true,
protocol: "acp",
},
}),
);
expect(screen.queryByTestId(STRATEGY_SELECT)).toBeNull();
});
it("renders nothing for an agent that has not been saved yet", () => {
renderIndexComposition(undefined);
expect(screen.queryByTestId(STRATEGY_SELECT)).toBeNull();
});
it("does not toast a handled stale-page update error", async () => {
const staleError = new ApiError("interim settings interlock required", 403, {
error_code: INTERIM_SETTINGS_INTERLOCK_ERROR_CODE,
});
staleError.handled = true;
updateMcpStrategy.mockRejectedValueOnce(staleError);
renderIndexComposition(tuiAgent());
fireEvent.click(await screen.findByTestId(STRATEGY_SELECT));
fireEvent.click(await screen.findByRole("option", { name: /claude/ }));
await waitFor(() => expect(updateMcpStrategy).toHaveBeenCalledOnce());
expect(toast).not.toHaveBeenCalled();
expect(screen.queryByText(staleError.message)).toBeNull();
});
});