import { render, waitFor } from "@testing-library/react"; import { afterEach, describe, expect, it, vi } from "vitest"; import type { Icon } from "@tabler/icons-react"; import type { GitHubIssue, GitHubPR } from "@/lib/types/github"; import { repositoryId, workspaceId, type Repository, type Task, type Workflow, type WorkflowStep, } from "@/lib/types/http"; import { QuickTaskLauncher, type LaunchPayload, type TaskPreset } from "./quick-task-launcher"; const NOW = "2026-07-01T00:00:00Z"; const WORKSPACE_ID = "workspace-1"; const WORKFLOW_ID = "workflow-1"; const PR_HEAD_BRANCH = "feature/adding-a-download-ot-5sl"; const TASK_WORKTREE_ROOT = "/root/.kandev/tasks"; const TASK_WORKTREE_PATH = "/root/.kandev/tasks/pr-1541-fix-skip-cle_3bm/kdlbs-kandev"; const REPO_URL = "https://github.com/kdlbs/kandev/pull/1567"; const ISSUE_URL = "https://github.com/kdlbs/kandev/issues/1567"; const LOCAL_REPO_ID = "local-repo"; const mocks = vi.hoisted(() => ({ dialogProps: undefined as | { initialValues?: Record; onSuccess?: (task: Task, mode?: "create" | "edit", meta?: { autoFocus?: boolean }) => void; } | undefined, push: vi.fn(), createTaskPR: vi.fn(), linkTaskIssue: vi.fn(), upsertTaskIssue: vi.fn(), })); vi.mock("@/components/task-create-dialog", () => ({ TaskCreateDialog: (props: { initialValues?: Record }) => { mocks.dialogProps = props; return null; }, })); vi.mock("@/lib/routing/client-router", () => ({ useRouter: () => ({ push: mocks.push }), })); vi.mock("@/components/state-provider", () => ({ useAppStore: (selector: (state: typeof mocks) => unknown) => selector(mocks), })); vi.mock("@/lib/api/domains/github-api", () => ({ createTaskPR: mocks.createTaskPR, linkTaskIssue: mocks.linkTaskIssue, })); const preset: TaskPreset = { id: "review", label: "Review", hint: "Review the PR", icon: (() => null) as unknown as Icon, prompt: ({ url }) => `Review ${url}`, }; const workflow = { id: WORKFLOW_ID, workspace_id: WORKSPACE_ID, name: "Workflow", created_at: NOW, updated_at: NOW, } as Workflow; const step = { id: "step-1", workflow_id: WORKFLOW_ID, name: "Backlog", position: 1, color: "gray", created_at: NOW, updated_at: NOW, } as WorkflowStep; function pr(overrides: Partial = {}): GitHubPR { return { number: 1567, title: "feat: add Download option", url: REPO_URL, html_url: REPO_URL, state: "open", head_branch: PR_HEAD_BRANCH, base_branch: "main", author_login: "contributor", repo_owner: "kdlbs", repo_name: "kandev", draft: false, mergeable: true, additions: 0, deletions: 0, requested_reviewers: [], created_at: NOW, updated_at: NOW, merged_at: null, closed_at: null, ...overrides, }; } function issue(overrides: Partial = {}): GitHubIssue { return { number: 1567, title: "add Download option", body: "", url: ISSUE_URL, html_url: ISSUE_URL, state: "open", author_login: "contributor", repo_owner: "kdlbs", repo_name: "kandev", labels: [], assignees: [], created_at: NOW, updated_at: NOW, closed_at: null, ...overrides, }; } type RepoOverrides = Omit, "id" | "workspace_id"> & { id: string }; function repo(overrides: RepoOverrides): Repository { const { id, ...rest } = overrides; return { id: repositoryId(id), workspace_id: workspaceId(WORKSPACE_ID), name: "kdlbs/kandev", source_type: "local", local_path: "/work/kandev", provider: "github", provider_repo_id: "", provider_owner: "kdlbs", provider_name: "kandev", default_branch: "main", worktree_branch_prefix: "feature/", pull_before_worktree: true, setup_script: "", cleanup_script: "", dev_script: "", copy_files: "", created_at: NOW, updated_at: NOW, ...rest, }; } function renderLauncher(repositories: Repository[], prOverrides: Partial = {}) { const payload: LaunchPayload = { kind: "pr", pr: pr(prOverrides), preset }; render( , ); return mocks.dialogProps?.initialValues; } function renderIssueLauncher( repositories: Repository[], issueOverrides: Partial = {}, ) { const payload: LaunchPayload = { kind: "issue", issue: issue(issueOverrides), preset }; render( , ); return mocks.dialogProps?.initialValues; } afterEach(() => { mocks.dialogProps = undefined; mocks.push.mockClear(); mocks.createTaskPR.mockClear(); mocks.linkTaskIssue.mockClear(); mocks.upsertTaskIssue.mockClear(); }); describe("QuickTaskLauncher repository defaults", () => { it("falls back to Remote mode when the only matching repo is a task worktree path", () => { const initialValues = renderLauncher([ repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH }), ]); expect(initialValues).toMatchObject({ githubUrl: REPO_URL, branch: PR_HEAD_BRANCH, checkoutBranch: PR_HEAD_BRANCH, prNumber: 1567, prBaseBranch: "main", }); expect(initialValues?.repositoryId).toBeUndefined(); }); it("opens PR launches in Remote mode even when a provider-backed repo exists", () => { const initialValues = renderLauncher([ repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH }), repo({ id: "provider-repo", source_type: "provider", local_path: "", }), ]); expect(initialValues).toMatchObject({ githubUrl: REPO_URL, branch: PR_HEAD_BRANCH, checkoutBranch: PR_HEAD_BRANCH, prNumber: 1567, prBaseBranch: "main", }); expect(initialValues?.repositoryId).toBeUndefined(); }); it("still preselects an ordinary matching local GitHub repo for issues", () => { const initialValues = renderIssueLauncher([ repo({ id: LOCAL_REPO_ID, local_path: "/work/kandev" }), ]); expect(initialValues).toMatchObject({ repositoryId: LOCAL_REPO_ID, }); expect(initialValues?.githubUrl).toBeUndefined(); }); it("rejects task worktree roots without a trailing slash", () => { const initialValues = renderIssueLauncher([ repo({ id: "task-worktree-root", local_path: TASK_WORKTREE_ROOT }), ]); expect(initialValues).toMatchObject({ githubUrl: "github.com/kdlbs/kandev", }); expect(initialValues?.repositoryId).toBeUndefined(); }); it("derives a PR URL when the GitHub payload omits URL fields", () => { const initialValues = renderLauncher( [repo({ id: "task-worktree", local_path: TASK_WORKTREE_PATH })], { html_url: "", url: "" }, ); expect(initialValues).toMatchObject({ githubUrl: REPO_URL, branch: PR_HEAD_BRANCH, checkoutBranch: PR_HEAD_BRANCH, prNumber: 1567, prBaseBranch: "main", }); expect(initialValues?.repositoryId).toBeUndefined(); }); }); describe("QuickTaskLauncher issue linking", () => { it.each([true, false])( "links and stores the launched issue with auto-focus %s", async (autoFocus) => { const link = { task_id: "task-1", task_title: "Review: add Download option", owner: "kdlbs", repo: "kandev", issue_number: 1567, issue_url: ISSUE_URL, issue_title: "add Download option", }; mocks.linkTaskIssue.mockResolvedValue(link); renderIssueLauncher([repo({ id: LOCAL_REPO_ID })]); mocks.dialogProps?.onSuccess?.({ id: "task-1" } as Task, "create", { autoFocus }); expect(mocks.linkTaskIssue).toHaveBeenCalledWith("task-1", { issue: ISSUE_URL }); await waitFor(() => { expect(mocks.upsertTaskIssue).toHaveBeenCalledWith(WORKSPACE_ID, link); }); expect(mocks.push).toHaveBeenCalledTimes(autoFocus ? 1 : 0); if (autoFocus) expect(mocks.push).toHaveBeenCalledWith("/t/task-1"); }, ); it("navigates when issue linking fails", async () => { mocks.linkTaskIssue.mockRejectedValueOnce(new Error("offline")); renderIssueLauncher([repo({ id: LOCAL_REPO_ID })]); mocks.dialogProps?.onSuccess?.({ id: "task-1" } as Task); expect(mocks.push).toHaveBeenCalledWith("/t/task-1"); await waitFor(() => expect(mocks.linkTaskIssue).toHaveBeenCalledTimes(1)); expect(mocks.upsertTaskIssue).not.toHaveBeenCalled(); }); });