/** * @file AgentInstructionsSettings.test.tsx * @description My agent instructions: a failed load shows the error with a * Retry (the editor stays locked, so a save cannot overwrite unloaded text) * and Retry unlocks it; Choose file puts a local file's text into the draft * without saving, and refuses one longer than the limit; "Applies to" names * only the installed, enabled tools the server says receive them. * @author Nguyễn Ngọc Trí Vĩ */ import { fireEvent, render, screen, waitFor } from "@testing-library/react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { AgentInstructionsSettings } from "../AgentInstructionsSettings"; import { api } from "../../../lib/api"; import { toast } from "../../../lib/toast"; vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { me: { instructions: vi.fn(), saveInstructions: vi.fn() } }, }; }); const aiTools = vi.hoisted(() => ({ tools: [] as { id: string; label: string; installed: boolean; enabled: boolean }[], })); vi.mock("../../../hooks/useAiTools", () => ({ useAiTools: () => aiTools })); vi.mock("../../../lib/toast", () => ({ toast: { success: vi.fn(), error: vi.fn() } })); const instructions = vi.mocked(api.me.instructions); const saveInstructions = vi.mocked(api.me.saveInstructions); const editor = () => screen.getByRole("textbox") as HTMLTextAreaElement; // jsdom's File has no text(); every browser AgentHub supports does. const pick = (text: string, name = "rules.md") => { const file = Object.assign(new File([text], name, { type: "text/markdown" }), { text: () => Promise.resolve(text), }); fireEvent.change(screen.getByTestId("agent-instructions-file"), { target: { files: [file] } }); }; describe("AgentInstructionsSettings", () => { beforeEach(() => { vi.clearAllMocks(); aiTools.tools = []; }); it("names only the installed, enabled tools that receive the instructions", async () => { aiTools.tools = [ { id: "claude", label: "Claude Code", installed: true, enabled: true }, { id: "codex", label: "Codex", installed: true, enabled: false }, { id: "cursor", label: "Cursor CLI", installed: true, enabled: true }, ]; instructions.mockResolvedValue({ instructions: "", max: 100, tools: ["claude", "codex"] }); render(); expect(await screen.findByText("Claude Code")).toBeInTheDocument(); expect(screen.getByText("Applies to")).toBeInTheDocument(); expect(screen.queryByText("Codex")).toBeNull(); expect(screen.queryByText("Cursor CLI")).toBeNull(); }); it("shows a failed load with Retry, and Retry unlocks the editor", async () => { instructions.mockRejectedValueOnce(new Error("HTTP 404")); render(); expect(await screen.findByText(/HTTP 404/)).toBeInTheDocument(); expect(editor()).toBeDisabled(); instructions.mockResolvedValueOnce({ instructions: "# saved", max: 100, tools: [] }); fireEvent.click(screen.getByRole("button", { name: /retry/i })); await waitFor(() => expect(editor()).not.toBeDisabled()); expect(editor().value).toBe("# saved"); }); it("Choose file fills the draft without saving; Save sends it", async () => { instructions.mockResolvedValue({ instructions: "", max: 100, tools: [] }); saveInstructions.mockResolvedValue({ instructions: "# from file", max: 100, tools: [] }); render(); await waitFor(() => expect(editor()).not.toBeDisabled()); pick("# from file"); await waitFor(() => expect(editor().value).toBe("# from file")); expect(saveInstructions).not.toHaveBeenCalled(); fireEvent.click(screen.getByRole("button", { name: /^save$/i })); await waitFor(() => expect(saveInstructions).toHaveBeenCalledWith("# from file")); }); it("refuses a file longer than the limit and keeps the draft", async () => { instructions.mockResolvedValue({ instructions: "keep", max: 5, tools: [] }); render(); await waitFor(() => expect(editor()).not.toBeDisabled()); pick("way too long"); await waitFor(() => expect(toast.error).toHaveBeenCalled()); expect(editor().value).toBe("keep"); }); });