6033d5717c
Co-authored-by: Kandev Agent <agent@kandev.dev>
275 lines
8.4 KiB
TypeScript
275 lines
8.4 KiB
TypeScript
import { describe, it, expect } from "vitest";
|
|
import {
|
|
computeDisabledReason,
|
|
isNativeSubmitDisabled,
|
|
resolveDisabledReason,
|
|
REASON_NO_COMPATIBLE_AGENT,
|
|
REASON_TITLE,
|
|
REASON_REPO,
|
|
REASON_BRANCH,
|
|
REASON_WORKSPACE,
|
|
REASON_WORKFLOW,
|
|
REASON_AGENT,
|
|
REASON_DESCRIPTION,
|
|
REASON_PROMPT,
|
|
} from "./task-create-dialog-footer";
|
|
import { t } from "@/lib/i18n";
|
|
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,
|
|
isCreatingSession: false,
|
|
isCreatingTask: false,
|
|
hasTitle: true,
|
|
hasDescription: true,
|
|
hasRepositorySelection: true,
|
|
hasAllBranches: true,
|
|
agentProfileId: "agent-1",
|
|
workspaceId: "ws-1",
|
|
effectiveWorkflowId: "wf-1",
|
|
executorHint: null,
|
|
noCompatibleAgent: false,
|
|
executorProfileName: null,
|
|
onCancel: () => {},
|
|
onUpdateWithoutAgent: () => {},
|
|
onCreateWithoutAgent: () => {},
|
|
onCreateWithPlanMode: () => {},
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
describe("isNativeSubmitDisabled", () => {
|
|
it("matches pending uploads and other externally blocked native submissions", () => {
|
|
expect(isNativeSubmitDisabled(makeProps({ submitBlockedReason: "upload pending" }))).toBe(true);
|
|
});
|
|
|
|
it("matches missing-profile rejection for the start-task path", () => {
|
|
expect(isNativeSubmitDisabled(makeProps({ agentProfileId: "" }))).toBe(true);
|
|
});
|
|
|
|
it("allows a complete native submission", () => {
|
|
expect(isNativeSubmitDisabled(makeProps())).toBe(false);
|
|
});
|
|
});
|
|
|
|
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("requires the prompt instead of a title in auto-title mode", () => {
|
|
expect(
|
|
computeDisabledReason(
|
|
makeProps({ autoTitle: true, hasTitle: false, hasDescription: false }),
|
|
KIND_START,
|
|
),
|
|
).toBe(REASON_PROMPT);
|
|
expect(
|
|
computeDisabledReason(
|
|
makeProps({ autoTitle: true, hasTitle: false, hasDescription: true }),
|
|
KIND_START,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
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);
|
|
});
|
|
|
|
it("flags no compatible agent for the selected executor profile", () => {
|
|
const reason = computeDisabledReason(
|
|
makeProps({
|
|
agentProfileId: "",
|
|
noCompatibleAgent: true,
|
|
executorProfileName: "Docker (sandbox)",
|
|
}),
|
|
KIND_START,
|
|
);
|
|
expect(reason).toBe(REASON_NO_COMPATIBLE_AGENT);
|
|
});
|
|
|
|
// `computeDisabledReason` is pure and returns catalog keys, so the executor
|
|
// profile name only reaches the copy once the component resolves it.
|
|
it("resolves the no-compatible-agent key with the executor profile name", () => {
|
|
const text = resolveDisabledReason(t, REASON_NO_COMPATIBLE_AGENT, "Docker (sandbox)");
|
|
expect(text).toContain("Docker (sandbox)");
|
|
expect(text).toContain("credentials");
|
|
});
|
|
|
|
it("falls back to a generic target when no executor profile is named", () => {
|
|
expect(resolveDisabledReason(t, REASON_NO_COMPATIBLE_AGENT, null)).toContain("this executor");
|
|
});
|
|
|
|
it("passes caller-supplied blocked reasons through untranslated", () => {
|
|
expect(resolveDisabledReason(t, "Uploads still in progress", null)).toBe(
|
|
"Uploads still in progress",
|
|
);
|
|
});
|
|
});
|
|
|
|
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("requires description in session mode even for CLI/passthrough profiles", () => {
|
|
// CLI-mode parity: the backend now auto-injects the prompt into the CLI's
|
|
// stdin, so the prompt is required just like in ACP mode.
|
|
expect(
|
|
computeDisabledReason(
|
|
makeProps({ isSessionMode: true, hasDescription: false }),
|
|
KIND_DEFAULT,
|
|
),
|
|
).toBe(REASON_DESCRIPTION);
|
|
});
|
|
|
|
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();
|
|
});
|
|
});
|
|
|
|
describe("computeDisabledReason (submitBlockedReason)", () => {
|
|
const REASON = "Preparing kandev repository…";
|
|
|
|
it("returns the supplied reason for start-task even when nothing is missing", () => {
|
|
expect(computeDisabledReason(makeProps({ submitBlockedReason: REASON }), KIND_START)).toBe(
|
|
REASON,
|
|
);
|
|
});
|
|
|
|
it("returns the supplied reason for default in create mode", () => {
|
|
expect(computeDisabledReason(makeProps({ submitBlockedReason: REASON }), KIND_DEFAULT)).toBe(
|
|
REASON,
|
|
);
|
|
});
|
|
|
|
it("returns the supplied reason for update mode (overrides title check)", () => {
|
|
expect(
|
|
computeDisabledReason(
|
|
makeProps({ submitBlockedReason: REASON, hasTitle: false }),
|
|
KIND_UPDATE,
|
|
),
|
|
).toBe(REASON);
|
|
});
|
|
|
|
it("still suppresses the reason while a submission is in flight", () => {
|
|
expect(
|
|
computeDisabledReason(
|
|
makeProps({ submitBlockedReason: REASON, isCreatingTask: true }),
|
|
KIND_START,
|
|
),
|
|
).toBeNull();
|
|
});
|
|
|
|
it("ignores empty/null reason and falls back to normal logic", () => {
|
|
expect(computeDisabledReason(makeProps({ submitBlockedReason: null }), KIND_START)).toBeNull();
|
|
expect(computeDisabledReason(makeProps({ submitBlockedReason: "" }), KIND_START)).toBeNull();
|
|
});
|
|
});
|