Files
kandev/apps/web/components/task-create-dialog-footer.test.ts
T
Kandev Agent 9e8079de69 feat(multi-repo): per-repo git ops, unified branch endpoint, chip-style task create
Bundles a string of multi-repo fixes and refactors that came up while
testing the feature end-to-end:

- workspace_path stored on multi-repo TaskEnvironment was one level too
  high, so agentctl booted with the wrong WorkDir on cold start; fixed
  in resolveTaskEnvWorkspacePath + GetWorkspaceInfoForSession.
- Stage/Unstage/Discard/Commit/Revert/Reset/Amend now thread `repo`
  through agentctl client → WS handler → frontend hook → SessionGit
  fan-out, so the right git repo is targeted (was running at the task
  root, which isn't a git repo for multi-repo tasks).
- agentctl GitLog is multi-repo aware: when no `repo` is specified and
  per-repo trackers exist, fans out one log per repo and merges sorted
  newest-first with RepositoryName stamped on each commit.
- Per-repo grouping for files (Changes panel) and commits, with
  collapsible group headers; commits section defaults to collapsed.
  Algorithm extracted into one generic groupByRepositoryName helper
  (was copy-pasted three times).
- Task creation dialog rewritten around a unified `fs.repositories[]`
  list — no more "primary vs extras" split. Chip-style row with one
  pill per repo + branch, "+" to add, x to remove, GitHub URL toggle
  inline. Discovered (on-machine) repos appear in any chip's dropdown.
  Removes ~6 fields and 3 helpers from the dialog state.
- Branch listing endpoints collapsed to one:
  GET /workspaces/:id/branches?repository_id=X | path=Y
  Frontend collapses to a single useBranches hook with a tagged-union
  source. Drops two HTTP routes, two service methods, two API client
  funcs, and one whole hook file.

Tests added across the chain: workspace_path resolver, sort-commits-
by-date, manager poll-mode propagation, RepoSubpaths, chip grouping
+ collapse + per-repo isLatest, group-by-repo helper, dialog payload
builder + chip wiring + discovered-repo flow.
2026-04-26 21:23:48 +01:00

169 lines
4.8 KiB
TypeScript

import { describe, it, expect } from "vitest";
import {
computeDisabledReason,
REASON_TITLE,
REASON_REPO,
REASON_BRANCH,
REASON_WORKSPACE,
REASON_WORKFLOW,
REASON_AGENT,
REASON_DESCRIPTION,
} from "./task-create-dialog-footer";
import type { ButtonKind, TaskCreateDialogFooterProps } from "./task-create-dialog-footer";
const KIND_START: ButtonKind = "start-task";
const KIND_UPDATE: ButtonKind = "update";
const KIND_DEFAULT: ButtonKind = "default";
function makeProps(
overrides: Partial<TaskCreateDialogFooterProps> = {},
): TaskCreateDialogFooterProps {
return {
isSessionMode: false,
isCreateMode: true,
isEditMode: false,
isTaskStarted: false,
isPassthroughProfile: false,
isCreatingSession: false,
isCreatingTask: false,
hasTitle: true,
hasDescription: true,
hasRepositorySelection: true,
hasAllBranches: true,
agentProfileId: "agent-1",
workspaceId: "ws-1",
effectiveWorkflowId: "wf-1",
executorHint: null,
onCancel: () => {},
onUpdateWithoutAgent: () => {},
onCreateWithoutAgent: () => {},
onCreateWithPlanMode: () => {},
...overrides,
};
}
describe("computeDisabledReason (start-task)", () => {
it("returns null when nothing is missing", () => {
expect(computeDisabledReason(makeProps(), KIND_START)).toBeNull();
});
it("returns null while a submission is in flight", () => {
expect(
computeDisabledReason(makeProps({ isCreatingTask: true, hasTitle: false }), KIND_START),
).toBeNull();
});
it("flags missing title first", () => {
expect(
computeDisabledReason(
makeProps({ hasTitle: false, hasRepositorySelection: false }),
KIND_START,
),
).toBe(REASON_TITLE);
});
it("flags missing repository selection", () => {
expect(computeDisabledReason(makeProps({ hasRepositorySelection: false }), KIND_START)).toBe(
REASON_REPO,
);
});
it("flags missing branch", () => {
expect(computeDisabledReason(makeProps({ hasAllBranches: false }), KIND_START)).toBe(
REASON_BRANCH,
);
});
it("flags missing workspace in create mode", () => {
expect(computeDisabledReason(makeProps({ workspaceId: null }), KIND_START)).toBe(
REASON_WORKSPACE,
);
});
it("flags missing workflow in create mode", () => {
expect(computeDisabledReason(makeProps({ effectiveWorkflowId: null }), KIND_START)).toBe(
REASON_WORKFLOW,
);
});
it("ignores missing workspace/workflow outside create mode", () => {
expect(
computeDisabledReason(
makeProps({ isCreateMode: false, isEditMode: true, workspaceId: null }),
KIND_START,
),
).toBeNull();
});
it("flags missing agent profile for start-task button", () => {
expect(computeDisabledReason(makeProps({ agentProfileId: "" }), KIND_START)).toBe(REASON_AGENT);
});
});
describe("computeDisabledReason (update)", () => {
it("only flags missing title for the update button", () => {
expect(
computeDisabledReason(
makeProps({ hasTitle: false, hasRepositorySelection: false, agentProfileId: "" }),
KIND_UPDATE,
),
).toBe(REASON_TITLE);
});
it("returns null for update when title is present, even with other gaps", () => {
expect(
computeDisabledReason(
makeProps({ hasRepositorySelection: false, agentProfileId: "" }),
KIND_UPDATE,
),
).toBeNull();
});
});
describe("computeDisabledReason (default)", () => {
it("does not require agent outside session mode", () => {
expect(computeDisabledReason(makeProps({ agentProfileId: "" }), KIND_DEFAULT)).toBeNull();
});
it("requires agent in session mode", () => {
expect(
computeDisabledReason(makeProps({ isSessionMode: true, agentProfileId: "" }), KIND_DEFAULT),
).toBe(REASON_AGENT);
});
it("flags missing session description in session mode", () => {
expect(
computeDisabledReason(
makeProps({ isSessionMode: true, hasDescription: false }),
KIND_DEFAULT,
),
).toBe(REASON_DESCRIPTION);
});
it("does not flag missing description for passthrough profiles", () => {
expect(
computeDisabledReason(
makeProps({ isSessionMode: true, hasDescription: false, isPassthroughProfile: true }),
KIND_DEFAULT,
),
).toBeNull();
});
it("ignores base reasons in session mode to match DefaultSubmitButton disabled logic", () => {
// In session mode the default button is only disabled by !agentProfileId or
// missing description — NOT by missing title/repo/branch/workspace/workflow.
// The tooltip must not contradict that state.
expect(
computeDisabledReason(
makeProps({
isSessionMode: true,
hasTitle: false,
hasRepositorySelection: false,
hasAllBranches: false,
}),
KIND_DEFAULT,
),
).toBeNull();
});
});