import { describe, it, expect, vi, beforeEach } from "vitest"; import { renderHook } from "@testing-library/react"; import { useBranchAutoSelectEffect, useWorkflowAgentProfileEffect, } from "./task-create-dialog-effects"; import type { DialogFormState } from "@/components/task-create-dialog-types"; import type { AgentProfileOption } from "@/lib/state/slices"; import { STORAGE_KEYS } from "@/lib/settings/constants"; // Minimal fake of DialogFormState - the hook destructures only three fields, // so the rest can be undefined behind an `as` cast and never read. type Fake = Pick< DialogFormState, "selectedWorkflowId" | "setAgentProfileId" | "setWorkflowAgentProfileId" >; function makeFs(overrides: Partial = {}): DialogFormState { return { selectedWorkflowId: null, setAgentProfileId: vi.fn(), setWorkflowAgentProfileId: vi.fn(), ...overrides, } as unknown as DialogFormState; } function makeProfile(id: string): AgentProfileOption { return { id, label: `agent • ${id}`, agent_id: `agent-${id}`, agent_name: "agent", cli_passthrough: false, }; } beforeEach(() => { localStorage.clear(); }); describe("useWorkflowAgentProfileEffect", () => { it("clears workflowAgentProfileId and leaves agentProfileId alone when no workflow is selected", () => { localStorage.setItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, JSON.stringify("stale-id")); const fs = makeFs({ selectedWorkflowId: null }); renderHook(() => useWorkflowAgentProfileEffect(fs, [], [makeProfile("real-id")])); expect(fs.setWorkflowAgentProfileId).toHaveBeenCalledWith(""); // No workflow → effect must early-return before touching agentProfileId, // even if localStorage holds a value (stale or otherwise). expect(fs.setAgentProfileId).not.toHaveBeenCalled(); }); it("locks to workflow.agent_profile_id when the profile exists", () => { const fs = makeFs({ selectedWorkflowId: "wf-1" }); const workflows = [{ id: "wf-1", agent_profile_id: "real-id" }]; renderHook(() => useWorkflowAgentProfileEffect(fs, workflows, [makeProfile("real-id")])); expect(fs.setWorkflowAgentProfileId).toHaveBeenCalledWith("real-id"); expect(fs.setAgentProfileId).toHaveBeenCalledWith("real-id"); }); it("locks the selector but does NOT set agentProfileId when workflow's profile is missing", () => { const fs = makeFs({ selectedWorkflowId: "wf-1" }); const workflows = [{ id: "wf-1", agent_profile_id: "missing-id" }]; renderHook(() => useWorkflowAgentProfileEffect(fs, workflows, [makeProfile("real-id")])); // The lock still applies (otherwise the user could pick an alternate // profile and submit a workflow-locked task with the wrong agent). expect(fs.setWorkflowAgentProfileId).toHaveBeenCalledWith("missing-id"); expect(fs.setAgentProfileId).not.toHaveBeenCalledWith("missing-id"); }); it("restores last-used agentProfileId when the workflow has no override and the id is still valid", () => { localStorage.setItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, JSON.stringify("real-id")); const fs = makeFs({ selectedWorkflowId: "wf-1" }); const workflows = [{ id: "wf-1" /* no agent_profile_id */ }]; renderHook(() => useWorkflowAgentProfileEffect(fs, workflows, [makeProfile("real-id")])); expect(fs.setWorkflowAgentProfileId).toHaveBeenCalledWith(""); expect(fs.setAgentProfileId).toHaveBeenCalledWith("real-id"); }); it("does NOT restore a stale localStorage agentProfileId that is not in agentProfiles", () => { // Regression guard: a clean DB mints fresh UUIDs for the same agents, so // the previous run's lastAgentProfileId no longer matches any profile. // Writing it back here poisons the dialog into "No compatible agent // profiles" because useDefaultSelectionsEffect skips when agentProfileId // is already truthy. localStorage.setItem(STORAGE_KEYS.LAST_AGENT_PROFILE_ID, JSON.stringify("stale-id")); const fs = makeFs({ selectedWorkflowId: "wf-1" }); const workflows = [{ id: "wf-1" /* no agent_profile_id */ }]; renderHook(() => useWorkflowAgentProfileEffect(fs, workflows, [makeProfile("real-id")])); expect(fs.setAgentProfileId).not.toHaveBeenCalledWith("stale-id"); }); it("clears agentProfileId when the workflow has no override and there is no last-used id", () => { const fs = makeFs({ selectedWorkflowId: "wf-1" }); const workflows = [{ id: "wf-1" /* no agent_profile_id */ }]; renderHook(() => useWorkflowAgentProfileEffect(fs, workflows, [makeProfile("real-id")])); // Empty string lets useDefaultSelectionsEffect take over the fallback // chain (workspace default → first profile). expect(fs.setAgentProfileId).toHaveBeenCalledWith(""); }); }); type BranchFake = Pick< DialogFormState, "githubBranch" | "githubBranches" | "useGitHubUrl" | "setGitHubBranch" | "githubPrHeadBranch" >; function makeBranchFs(overrides: Partial = {}): DialogFormState { return { githubBranch: "", githubBranches: [], useGitHubUrl: true, setGitHubBranch: vi.fn(), githubPrHeadBranch: null, ...overrides, } as unknown as DialogFormState; } describe("useBranchAutoSelectEffect", () => { it("selects the PR head branch when it is present in the base repo's branch list", () => { const fs = makeBranchFs({ githubBranches: [ { name: "main", type: "remote" }, { name: "feature/x", type: "remote" }, ], githubPrHeadBranch: "feature/x", }); renderHook(() => useBranchAutoSelectEffect(fs)); expect(fs.setGitHubBranch).toHaveBeenCalledWith("feature/x"); }); it("still surfaces the PR head branch for fork PRs whose head is NOT in the base repo's branch list", () => { // Regression guard for the fork-PR display bug: previously the effect // fell through to "main" when the PR head was missing from the base // repo's branches, visually contradicting the URL the user just pasted. const fs = makeBranchFs({ githubBranches: [ { name: "main", type: "remote" }, { name: "develop", type: "remote" }, ], githubPrHeadBranch: "jira-hosted-path-auth", }); renderHook(() => useBranchAutoSelectEffect(fs)); expect(fs.setGitHubBranch).toHaveBeenCalledWith("jira-hosted-path-auth"); }); it("falls back to main when there is no PR head branch", () => { const fs = makeBranchFs({ githubBranches: [ { name: "feature/y", type: "remote" }, { name: "main", type: "remote" }, ], }); renderHook(() => useBranchAutoSelectEffect(fs)); expect(fs.setGitHubBranch).toHaveBeenCalledWith("main"); }); it("does nothing when useGitHubUrl is false", () => { const fs = makeBranchFs({ useGitHubUrl: false, githubBranches: [{ name: "main", type: "remote" }], githubPrHeadBranch: "feature/x", }); renderHook(() => useBranchAutoSelectEffect(fs)); expect(fs.setGitHubBranch).not.toHaveBeenCalled(); }); it("does nothing once a branch has already been selected (manual override)", () => { const fs = makeBranchFs({ githubBranch: "develop", githubBranches: [ { name: "main", type: "remote" }, { name: "develop", type: "remote" }, ], githubPrHeadBranch: "feature/x", }); renderHook(() => useBranchAutoSelectEffect(fs)); expect(fs.setGitHubBranch).not.toHaveBeenCalled(); }); });