test(FN-1368): tests for planning UI and interview modal

This commit is contained in:
gsxdsm
2026-04-10 02:00:45 -07:00
parent 81dac0f3bf
commit 925bb2c43b
2 changed files with 866 additions and 0 deletions

View File

@@ -0,0 +1,406 @@
import { act, fireEvent, render, screen, waitFor } from "@testing-library/react";
import { beforeEach, describe, expect, it, vi } from "vitest";
import { MilestoneSliceInterviewModal } from "./MilestoneSliceInterviewModal";
const mockStartMilestoneInterview = vi.fn();
const mockStartSliceInterview = vi.fn();
const mockRespondToMilestoneInterview = vi.fn();
const mockRespondToSliceInterview = vi.fn();
const mockApplyMilestoneInterview = vi.fn();
const mockApplySliceInterview = vi.fn();
const mockSkipMilestoneInterview = vi.fn();
const mockSkipSliceInterview = vi.fn();
const mockConnectMilestoneInterviewStream = vi.fn();
const mockConnectSliceInterviewStream = vi.fn();
const mockAcquireSessionLock = vi.fn();
const mockReleaseSessionLock = vi.fn();
const mockForceAcquireSessionLock = vi.fn();
vi.mock("../api", () => ({
startMilestoneInterview: (...args: any[]) => mockStartMilestoneInterview(...args),
startSliceInterview: (...args: any[]) => mockStartSliceInterview(...args),
respondToMilestoneInterview: (...args: any[]) => mockRespondToMilestoneInterview(...args),
respondToSliceInterview: (...args: any[]) => mockRespondToSliceInterview(...args),
applyMilestoneInterview: (...args: any[]) => mockApplyMilestoneInterview(...args),
applySliceInterview: (...args: any[]) => mockApplySliceInterview(...args),
skipMilestoneInterview: (...args: any[]) => mockSkipMilestoneInterview(...args),
skipSliceInterview: (...args: any[]) => mockSkipSliceInterview(...args),
connectMilestoneInterviewStream: (...args: any[]) => mockConnectMilestoneInterviewStream(...args),
connectSliceInterviewStream: (...args: any[]) => mockConnectSliceInterviewStream(...args),
acquireSessionLock: (...args: any[]) => mockAcquireSessionLock(...args),
releaseSessionLock: (...args: any[]) => mockReleaseSessionLock(...args),
forceAcquireSessionLock: (...args: any[]) => mockForceAcquireSessionLock(...args),
}));
vi.mock("../hooks/useSessionLock", () => ({
useSessionLock: vi.fn(() => ({
isLockedByOther: false,
takeControl: vi.fn(),
isLoading: false,
})),
}));
vi.mock("../hooks/useAiSessionSync", () => ({
useAiSessionSync: vi.fn(() => ({
activeTabMap: new Map(),
broadcastUpdate: vi.fn(),
broadcastCompleted: vi.fn(),
broadcastLock: vi.fn(),
broadcastUnlock: vi.fn(),
broadcastHeartbeat: vi.fn(),
})),
}));
vi.mock("../utils/getSessionTabId", () => ({
getSessionTabId: vi.fn(() => "test-tab-id"),
}));
vi.mock("lucide-react", () => ({
X: () => <span data-testid="x-icon">X</span>,
Loader2: ({ className }: any) => <span data-testid="loader-icon" className={className}>Loader</span>,
CheckCircle: () => <span data-testid="check-circle-icon">CheckCircle</span>,
ArrowRight: () => <span data-testid="arrow-right-icon">ArrowRight</span>,
Sparkles: () => <span data-testid="sparkles-icon">Sparkles</span>,
ChevronRight: () => <span data-testid="chevron-right-icon">ChevronRight</span>,
ChevronDown: () => <span data-testid="chevron-down-icon">ChevronDown</span>,
Minimize2: () => <span data-testid="minimize-icon">Minimize2</span>,
}));
const SAMPLE_QUESTION = {
id: "scope",
type: "single_select" as const,
question: "What is the target scope?",
description: "Pick the size for this feature.",
options: [
{ id: "mvp", label: "MVP" },
{ id: "full", label: "Full" },
],
};
describe("MilestoneSliceInterviewModal", () => {
let streamHandlers: any;
beforeEach(() => {
mockStartMilestoneInterview.mockReset();
mockStartSliceInterview.mockReset();
mockRespondToMilestoneInterview.mockReset();
mockRespondToSliceInterview.mockReset();
mockApplyMilestoneInterview.mockReset();
mockApplySliceInterview.mockReset();
mockSkipMilestoneInterview.mockReset();
mockSkipSliceInterview.mockReset();
mockConnectMilestoneInterviewStream.mockReset();
mockConnectSliceInterviewStream.mockReset();
mockAcquireSessionLock.mockResolvedValue({ acquired: true, currentHolder: null });
mockReleaseSessionLock.mockResolvedValue(undefined);
mockForceAcquireSessionLock.mockResolvedValue(undefined);
// Setup stream handlers capture
mockConnectMilestoneInterviewStream.mockImplementation((sessionId, projectId, handlers) => {
streamHandlers = handlers;
return {
close: vi.fn(),
isConnected: vi.fn(() => true),
};
});
mockConnectSliceInterviewStream.mockImplementation((sessionId, projectId, handlers) => {
streamHandlers = handlers;
return {
close: vi.fn(),
isConnected: vi.fn(() => true),
};
});
});
describe("initial view", () => {
it("renders with correct title for milestone", () => {
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
expect(screen.getByText("Plan Milestone: Test Milestone")).toBeDefined();
});
it("renders with correct title for slice", () => {
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="slice"
targetId="SL-001"
targetTitle="Test Slice"
projectId="test-project"
/>
);
expect(screen.getByText("Plan Slice: Test Slice")).toBeDefined();
});
it("shows three action buttons: Start Interview, Use Mission Context, Cancel", () => {
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
expect(screen.getByText("Start Interview")).toBeDefined();
expect(screen.getByText("Use Mission Context")).toBeDefined();
expect(screen.getByText("Cancel")).toBeDefined();
});
it("calls onClose when Cancel is clicked", () => {
const onClose = vi.fn();
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={onClose}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Cancel"));
expect(onClose).toHaveBeenCalled();
});
});
describe("Start Interview button", () => {
it("calls startMilestoneInterview for targetType=milestone", async () => {
mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" });
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Start Interview"));
await waitFor(() => {
expect(mockStartMilestoneInterview).toHaveBeenCalledWith("MS-001", "test-project");
});
});
it("calls startSliceInterview for targetType=slice", async () => {
mockStartSliceInterview.mockResolvedValue({ sessionId: "session-123" });
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="slice"
targetId="SL-001"
targetTitle="Test Slice"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Start Interview"));
await waitFor(() => {
expect(mockStartSliceInterview).toHaveBeenCalledWith("SL-001", "test-project");
});
});
it("shows loading state after clicking Start Interview", async () => {
mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" });
mockConnectMilestoneInterviewStream.mockReturnValue({
close: vi.fn(),
isConnected: vi.fn(() => false),
});
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Start Interview"));
await waitFor(() => {
expect(screen.getByText("Preparing next question...")).toBeDefined();
});
});
});
describe("Use Mission Context button", () => {
it("calls skipMilestoneInterview and onApplied for targetType=milestone", async () => {
mockSkipMilestoneInterview.mockResolvedValue({ id: "MS-001", title: "Test Milestone" });
const onApplied = vi.fn();
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={onApplied}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Use Mission Context"));
await waitFor(() => {
expect(mockSkipMilestoneInterview).toHaveBeenCalledWith("MS-001", "test-project");
expect(onApplied).toHaveBeenCalled();
});
});
it("calls skipSliceInterview and onApplied for targetType=slice", async () => {
mockSkipSliceInterview.mockResolvedValue({ id: "SL-001", title: "Test Slice" });
const onApplied = vi.fn();
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={onApplied}
targetType="slice"
targetId="SL-001"
targetTitle="Test Slice"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Use Mission Context"));
await waitFor(() => {
expect(mockSkipSliceInterview).toHaveBeenCalledWith("SL-001", "test-project");
expect(onApplied).toHaveBeenCalled();
});
});
it("does not call any interview API when Cancel is clicked", () => {
const onClose = vi.fn();
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={onClose}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Cancel"));
expect(mockStartMilestoneInterview).not.toHaveBeenCalled();
expect(mockSkipMilestoneInterview).not.toHaveBeenCalled();
expect(onClose).toHaveBeenCalled();
});
});
describe("question flow", () => {
it("shows question after interview starts and AI responds", async () => {
mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" });
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Start Interview"));
// Simulate AI response with question
await waitFor(() => {
// Loading state should appear first
expect(screen.getByText(/Preparing next question/)).toBeDefined();
});
// Simulate question event from stream
act(() => {
streamHandlers.onQuestion(SAMPLE_QUESTION);
});
await waitFor(() => {
expect(screen.getByText("What is the target scope?")).toBeDefined();
expect(screen.getByText("Pick the size for this feature.")).toBeDefined();
});
});
});
describe("summary and apply", () => {
it("shows summary view after interview completes", async () => {
mockStartMilestoneInterview.mockResolvedValue({ sessionId: "session-123" });
render(
<MilestoneSliceInterviewModal
isOpen={true}
onClose={vi.fn()}
onApplied={vi.fn()}
targetType="milestone"
targetId="MS-001"
targetTitle="Test Milestone"
projectId="test-project"
/>
);
fireEvent.click(screen.getByText("Start Interview"));
// Wait for loading state
await waitFor(() => {
expect(screen.getByText(/Preparing next question/)).toBeDefined();
});
// Simulate summary event
act(() => {
if (streamHandlers?.onSummary) {
streamHandlers.onSummary({
description: "Refined description",
});
}
});
// Give React time to update
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 100));
});
// Summary should show Refined Scope header
expect(screen.getByText("Refined Scope")).toBeDefined();
});
});
});

View File

@@ -5,6 +5,10 @@ import { MissionManager } from "../MissionManager";
const mockFetchAiSession = vi.fn();
const mockCancelMissionInterview = vi.fn();
const mockConnectMissionInterviewStream = vi.fn();
const mockPreviewEnrichedDescription = vi.fn();
const mockSkipMilestoneInterview = vi.fn();
const mockSkipSliceInterview = vi.fn();
const mockTriageFeature = vi.fn();
vi.mock("../../api", async () => {
const actual = await vi.importActual<typeof import("../../api")>("../../api");
@@ -13,9 +17,38 @@ vi.mock("../../api", async () => {
fetchAiSession: (...args: any[]) => mockFetchAiSession(...args),
cancelMissionInterview: (...args: any[]) => mockCancelMissionInterview(...args),
connectMissionInterviewStream: (...args: any[]) => mockConnectMissionInterviewStream(...args),
previewEnrichedDescription: (...args: any[]) => mockPreviewEnrichedDescription(...args),
skipMilestoneInterview: (...args: any[]) => mockSkipMilestoneInterview(...args),
skipSliceInterview: (...args: any[]) => mockSkipSliceInterview(...args),
triageFeature: (...args: any[]) => mockTriageFeature(...args),
};
});
vi.mock("lucide-react", () => ({
X: () => <span data-testid="x-icon">X</span>,
Plus: () => <span data-testid="plus-icon">+</span>,
Pencil: () => <span data-testid="pencil-icon">Pencil</span>,
Trash2: () => <span data-testid="trash-icon">Trash</span>,
ChevronRight: () => <span data-testid="chevron-right-icon">ChevronRight</span>,
ChevronDown: () => <span data-testid="chevron-down-icon">ChevronDown</span>,
ChevronLeft: () => <span data-testid="chevron-left-icon">ChevronLeft</span>,
Target: () => <span data-testid="target-icon">Target</span>,
Layers: () => <span data-testid="layers-icon">Layers</span>,
Package: () => <span data-testid="package-icon">Package</span>,
Box: () => <span data-testid="box-icon">Box</span>,
Check: () => <span data-testid="check-icon">Check</span>,
Loader2: ({ className }: any) => <span data-testid="loader-icon" className={className}>Loader</span>,
Link: () => <span data-testid="link-icon">Link</span>,
Unlink: () => <span data-testid="unlink-icon">Unlink</span>,
Play: () => <span data-testid="play-icon">Play</span>,
Square: () => <span data-testid="square-icon">Square</span>,
Sparkles: () => <span data-testid="sparkles-icon">Sparkles</span>,
Zap: () => <span data-testid="zap-icon">Zap</span>,
Activity: () => <span data-testid="activity-icon">Activity</span>,
FileText: () => <span data-testid="file-text-icon">FileText</span>,
Minimize2: () => <span data-testid="minimize-icon">Minimize2</span>,
}));
// Mock data
const mockMissions = [
{
@@ -58,6 +91,7 @@ const mockMissionDetail = {
title: "Database Schema",
description: "Set up auth tables",
status: "planning",
interviewState: "not_started",
dependencies: [] as string[],
slices: [
{
@@ -65,6 +99,7 @@ const mockMissionDetail = {
title: "User Tables",
description: "Create user tables",
status: "pending",
planState: "not_started",
features: [
{
id: "F-001",
@@ -1492,6 +1527,431 @@ describe("MissionManager", () => {
});
});
// ── Plan Buttons & Interview ──
describe("plan buttons and interview modal", () => {
const mockMissionWithPlanData = {
id: "M-PLAN1",
title: "Plan Test Mission",
description: "Test mission for plan buttons",
status: "active",
autopilotEnabled: false,
autopilotState: "inactive",
milestones: [
{
id: "MS-PLAN1",
title: "Test Milestone",
description: "A milestone for testing",
status: "active",
interviewState: "not_started",
dependencies: [] as string[],
slices: [
{
id: "SL-PLAN1",
title: "Test Slice",
description: "A slice for testing",
status: "pending",
planState: "not_started",
features: [
{
id: "F-PLAN1",
title: "Test Feature",
description: "A feature for testing",
acceptanceCriteria: "Test criteria",
status: "defined",
taskId: null,
sliceId: "SL-PLAN1",
missionId: "M-PLAN1",
},
],
milestoneId: "MS-PLAN1",
missionId: "M-PLAN1",
},
{
id: "SL-PLAN2",
title: "Completed Slice",
description: "A completed slice",
status: "complete",
planState: "planned",
features: [],
milestoneId: "MS-PLAN1",
missionId: "M-PLAN1",
},
],
missionId: "M-PLAN1",
},
{
id: "MS-PLAN2",
title: "Completed Milestone",
description: "A completed milestone",
status: "complete",
interviewState: "completed",
dependencies: [] as string[],
slices: [],
missionId: "M-PLAN1",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
function createPlanFetchMock() {
return vi.fn((url: string) => {
// Return mission list (array) for the missions endpoint
if (url.match(/\/api\/missions$/) || url.match(/\/api\/missions\?/)) {
return Promise.resolve(mockApiResponse([mockMissionWithPlanData]));
}
// Return mission detail for specific mission
if (url.includes("/api/missions/")) {
return Promise.resolve(mockApiResponse(mockMissionWithPlanData));
}
return Promise.resolve(mockApiResponse([]));
}) as unknown as typeof fetch;
}
beforeEach(() => {
mockFetchAiSession.mockReset();
mockCancelMissionInterview.mockReset();
mockConnectMissionInterviewStream.mockReset();
mockPreviewEnrichedDescription.mockReset();
mockSkipMilestoneInterview.mockReset();
mockSkipSliceInterview.mockReset();
mockTriageFeature.mockReset();
mockFetchAiSession.mockResolvedValue(null);
mockCancelMissionInterview.mockResolvedValue(undefined);
mockConnectMissionInterviewStream.mockReturnValue({ close: vi.fn(), isConnected: vi.fn(() => false) });
mockPreviewEnrichedDescription.mockReset();
mockSkipMilestoneInterview.mockResolvedValue({});
mockSkipSliceInterview.mockResolvedValue({});
mockTriageFeature.mockResolvedValue({});
});
it("shows Plan button next to milestones that are not complete", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Should show Plan button for the active milestone
const planButton = screen.getByTitle("Plan milestone");
expect(planButton).toBeDefined();
});
});
it("does NOT show Plan button for completed milestones", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Find the "Completed Milestone" section
expect(screen.getByText("Completed Milestone")).toBeDefined();
});
// Should not have a Plan button for completed milestone
const completedMilestone = screen.getByText("Completed Milestone").closest(".mission-milestone");
expect(completedMilestone).toBeDefined();
});
it("shows Plan button next to slices that are not complete", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Should show Plan button for the pending slice
const planButton = screen.getByTitle("Plan slice");
expect(planButton).toBeDefined();
});
});
it("does NOT show Plan button for completed slices", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Find the "Completed Slice" section
expect(screen.getByText("Completed Slice")).toBeDefined();
});
// Should not have a Plan button for completed slice
const completedSlice = screen.getByText("Completed Slice").closest(".mission-slice");
expect(completedSlice).toBeDefined();
});
it("shows planning state indicator for milestones", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Should show a plan state indicator
const indicators = document.querySelectorAll(".mission-plan-state-indicator");
expect(indicators.length).toBeGreaterThan(0);
});
});
it("shows planning state indicator for slices", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
// Should show plan state indicator for the slice
const indicators = document.querySelectorAll(".mission-plan-state-indicator");
expect(indicators.length).toBeGreaterThan(0);
});
});
it("clicking Plan button opens the MilestoneSliceInterviewModal", async () => {
globalThis.fetch = createPlanFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Plan Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Plan Test Mission"));
await waitFor(() => {
const planButton = screen.getByTitle("Plan milestone");
expect(planButton).toBeDefined();
});
// Click Plan button
fireEvent.click(screen.getByTitle("Plan milestone"));
// Modal should open
await waitFor(() => {
expect(screen.getByTestId("milestone-slice-interview-modal")).toBeDefined();
});
});
});
// ── Triage Preview ──
describe("triage preview", () => {
const mockMissionWithFeature = {
id: "M-TRIAGE1",
title: "Triage Test Mission",
description: "Test mission for triage preview",
status: "active",
autopilotEnabled: false,
autopilotState: "inactive",
milestones: [
{
id: "MS-TRIAGE1",
title: "Test Milestone",
description: "A milestone for testing",
status: "active",
interviewState: "not_started",
dependencies: [] as string[],
slices: [
{
id: "SL-TRIAGE1",
title: "Test Slice",
description: "A slice for testing",
status: "pending",
planState: "not_started",
features: [
{
id: "F-TRIAGE1",
title: "Test Feature",
description: "A feature for testing triage preview",
acceptanceCriteria: "Test criteria",
status: "defined",
taskId: null,
sliceId: "SL-TRIAGE1",
missionId: "M-TRIAGE1",
},
],
milestoneId: "MS-TRIAGE1",
missionId: "M-TRIAGE1",
},
],
missionId: "M-TRIAGE1",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
function createTriageFetchMock() {
return vi.fn((url: string) => {
// Return mission list (array) for the missions endpoint
if (url.match(/\/api\/missions$/) || url.match(/\/api\/missions\?/)) {
return Promise.resolve(mockApiResponse([mockMissionWithFeature]));
}
// Return mission detail for specific mission
if (url.includes("/api/missions/")) {
return Promise.resolve(mockApiResponse(mockMissionWithFeature));
}
return Promise.resolve(mockApiResponse([]));
}) as unknown as typeof fetch;
}
beforeEach(() => {
mockFetchAiSession.mockReset();
mockCancelMissionInterview.mockReset();
mockConnectMissionInterviewStream.mockReset();
mockPreviewEnrichedDescription.mockReset();
mockSkipMilestoneInterview.mockReset();
mockSkipSliceInterview.mockReset();
mockTriageFeature.mockReset();
mockFetchAiSession.mockResolvedValue(null);
mockCancelMissionInterview.mockResolvedValue(undefined);
mockConnectMissionInterviewStream.mockReturnValue({ close: vi.fn(), isConnected: vi.fn(() => false) });
mockPreviewEnrichedDescription.mockResolvedValue({ description: "Enriched description with more details" });
mockSkipMilestoneInterview.mockResolvedValue({});
mockSkipSliceInterview.mockResolvedValue({});
mockTriageFeature.mockResolvedValue({});
});
it("shows triage preview when clicking triage button", async () => {
globalThis.fetch = createTriageFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Triage Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Triage Test Mission"));
await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined();
});
// Click triage button
fireEvent.click(screen.getByTitle("Triage — create task"));
// Preview should appear
await waitFor(() => {
expect(screen.getByText("Enriched Description Preview")).toBeDefined();
expect(screen.getByText("Enriched description with more details")).toBeDefined();
});
});
it("Create Task button in preview confirms triage", async () => {
globalThis.fetch = createTriageFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Triage Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Triage Test Mission"));
await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined();
});
// Click triage button to show preview
fireEvent.click(screen.getByTitle("Triage — create task"));
await waitFor(() => {
expect(screen.getByText("Enriched Description Preview")).toBeDefined();
});
// Click Create Task
fireEvent.click(screen.getByText("Create Task"));
// triageFeature should have been called
await waitFor(() => {
expect(mockTriageFeature).toHaveBeenCalled();
});
});
it("Cancel button in preview dismisses without creating task", async () => {
globalThis.fetch = createTriageFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Triage Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Triage Test Mission"));
await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined();
});
// Click triage button to show preview
fireEvent.click(screen.getByTitle("Triage — create task"));
await waitFor(() => {
expect(screen.getByText("Enriched Description Preview")).toBeDefined();
});
// Click Cancel
fireEvent.click(screen.getByText("Cancel"));
// Preview should be gone
await waitFor(() => {
expect(screen.queryByText("Enriched Description Preview")).toBeNull();
});
// triageFeature should NOT have been called
expect(mockTriageFeature).not.toHaveBeenCalled();
});
it("falls back to direct triage when preview endpoint fails", async () => {
// Mock preview to reject
mockPreviewEnrichedDescription.mockRejectedValue(new Error("Preview not available"));
globalThis.fetch = createTriageFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Triage Test Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Triage Test Mission"));
await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined();
});
// Click triage button - should fall back to direct triage
fireEvent.click(screen.getByTitle("Triage — create task"));
// Should call triageFeature directly
await waitFor(() => {
expect(mockTriageFeature).toHaveBeenCalled();
});
});
});
// ── Autopilot UI ──
describe("autopilot UI", () => {
const autopilotMockMissions = [