import { describe, expect, it } from "vitest"; import type { ExecutorProfile } from "@/lib/types/http"; import { computeAgentCompatState, computeHasRepositorySelection, computeSelectedRepoCount, filterCompatibleAgentProfiles, } from "./task-create-dialog-computed"; import type { DialogFormState } from "@/components/task-create-dialog-types"; import type { AgentProfileOption } from "@/lib/state/slices/settings/types"; const URL_A = "github.com/a/b"; const URL_B = "github.com/c/d"; // Minimal fs stub for the two pure helpers below. Each function only reads // `noRepository`, `useRemote`, `remoteRepos`, and `repositories`, so we cast a // partial through `unknown` to avoid having to materialise the full // DialogFormState surface in tests. function fsStub(overrides: { noRepository?: boolean; useRemote?: boolean; remoteRepos?: Array<{ url?: string; branch?: string }>; repositories?: Array<{ repositoryId?: string; localPath?: string; branch?: string }>; }): DialogFormState { return { noRepository: false, useRemote: false, remoteRepos: [], repositories: [], ...overrides, } as unknown as DialogFormState; } describe("computeHasRepositorySelection", () => { it("returns true in no-repository mode", () => { expect(computeHasRepositorySelection(fsStub({ noRepository: true }))).toBe(true); }); it("returns true when any workspace/local row is set", () => { expect(computeHasRepositorySelection(fsStub({ repositories: [{ repositoryId: "r-1" }] }))).toBe( true, ); expect( computeHasRepositorySelection(fsStub({ repositories: [{ localPath: "/tmp/repo" }] })), ).toBe(true); }); it("Remote mode: returns true when ANY row has a non-empty URL", () => { // First row empty, second row populated — the previous implementation // only inspected `remoteRepos[0]` and would have returned false here. expect( computeHasRepositorySelection( fsStub({ useRemote: true, remoteRepos: [{ url: "" }, { url: "github.com/acme/site" }], }), ), ).toBe(true); }); it("Remote mode: returns true when only the first row is populated", () => { expect( computeHasRepositorySelection( fsStub({ useRemote: true, remoteRepos: [{ url: "github.com/acme/site" }], }), ), ).toBe(true); }); it("Remote mode: returns false when every row is empty / whitespace", () => { expect( computeHasRepositorySelection( fsStub({ useRemote: true, remoteRepos: [{ url: "" }, { url: " " }], }), ), ).toBe(false); }); it("returns false on the empty form", () => { expect(computeHasRepositorySelection(fsStub({}))).toBe(false); }); it("Remote mode: ignores hidden workspace/local rows", () => { // Toggle-back preserves the workspace/local rows when useRemote flips on // — they're hidden from the UI and must not count toward the selection // gate. Otherwise the submit button stays enabled in Remote mode even // when the user hasn't filled in any URLs. expect( computeHasRepositorySelection( fsStub({ useRemote: true, repositories: [{ repositoryId: "r-1" }], remoteRepos: [{ url: "" }], }), ), ).toBe(false); }); }); describe("computeSelectedRepoCount", () => { it("counts workspace/local rows", () => { expect( computeSelectedRepoCount( fsStub({ repositories: [{ repositoryId: "r-1" }, { localPath: "/x" }] }), ), ).toBe(2); }); it("ignores empty workspace rows", () => { // Half-filled rows (no repositoryId, no localPath) shouldn't count // toward the multi-repo gate even though they exist in the array. expect(computeSelectedRepoCount(fsStub({ repositories: [{ branch: "main" }] }))).toBe(0); }); it("Remote mode: counts non-empty URL rows only", () => { // Two remote URLs — without the local rows, this alone trips the gate. expect( computeSelectedRepoCount( fsStub({ useRemote: true, remoteRepos: [{ url: URL_A }, { url: URL_B }], }), ), ).toBe(2); }); it("Remote mode: ignores hidden workspace/local rows", () => { // useRemote=true hides the workspace/local list — those rows must not // contribute to the count even if they're still in the form state from // a prior mode (toggle-back preserves them). expect( computeSelectedRepoCount( fsStub({ useRemote: true, repositories: [{ repositoryId: "r-1" }, { localPath: "/tmp/x" }], remoteRepos: [{ url: URL_A }], }), ), ).toBe(1); }); it("Remote mode: returns 0 when only hidden workspace/local rows exist", () => { expect( computeSelectedRepoCount( fsStub({ useRemote: true, repositories: [{ repositoryId: "r-1" }], remoteRepos: [], }), ), ).toBe(0); }); it("returns 0 in no-repository mode regardless of stale rows", () => { expect( computeSelectedRepoCount( fsStub({ noRepository: true, repositories: [{ repositoryId: "r-1" }], remoteRepos: [{ url: URL_A }], useRemote: true, }), ), ).toBe(0); }); it("ignores remote rows when useRemote is false", () => { // The Remote toggle gates the count — switching modes shouldn't smuggle // the URL rows into the local count. expect( computeSelectedRepoCount( fsStub({ useRemote: false, remoteRepos: [{ url: URL_A }, { url: URL_B }], }), ), ).toBe(0); }); it("trips the multi-repo gate (>1) on a 2-row Remote selection", () => { // Multi-row Remote selection must trigger the same executor gating as // a 2-workspace-repo task — the previous implementation only counted // workspace/local rows, so this would have stayed at 0 even with two // populated URLs. const count = computeSelectedRepoCount( fsStub({ useRemote: true, remoteRepos: [{ url: "https://github.com/a/b" }, { url: "https://github.com/c/d" }], }), ); expect(count).toBe(2); expect(count > 1).toBe(true); }); }); describe("filterCompatibleAgentProfiles", () => { it("removes disabled raw profiles before compatibility and autopick", () => { const profiles = [ { id: "enabled", label: "Agent • Enabled", agent_id: "a1", agent_name: "mock-agent", cli_passthrough: false, enabled: true, }, { id: "disabled", label: "Agent • Disabled", agent_id: "a1", agent_name: "mock-agent", cli_passthrough: false, enabled: false, }, ] satisfies AgentProfileOption[]; expect( filterCompatibleAgentProfiles( profiles, { executor_type: "local", config: {} } as unknown as ExecutorProfile, true, [], ).map((p) => p.id), ).toEqual(["enabled"]); }); }); describe("computeAgentCompatState", () => { const sprites = { id: "exec-sprites", executor_type: "sprites", config: {}, } as unknown as ExecutorProfile; function profile(id: string, enabled = true): AgentProfileOption { return { id, label: id, agent_id: `agent-${id}`, agent_name: id, cli_passthrough: false, enabled, }; } const claude = profile("claude"); const cursor = profile("cursor"); function state( overrides: Partial[0]> = {}, ): ReturnType { return computeAgentCompatState({ selectedExecutorProfile: sprites, compatibleAgentProfiles: [cursor], selectedAgentProfileId: claude.id, selectedAgentProfile: claude, workflowAgentLocked: false, dynamicRoutingEnabled: true, ...overrides, }); } it("is compatible while no executor profile is selected", () => { expect(state({ selectedExecutorProfile: null, compatibleAgentProfiles: [] })).toBe( "compatible", ); }); // @covers AC-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001.4 it("is none-compatible when the compatible list is empty and nothing locks the agent", () => { expect(state({ compatibleAgentProfiles: [] })).toBe("none-compatible"); expect( state({ compatibleAgentProfiles: [], selectedAgentProfileId: "", selectedAgentProfile: null, }), ).toBe("none-compatible"); }); it("is compatible while nothing is selected and a compatible profile exists", () => { expect(state({ selectedAgentProfileId: "", selectedAgentProfile: null })).toBe("compatible"); }); // @covers AC-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001.6 it("is selected-incompatible, never none-compatible, when the selection is absent from a non-empty list", () => { expect(state()).toBe("selected-incompatible"); }); // @covers AC-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001.3 it("is compatible when the selection is in the list", () => { expect(state({ compatibleAgentProfiles: [claude, cursor] })).toBe("compatible"); }); // @covers AC-TASKS-TASK-CREATE-AGENT-COMPATIBILITY-001.5 it("keeps the workflow-locked state even when nothing else is compatible", () => { expect(state({ compatibleAgentProfiles: [], workflowAgentLocked: true })).toBe( "selected-incompatible", ); }); it("treats a disabled locked profile as no compatible agent, not a credential problem", () => { const disabled = profile("disabled", false); expect( state({ selectedAgentProfileId: disabled.id, selectedAgentProfile: disabled, workflowAgentLocked: true, }), ).toBe("none-compatible"); }); it("keeps an unlocked disabled selection distinct when an alternative exists", () => { const disabled = profile("disabled", false); expect( state({ selectedAgentProfileId: disabled.id, selectedAgentProfile: disabled, workflowAgentLocked: false, compatibleAgentProfiles: [cursor], }), ).toBe("selected-unavailable"); }); it("keeps an unlocked dynamic selection distinct when routing is disabled", () => { const dynamic = { ...profile("dynamic"), kind: "dynamic" } satisfies AgentProfileOption; expect( state({ selectedAgentProfileId: dynamic.id, selectedAgentProfile: dynamic, workflowAgentLocked: false, dynamicRoutingEnabled: false, compatibleAgentProfiles: [cursor], }), ).toBe("selected-unavailable"); }); it("treats an unknown selected id as a credential problem when a compatible profile exists", () => { expect(state({ selectedAgentProfileId: "gone", selectedAgentProfile: null })).toBe( "selected-incompatible", ); }); });