import { renderHook } from "@testing-library/react"; import type { FormEvent } from "react"; import { beforeEach, describe, expect, it, vi } from "vitest"; import type { TaskCreateDialogProps } from "./task-create-dialog"; import type { RepositoryBranchesState } from "@/lib/state/slices/workspace/types"; const mocks = vi.hoisted(() => ({ agentGeneratedTaskTitles: false, submit: vi.fn(), compatibilityBlocked: false, agentCompatState: "compatible" as | "compatible" | "selected-incompatible" | "selected-unavailable" | "none-compatible", shortcutHandler: null as ((event: unknown) => void) | null, submitDeps: {} as Record, workflowAgentOverridesBlockedReason: undefined as string | undefined, })); vi.mock("@/lib/keyboard/constants", () => ({ SHORTCUTS: { SUBMIT: "submit" } })); vi.mock("@/hooks/use-is-utility-configured", () => ({ useIsUtilityConfigured: () => false })); vi.mock("@/hooks/use-keyboard-shortcut", () => ({ useKeyboardShortcutHandler: (_shortcut: string, handler: (event: unknown) => void) => { mocks.shortcutHandler = handler; return vi.fn(); }, })); vi.mock("@/hooks/use-utility-agent-generator", () => ({ useUtilityAgentGenerator: () => ({ enhancePrompt: vi.fn(), isEnhancingPrompt: false }), })); vi.mock("@/hooks/use-prompt-result-delivery", () => ({ usePromptResultDelivery: () => ({ pendingResult: null, captureScope: vi.fn(), deliver: vi.fn(), applyPending: vi.fn(), copyPending: vi.fn(), }), })); vi.mock("@/components/toast-provider", () => ({ useToast: () => ({ toast: vi.fn() }) })); vi.mock("@/components/state-provider", () => ({ useAppStoreApi: () => ({ getState: () => ({ repositoryBranchPolicies: { revisionByRepositoryId: {} }, }), }), useAppStore: (selector: (state: unknown) => unknown) => selector({ userSettings: { agentGeneratedTaskTitles: mocks.agentGeneratedTaskTitles }, upsertRepository: vi.fn(), setRepositoryBranchPolicies: vi.fn(), setRepositoryBranchPoliciesLoading: vi.fn(), repositorySets: { itemsByWorkspaceId: {}, loadingByWorkspaceId: {}, loadedByWorkspaceId: {}, revisionByWorkspaceId: {}, }, setRepositorySets: vi.fn(), setRepositorySetsLoading: vi.fn(), }), })); vi.mock("@/components/task-create-dialog-submit", () => ({ useTaskSubmitHandlers: (deps: Record) => { mocks.submitDeps = deps; return { handleSubmit: mocks.submit, pendingDiscard: null }; }, })); vi.mock("@/components/task-create-dialog-workflow-context", () => ({ useResolvedTaskCreateWorkflowContext: (props: TaskCreateDialogProps) => props, })); vi.mock("@/components/task-create-dialog-workflow-agent-override-validation", () => ({ buildWorkflowAgentOverrideValidation: () => ({ rows: [], options: [], loading: false, error: false, invalid: Boolean(mocks.workflowAgentOverridesBlockedReason), blockedReason: mocks.workflowAgentOverridesBlockedReason, }), })); vi.mock("@/components/task-create-dialog-state", () => ({ computeIsTaskStarted: () => false, useDialogFormState: () => ({ taskName: "", setTaskName: vi.fn(), hasTitle: false, setHasTitle: vi.fn(), hasDescription: false, setHasDescription: vi.fn(), descriptionInputRef: { current: null }, openCycle: 1, repositories: [], discoveredRepositories: [], remoteRepos: [], prInfoByUrl: {}, useRemote: false, executorId: "", executorProfileId: "", freshBranchEnabled: false, noRepository: false, blockedBy: ["dep-1"], setBlockedBy: vi.fn(), workspacePath: "", isCreatingSession: false, isCreatingTask: false, setIsCreatingSession: vi.fn(), setIsCreatingTask: vi.fn(), setRepositories: vi.fn(), setRemoteRepos: vi.fn(), setAgentProfileId: vi.fn(), setExecutorId: vi.fn(), setSelectedWorkflowId: vi.fn(), setFetchedSteps: vi.fn(), clearDraft: vi.fn(), currentDefaults: { description: "" }, }), useTaskCreateDialogData: () => ({ workflows: [], agentProfiles: [], executors: [], snapshots: {}, workspaceSnapshotRead: {}, repositories: [], repositoriesLoading: false, refreshRepositories: vi.fn(), taskCreateLastUsed: { repositoryId: null, agentProfileId: null, executorProfileId: null, branch: null, }, userSettingsLoaded: true, computed: { effectiveWorkflowId: null, effectiveDefaultStepId: null, effectiveAgentProfileId: "", compatibleAgentProfiles: [], authLoaded: true, workspaceDefaults: {}, isLocalExecutor: true, isPassthroughProfile: false, agentProfileOptions: [], executorProfileOptions: [], agentProfilesLoading: false, executorsLoading: false, workflowAgentLocked: false, noCompatibleAgent: mocks.compatibilityBlocked, agentCompatState: mocks.agentCompatState, selectedExecutorProfileName: "", executorHint: "", hasRepositorySelection: false, }, }), useTaskCreateDialogEffects: vi.fn(), useLockedFieldSync: vi.fn(), useDialogHandlers: () => ({}), useSessionRepoName: () => null, })); import { hasUnavailableSavedBase, useTaskCreateDialogSetup } from "./task-create-dialog-setup"; const props: TaskCreateDialogProps = { open: true, onOpenChange: vi.fn(), workspaceId: "workspace-1", workflowId: "workflow-1", defaultStepId: null, steps: [], }; describe("useTaskCreateDialogSetup auto-title mode", () => { beforeEach(() => { mocks.agentGeneratedTaskTitles = false; mocks.compatibilityBlocked = false; mocks.agentCompatState = "compatible"; mocks.shortcutHandler = null; mocks.submit.mockReset(); mocks.workflowAgentOverridesBlockedReason = undefined; }); it.each([ ["create", false, false], ["create", true, true], ["edit", true, false], ["session", true, false], ] as const)("derives autoTitle=%s for %s mode with setting=%s", (mode, enabled, expected) => { mocks.agentGeneratedTaskTitles = enabled; const { result } = renderHook(() => useTaskCreateDialogSetup({ ...props, mode })); expect(result.current.autoTitle).toBe(expected); }); }); it("forwards the selected dependencies to the submit handlers", () => { // The payload builder handled blocked_by correctly all along; the break was // this hop — useSubmitHandlersWiring never passed blockedBy through, so the // create dialog's selection silently never reached the request. Assert the // hop itself, not just the leaf. renderHook(() => useTaskCreateDialogSetup({ ...props, mode: "create" })); expect(mocks.submitDeps.blockedBy).toEqual(["dep-1"]); }); describe("compatibility submit guard", () => { it.each(["selected-incompatible", "none-compatible"] as const)( "blocks the form and keyboard submit for %s", (agentCompatState) => { mocks.compatibilityBlocked = true; mocks.agentCompatState = agentCompatState; const { result } = renderHook(() => useTaskCreateDialogSetup({ ...props, mode: "create" })); const event = { preventDefault: vi.fn() } as unknown as FormEvent; const shortcutEvent = { preventDefault: vi.fn() } as unknown as FormEvent; result.current.guardedHandleSubmit(event); mocks.shortcutHandler?.(shortcutEvent); expect(event.preventDefault).toHaveBeenCalledTimes(1); expect(shortcutEvent.preventDefault).toHaveBeenCalledTimes(1); expect(mocks.submit).not.toHaveBeenCalled(); }, ); }); describe("workflow override keyboard submit guard", () => { it.each(["replacement profile is unavailable", "workflow agents could not be loaded"])( "blocks create submission when validation fails: %s", (blockedReason) => { mocks.workflowAgentOverridesBlockedReason = blockedReason; const { result } = renderHook(() => useTaskCreateDialogSetup({ ...props, mode: "create" })); const formEvent = { preventDefault: vi.fn() } as unknown as FormEvent; const shortcutEvent = { preventDefault: vi.fn() } as unknown as FormEvent; result.current.guardedHandleSubmit(formEvent); mocks.shortcutHandler?.(shortcutEvent); expect(formEvent.preventDefault).toHaveBeenCalledTimes(1); expect(shortcutEvent.preventDefault).toHaveBeenCalledTimes(1); expect(mocks.submit).not.toHaveBeenCalled(); }, ); }); it("validates saved bases against qualified branch option values", () => { const repositoryBranches = { itemsByRepositoryId: { "repo-1": [{ name: "main", type: "remote", remote: "origin" }], }, loadedByRepositoryId: { "repo-1": true }, } as unknown as RepositoryBranchesState; expect( hasUnavailableSavedBase( [{ key: "r0", repositoryId: "repo-1", branch: "", baseBranch: "main" }], repositoryBranches, ), ).toBe(true); expect( hasUnavailableSavedBase( [{ key: "r0", repositoryId: "repo-1", branch: "", baseBranch: "origin/main" }], repositoryBranches, ), ).toBe(false); });