284 lines
10 KiB
TypeScript
284 lines
10 KiB
TypeScript
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
import type { WorkflowStep } from "@/lib/types/http";
|
|
import { WorkflowStepAgentProfileSelector } from "./workflow-step-agent-profile-selector";
|
|
|
|
const breakpoint = { isMobile: false };
|
|
const ARIA_PRESSED = "aria-pressed";
|
|
const ARIA_TRUE = "true";
|
|
const START_NEW_TEST_ID = "step-1-profile-session-start-new";
|
|
const LIFECYCLE_TEST_ID = "step-1-profile-session-lifecycle-select";
|
|
const SOURCE_TARGET_TEST_ID = "step-2-session-target-step-step-1";
|
|
|
|
vi.mock("@/hooks/use-responsive-breakpoint", () => ({
|
|
useResponsiveBreakpoint: () => breakpoint,
|
|
}));
|
|
|
|
vi.mock("@/hooks/domains/settings/use-healthy-agent-profiles", () => ({
|
|
useHealthyAgentProfiles: () => [
|
|
{
|
|
id: "profile-a",
|
|
label: "Codex • Fast",
|
|
agent_id: "agent-a",
|
|
agent_name: "codex",
|
|
cli_passthrough: false,
|
|
},
|
|
{
|
|
id: "profile-b",
|
|
label: "Claude • Review",
|
|
agent_id: "agent-b",
|
|
agent_name: "claude",
|
|
cli_passthrough: false,
|
|
},
|
|
],
|
|
}));
|
|
|
|
vi.mock("@/components/agent-logo", () => ({
|
|
AgentLogo: ({ agentName }: { agentName: string }) => (
|
|
<span data-testid={`agent-logo-${agentName}`} aria-hidden="true" />
|
|
),
|
|
}));
|
|
|
|
const step = {
|
|
id: "step-1",
|
|
workflow_id: "workflow-1",
|
|
name: "Review",
|
|
position: 0,
|
|
color: "bg-blue-500",
|
|
agent_profile_id: "profile-a",
|
|
profile_session_start_policy: "reuse",
|
|
profile_session_end_policy: "complete",
|
|
created_at: "",
|
|
updated_at: "",
|
|
} as WorkflowStep;
|
|
|
|
beforeEach(() => {
|
|
breakpoint.isMobile = false;
|
|
});
|
|
|
|
afterEach(cleanup);
|
|
|
|
function renderSelector(
|
|
overrides: Partial<WorkflowStep> = {},
|
|
readOnly = false,
|
|
steps: WorkflowStep[] = [],
|
|
) {
|
|
const onUpdate = vi.fn();
|
|
const currentStep = { ...step, ...overrides };
|
|
render(
|
|
<WorkflowStepAgentProfileSelector
|
|
step={currentStep}
|
|
savedStep={step}
|
|
steps={steps}
|
|
onUpdate={onUpdate}
|
|
readOnly={readOnly}
|
|
/>,
|
|
);
|
|
return { onUpdate, trigger: screen.getByTestId("step-agent-profile-select") };
|
|
}
|
|
|
|
describe("WorkflowStepAgentProfileSelector", () => {
|
|
it("searches profiles and updates the selected profile from the desktop popover", async () => {
|
|
const { onUpdate, trigger } = renderSelector();
|
|
|
|
fireEvent.click(trigger);
|
|
expect(screen.getByPlaceholderText("Search agent profiles...")).toBeTruthy();
|
|
expect(screen.getAllByText("Codex • Fast").length).toBeGreaterThan(1);
|
|
|
|
fireEvent.click(screen.getByTestId("step-1-profile-option-profile-b"));
|
|
|
|
expect(onUpdate).toHaveBeenCalledWith({ agent_profile_id: "profile-b" });
|
|
await waitFor(() => expect(document.activeElement).toBe(trigger));
|
|
});
|
|
|
|
it("uses nested lifecycle navigation and updates start and end independently", () => {
|
|
const { onUpdate, trigger } = renderSelector();
|
|
|
|
fireEvent.click(trigger);
|
|
fireEvent.click(screen.getByTestId(LIFECYCLE_TEST_ID));
|
|
expect(screen.getByText("When this step starts:")).toBeTruthy();
|
|
expect(screen.getByText("When this step ends:")).toBeTruthy();
|
|
expect(
|
|
screen.getByTestId("step-1-profile-session-start-reuse").getAttribute(ARIA_PRESSED),
|
|
).toBe(ARIA_TRUE);
|
|
expect(
|
|
screen.getByTestId("step-1-profile-session-end-complete").getAttribute(ARIA_PRESSED),
|
|
).toBe(ARIA_TRUE);
|
|
|
|
fireEvent.click(screen.getByTestId(START_NEW_TEST_ID));
|
|
fireEvent.click(screen.getByTestId("step-1-profile-session-end-park"));
|
|
|
|
expect(onUpdate).toHaveBeenNthCalledWith(1, { profile_session_start_policy: "new" });
|
|
expect(onUpdate).toHaveBeenNthCalledWith(2, { profile_session_end_policy: "park" });
|
|
});
|
|
|
|
it("selects park on end when the lifecycle policy is unset", () => {
|
|
const { trigger } = renderSelector({ profile_session_end_policy: undefined });
|
|
|
|
fireEvent.click(trigger);
|
|
fireEvent.click(screen.getByTestId(LIFECYCLE_TEST_ID));
|
|
|
|
expect(screen.getByTestId("step-1-profile-session-end-park").getAttribute(ARIA_PRESSED)).toBe(
|
|
ARIA_TRUE,
|
|
);
|
|
expect(
|
|
screen.getByTestId("step-1-profile-session-end-complete").getAttribute(ARIA_PRESSED),
|
|
).toBe("false");
|
|
});
|
|
|
|
it("renders the same nested surface in the mobile drawer", () => {
|
|
breakpoint.isMobile = true;
|
|
const { trigger } = renderSelector({
|
|
profile_session_start_policy: "new",
|
|
profile_session_end_policy: undefined,
|
|
});
|
|
|
|
fireEvent.click(trigger);
|
|
expect(screen.getByRole("heading", { name: "Agent Profile" })).toBeTruthy();
|
|
fireEvent.click(screen.getByTestId(LIFECYCLE_TEST_ID));
|
|
expect(screen.getByRole("heading", { name: "Session lifecycle" })).toBeTruthy();
|
|
expect(screen.getByTestId(START_NEW_TEST_ID).getAttribute(ARIA_PRESSED)).toBe(ARIA_TRUE);
|
|
expect(screen.getByTestId("step-1-profile-session-end-park").getAttribute(ARIA_PRESSED)).toBe(
|
|
ARIA_TRUE,
|
|
);
|
|
});
|
|
|
|
it("keeps lifecycle editing available when conditional session settings are configured", () => {
|
|
const { onUpdate, trigger } = renderSelector({
|
|
events: { on_enter: [{ type: "configure_session", config: { rules: [] } }] },
|
|
});
|
|
|
|
expect((trigger as HTMLButtonElement).disabled).toBe(false);
|
|
fireEvent.click(trigger);
|
|
expect(screen.getByTestId("step-1-agent-profile-help")).toBeTruthy();
|
|
expect(
|
|
screen.getByTestId("step-1-profile-option-profile-b").getAttribute("data-disabled"),
|
|
).toBe("true");
|
|
|
|
fireEvent.click(screen.getByTestId(LIFECYCLE_TEST_ID));
|
|
expect((screen.getByTestId(START_NEW_TEST_ID) as HTMLButtonElement).disabled).toBe(false);
|
|
fireEvent.click(screen.getByTestId(START_NEW_TEST_ID));
|
|
expect(onUpdate).toHaveBeenCalledWith({ profile_session_start_policy: "new" });
|
|
});
|
|
});
|
|
|
|
describe("read-only workflow selector", () => {
|
|
it("keeps the current profile and policy visible while read-only", () => {
|
|
const { trigger } = renderSelector(
|
|
{ profile_session_start_policy: "new", profile_session_end_policy: "park" },
|
|
true,
|
|
);
|
|
expect((trigger as HTMLButtonElement).disabled).toBe(false);
|
|
expect(trigger.textContent).toContain("Codex • Fast");
|
|
expect(trigger.textContent).toContain("New on start");
|
|
expect(trigger.textContent).toContain("Park on end");
|
|
expect(screen.getByTestId("agent-logo-codex")).toBeTruthy();
|
|
});
|
|
|
|
it("allows inspection while disabling selector mutations", () => {
|
|
const { onUpdate, trigger } = renderSelector(undefined, true);
|
|
fireEvent.click(trigger);
|
|
expect(
|
|
screen.getByTestId("step-1-profile-option-profile-b").getAttribute("data-disabled"),
|
|
).toBe("true");
|
|
fireEvent.click(screen.getByTestId(LIFECYCLE_TEST_ID));
|
|
expect((screen.getByTestId(START_NEW_TEST_ID) as HTMLButtonElement).disabled).toBe(true);
|
|
fireEvent.click(screen.getByTestId(START_NEW_TEST_ID));
|
|
expect(onUpdate).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("keeps repair inspection available while disabling read-only repairs", () => {
|
|
const source = { ...step, position: 2 } as WorkflowStep;
|
|
const destination = {
|
|
...step,
|
|
id: "step-2",
|
|
position: 1,
|
|
agent_profile_id: "",
|
|
session_target: { kind: "step", step_id: "step-1" },
|
|
} as WorkflowStep;
|
|
const { trigger } = renderSelector(destination, true, [source, destination]);
|
|
expect(
|
|
(screen.getByTestId("workflow-session-target-clear") as HTMLButtonElement).disabled,
|
|
).toBe(true);
|
|
fireEvent.click(screen.getByTestId("workflow-session-target-choose-another"));
|
|
expect(screen.getByPlaceholderText("Search agent profiles...")).toBeTruthy();
|
|
expect(trigger).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe("workflow session targets", () => {
|
|
it("selects the initial workflow session and clears the profile override", () => {
|
|
const { onUpdate, trigger } = renderSelector();
|
|
|
|
fireEvent.click(trigger);
|
|
fireEvent.click(screen.getByTestId("step-1-session-target-initial"));
|
|
|
|
expect(onUpdate).toHaveBeenCalledWith({
|
|
session_target: { kind: "initial" },
|
|
agent_profile_id: "",
|
|
});
|
|
});
|
|
|
|
it("offers earlier direct-profile steps as session targets", () => {
|
|
const destination = {
|
|
...step,
|
|
id: "step-2",
|
|
name: "Review",
|
|
position: 1,
|
|
agent_profile_id: "",
|
|
session_target: null,
|
|
} as WorkflowStep;
|
|
|
|
const { onUpdate, trigger } = renderSelector(destination, false, [step, destination]);
|
|
|
|
fireEvent.click(trigger);
|
|
expect(screen.getByTestId(SOURCE_TARGET_TEST_ID)).toBeTruthy();
|
|
fireEvent.click(screen.getByTestId(SOURCE_TARGET_TEST_ID));
|
|
|
|
expect(onUpdate).toHaveBeenCalledWith({
|
|
session_target: { kind: "step", step_id: "step-1" },
|
|
agent_profile_id: "",
|
|
});
|
|
});
|
|
|
|
it("keeps target choices available in the mobile picker", () => {
|
|
breakpoint.isMobile = true;
|
|
const destination = {
|
|
...step,
|
|
id: "step-2",
|
|
position: 1,
|
|
agent_profile_id: "",
|
|
session_target: null,
|
|
} as WorkflowStep;
|
|
const { onUpdate, trigger } = renderSelector(destination, false, [step, destination]);
|
|
|
|
fireEvent.click(trigger);
|
|
expect(screen.getByTestId(SOURCE_TARGET_TEST_ID)).toBeTruthy();
|
|
fireEvent.click(screen.getByTestId(SOURCE_TARGET_TEST_ID));
|
|
|
|
expect(onUpdate).toHaveBeenCalledWith({
|
|
session_target: { kind: "step", step_id: "step-1" },
|
|
agent_profile_id: "",
|
|
});
|
|
});
|
|
|
|
it("shows repair actions when a source step is no longer earlier", () => {
|
|
const source = { ...step, position: 2 } as WorkflowStep;
|
|
const destination = {
|
|
...step,
|
|
id: "step-2",
|
|
name: "Review",
|
|
position: 1,
|
|
agent_profile_id: "",
|
|
session_target: { kind: "step", step_id: "step-1" },
|
|
} as WorkflowStep;
|
|
const { onUpdate } = renderSelector(destination, false, [source, destination]);
|
|
|
|
expect(screen.getByTestId("workflow-session-target-repair")).toBeTruthy();
|
|
expect(screen.getByText("The source step must stay earlier than this step.")).toBeTruthy();
|
|
|
|
fireEvent.click(screen.getByTestId("workflow-session-target-clear"));
|
|
expect(onUpdate).toHaveBeenCalledWith({ session_target: null, agent_profile_id: "" });
|
|
});
|
|
});
|