import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import { SettingsView } from "@/components/settings/SettingsView"; import { ClientProvider } from "@/providers/ClientProvider"; function jsonResponse(body: unknown): Response { return { ok: true, status: 200, json: async () => body, } as Response; } function settingsPayload() { return { agent: { model: "openai/gpt-4o", provider: "auto", resolved_provider: "openai", has_api_key: true, model_preset: "default", max_tokens: 8192, context_window_tokens: 65536, temperature: 0.1, reasoning_effort: null, timezone: "UTC", bot_name: "nanobot", bot_icon: "nb", tool_hint_max_length: 40, }, model_presets: [{ name: "default", label: "Default", active: true, is_default: true, model: "openai/gpt-4o", provider: "auto", max_tokens: 8192, context_window_tokens: 65536, temperature: 0.1, reasoning_effort: null, }], providers: [], web_search: { provider: "duckduckgo", api_key_hint: null, base_url: null, max_results: 5, timeout: 30, providers: [{ name: "duckduckgo", label: "DuckDuckGo", credential: "none" }], }, web: { enable: true, proxy: null, user_agent: null, search: { max_results: 5, timeout: 30 }, fetch: { use_jina_reader: true }, }, image_generation: { enabled: false, provider: "openrouter", provider_configured: false, model: "openai/gpt-5.4-image-2", default_aspect_ratio: "1:1", default_image_size: "1K", max_images_per_turn: 4, save_dir: "generated", providers: [], }, runtime: { config_path: "/tmp/config.json", workspace_path: "/tmp/workspace", gateway_host: "127.0.0.1", gateway_port: 18790, heartbeat: { enabled: true, interval_s: 1800, keep_recent_messages: 8, }, dream: { schedule: "every 2h", max_batch_size: 20, max_iterations: 15, annotate_line_ages: true, }, unified_session: false, }, advanced: { restrict_to_workspace: false, ssrf_whitelist_count: 0, mcp_server_count: 0, exec_enabled: true, exec_sandbox: null, exec_path_append_set: false, }, requires_restart: false, }; } const installedAnyGen = { name: "anygen", display_name: "AnyGen", category: "generation", description: "Generate docs, slides, websites and more via AnyGen cloud API", requires: "ANYGEN_API_KEY", source: "harness", entry_point: "cli-anything-anygen", install_supported: true, installed: true, available: true, status: "installed", logo_url: "https://www.google.com/s2/favicons?domain=anygen.io&sz=64", brand_color: "#111827", skill_installed: true, }; function renderSettingsView() { render( {}} onBackToChat={() => {}} onModelNameChange={() => {}} /> , ); } describe("SettingsView Apps catalog", () => { afterEach(() => { vi.unstubAllGlobals(); }); it("shows a visible uninstall button for installed CLI apps and calls uninstall", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = String(input); if (url === "/api/settings") { return jsonResponse(settingsPayload()); } if (url === "/api/settings/cli-apps") { return jsonResponse({ apps: [installedAnyGen], installed_count: 1, catalog_updated_at: "2026-04-18", }); } if (url === "/api/settings/mcp-presets") { return jsonResponse({ presets: [], installed_count: 0 }); } if (url === "/api/settings/cli-apps/uninstall?name=anygen") { return jsonResponse({ apps: [{ ...installedAnyGen, installed: false, status: "available" }], installed_count: 0, catalog_updated_at: "2026-04-18", last_action: { ok: true, message: "Uninstalled CLI for AnyGen.", still_available: false, }, }); } return { ok: false, status: 404, json: async () => ({}) } as Response; }); vi.stubGlobal("fetch", fetchMock); renderSettingsView(); expect(await screen.findByRole("heading", { name: "Apps" })).toBeInTheDocument(); expect(await screen.findByText("AnyGen")).toBeInTheDocument(); const uninstall = screen.getByRole("button", { name: "Uninstall CLI" }); fireEvent.click(uninstall); await waitFor(() => expect(fetchMock).toHaveBeenCalledWith( "/api/settings/cli-apps/uninstall?name=anygen", expect.objectContaining({ headers: { Authorization: "Bearer tok" }, }), ), ); expect(await screen.findByText("Uninstalled CLI for AnyGen.")).toBeInTheDocument(); fireEvent.click(screen.getByRole("button", { name: "Dismiss" })); expect(screen.queryByText("Uninstalled CLI for AnyGen.")).not.toBeInTheDocument(); }); });