Files
fusion/packages/dashboard/app/components/__tests__/MissionManager.test.tsx
gsxdsm 3031d05a68 fix(dashboard): clear all 661 test-file type errors
Production typecheck (tsconfig.json + tsconfig.app.json) was already
clean, but a third config that includes test files surfaced 661 errors
across 60+ test files — accumulated drift between mock fixtures and
production types. Six parallel typescript-pro agents fixed every one
without touching production code.

Per-scope before/after (errors → 0):
  ChatView                                                     183
  Mailbox + Agent suite (5 files)                              156
  Task / Modal suite (6 files)                                 127
  App + small components (12 files)                             96
  Hooks + api/auth (8 files)                                    48
  Long tail (32 files)                                          51
  -----------------------------------------------------------------
  Total                                                        661

Major fix categories:
- Untyped state objects inferring `never[]` / `null` literals (root
  cause of ~120 errors in ChatView alone — added a single
  `UseChatReturn` annotation)
- Mock objects missing fields that became required: `WorkflowStep.mode`,
  `ChatMessage.thinkingOutput / metadata`, `ChatSession.projectId`,
  `Task.log`, `ProjectHealth` fields, `PtyTerminalSessionInfo.createdAt`,
  `Agent.metadata`, `InboxResponse.total`, etc.
- Mock objects with stale fields that no longer exist:
  `AgentBudgetStatus.budgetPeriod`, `truncated` on log responses,
  `OutboxResponse.unreadCount`, `MergeResult.source/target/details`
- Modal props that became required (e.g. `PlanningModeModal.onTasksCreated`)
- String literals not in narrowed unions (`Column`, `WorkflowStepPhase`,
  `InsightStatus`, `AgentLogType`, etc.)
- `querySelector` returning `Element` cast to `HTMLElement` for
  `@testing-library/react`'s `within()`
- Vitest mock typing: `.mock.calls` access needing `vi.mocked(...)`,
  zero-param tuple handling, generic `vi.fn(() => [])` inferring
  `never[]`

Helpers introduced in test files (no shared infra):
- `makeSettings(overrides)` in ModelSelectorTab.test.tsx
- `makePromptOverrides(overrides)` in AgentPromptsManager.test.tsx
- `FileBrowserTestOverrides` type alias in FileBrowser.test.tsx
- `makeInboxResponse / makeOutboxResponse` in MailboxView.test.tsx

Verification:
- tsc -p tsconfig.json:        exit 0
- tsc -p tsconfig.app.json:    exit 0
- tsc -p tsconfig.test-check.json (new — includes test files): exit 0
- vitest run:                  9639 / 9641 (2 pre-existing failures
                               in terminal-mobile-keyboard-layout.test.ts
                               unrelated to this work; verified via
                               `git stash` + run on clean HEAD)

Adds packages/dashboard/tsconfig.test-check.json to keep this regression
guard available locally — same as tsconfig.app.json minus the test
exclude.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-27 19:59:52 -07:00

2831 lines
100 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, fireEvent, waitFor, act } from "@testing-library/react";
import { MissionManager } from "../MissionManager";
const mockFetchAiSession = vi.fn();
const mockFetchAiSessions = 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");
return {
...actual,
fetchAiSession: (...args: any[]) => mockFetchAiSession(...args),
fetchAiSessions: (...args: any[]) => mockFetchAiSessions(...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),
fetchMilestoneValidationTelemetry: (milestoneId: string, projectId?: string) => actual.fetchMilestoneValidationTelemetry(milestoneId, projectId),
fetchModels: () => Promise.resolve({ models: [], favoriteProviders: [], favoriteModels: [] }),
};
});
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>,
Lock: () => <span data-testid="lock-icon">Lock</span>,
RefreshCw: ({ className }: any) => <span data-testid="refresh-icon" className={className}>Refresh</span>,
AlertCircle: () => <span data-testid="alert-circle-icon">AlertCircle</span>,
}));
// Mock data
const mockMissions = [
{
id: "M-001",
title: "Build Auth System",
description: "Complete authentication flow",
status: "planning",
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "M-002",
title: "API Redesign",
description: "Redesign the REST API",
status: "active",
autopilotEnabled: true,
autopilotState: "watching",
milestones: [],
summary: {
totalMilestones: 2,
completedMilestones: 1,
totalFeatures: 5,
completedFeatures: 3,
progressPercent: 60,
},
createdAt: "2026-01-02T00:00:00.000Z",
updatedAt: "2026-01-02T00:00:00.000Z",
},
];
const mockMissionDetail = {
id: "M-001",
title: "Build Auth System",
description: "Complete authentication flow",
status: "planning",
milestones: [
{
id: "MS-001",
title: "Database Schema",
description: "Set up auth tables",
status: "planning",
interviewState: "not_started",
dependencies: [] as string[],
slices: [
{
id: "SL-001",
title: "User Tables",
description: "Create user tables",
status: "pending",
planState: "not_started",
features: [
{
id: "F-001",
title: "User model",
description: "Create user model",
acceptanceCriteria: "Model exists with required fields",
status: "defined",
taskId: null,
sliceId: "SL-001",
missionId: "M-001",
},
],
milestoneId: "MS-001",
missionId: "M-001",
},
],
missionId: "M-001",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
const mockAutopilotStatus = {
enabled: false,
state: "inactive",
watched: false,
};
const mockMilestoneValidationRollup = {
milestoneId: "MS-001",
totalAssertions: 0,
passedAssertions: 0,
failedAssertions: 0,
blockedAssertions: 0,
pendingAssertions: 0,
unlinkedAssertions: 0,
state: "not_started" as const,
};
/** Extended mock telemetry for parity tests — mirrors FN-1569 schema */
const mockMilestoneValidationTelemetryWithRounds = {
validationContract: {
assertions: [
{ id: "CA-001", title: "Auth works", assertion: "Users can log in", status: "pending" as const, orderIndex: 0 },
{ id: "CA-002", title: "Session persists", assertion: "Token refresh works", status: "pending" as const, orderIndex: 1 },
],
featureFulfillment: {
"F-001": { assertionIds: ["CA-001"], featureTitle: "User model", featureStatus: "in-progress" },
},
},
validationTelemetry: {
validationRounds: [
{
roundId: "VR-001",
featureId: "F-001",
featureTitle: "User model",
validatorStatus: "failed" as const,
implementationAttempt: 1,
validatorAttempt: 2, // retry count (validatorAttempt = retry count)
failedAssertionIds: ["CA-001"],
generatedFixFeatureIds: [],
startedAt: "2026-04-10T09:00:00.000Z",
completedAt: "2026-04-10T09:05:00.000Z",
},
{
roundId: "VR-002",
featureId: "F-001",
featureTitle: "User model",
validatorStatus: "failed" as const,
implementationAttempt: 2,
validatorAttempt: 3, // higher retry count — iterating surface
failedAssertionIds: ["CA-002"],
generatedFixFeatureIds: [],
startedAt: "2026-04-10T09:10:00.000Z",
completedAt: "2026-04-10T09:15:00.000Z",
},
],
lastValidatorStatus: "failed" as const,
totalRuns: 2,
},
fixFeatures: [
{
id: "FF-001",
title: "Fix: token refresh",
sourceFeatureId: "F-001",
runId: "VR-001",
failedAssertionIds: ["CA-001"],
status: "defined" as const,
loopState: "idle" as const,
},
],
rollup: {
milestoneId: "MS-001",
totalAssertions: 2,
passedAssertions: 0,
failedAssertions: 2,
blockedAssertions: 0,
pendingAssertions: 0,
unlinkedAssertions: 0,
state: "failed" as const,
},
};
/** Blocked milestone telemetry — mirrors FN-1569 blocked state */
const mockBlockedMilestoneTelemetry = {
validationContract: {
assertions: [
{ id: "CA-003", title: "API reachable", assertion: "External API responds", status: "blocked" as const, orderIndex: 0 },
],
featureFulfillment: {},
},
validationTelemetry: {
validationRounds: [
{
roundId: "VR-BLK",
featureId: "F-BLK",
featureTitle: "API integration",
validatorStatus: "blocked" as const,
implementationAttempt: 1,
validatorAttempt: 1,
failedAssertionIds: ["CA-003"],
generatedFixFeatureIds: [],
blockedReason: "External API unavailable — connection refused after 3 retries",
startedAt: "2026-04-10T10:00:00.000Z",
completedAt: "2026-04-10T10:01:00.000Z",
},
],
lastValidatorStatus: "blocked" as const,
totalRuns: 1,
},
fixFeatures: [],
rollup: {
milestoneId: "MS-001",
totalAssertions: 1,
passedAssertions: 0,
failedAssertions: 0,
blockedAssertions: 1,
pendingAssertions: 0,
unlinkedAssertions: 0,
state: "blocked" as const,
},
};
const mockMilestoneValidationTelemetry = {
validationContract: {
assertions: [],
featureFulfillment: {},
},
validationTelemetry: {
validationRounds: [],
lastValidatorStatus: null,
totalRuns: 0,
},
fixFeatures: [],
rollup: mockMilestoneValidationRollup,
};
const mockMissionEvents = [
{
id: "E-001",
missionId: "M-001",
eventType: "mission_started",
description: "Mission started",
metadata: null,
timestamp: "2026-01-03T10:00:00.000Z",
},
{
id: "E-002",
missionId: "M-001",
eventType: "warning",
description: "Task queue is delayed",
metadata: { queueDepth: 4 },
timestamp: "2026-01-03T10:10:00.000Z",
},
{
id: "E-003",
missionId: "M-001",
eventType: "feature_completed",
description: "Feature F-001 completed",
metadata: { featureId: "F-001" },
timestamp: "2026-01-03T10:20:00.000Z",
},
{
id: "E-004",
missionId: "M-001",
eventType: "autopilot_state_changed",
description: "Autopilot moved to watching",
metadata: { previous: "inactive", next: "watching" },
timestamp: "2026-01-03T10:30:00.000Z",
},
];
const mockMissionEventsPaged = Array.from({ length: 65 }, (_, index) => ({
id: `E-${String(index + 1).padStart(3, "0")}`,
missionId: "M-001",
eventType: index % 2 === 0 ? "feature_completed" : "slice_activated",
description: `Mission event ${index + 1}`,
metadata: { index: index + 1 },
timestamp: new Date(Date.UTC(2026, 0, 3, 10, index)).toISOString(),
}));
/** Create a mock Response that matches the real api() function's expectations (text + content-type headers) */
function mockApiResponse(data: unknown) {
return {
ok: true,
headers: new Headers({ "content-type": "application/json" }),
text: () => Promise.resolve(JSON.stringify(data)),
};
}
const mockMissionHealthById: Record<string, unknown> = {
"M-001": {
missionId: "M-001",
status: "planning",
tasksCompleted: 0,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 0,
currentSliceId: undefined,
currentMilestoneId: undefined,
estimatedCompletionPercent: 0,
lastErrorAt: undefined,
lastErrorDescription: undefined,
autopilotState: "inactive",
autopilotEnabled: false,
lastActivityAt: undefined,
},
"M-002": {
missionId: "M-002",
status: "active",
tasksCompleted: 3,
tasksFailed: 0,
tasksInFlight: 1,
totalTasks: 5,
currentSliceId: "SL-API-1",
currentMilestoneId: "MS-API-1",
estimatedCompletionPercent: 60,
lastErrorAt: undefined,
lastErrorDescription: undefined,
autopilotState: "watching",
autopilotEnabled: true,
lastActivityAt: "2026-01-02T00:00:00.000Z",
},
};
function getMockMissionHealth(missionId: string) {
return (
mockMissionHealthById[missionId] ?? {
missionId,
status: "planning",
tasksCompleted: 0,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 0,
currentSliceId: undefined,
currentMilestoneId: undefined,
estimatedCompletionPercent: 0,
lastErrorAt: undefined,
lastErrorDescription: undefined,
autopilotState: "inactive",
autopilotEnabled: false,
lastActivityAt: undefined,
}
);
}
function extractMissionId(url: string): string | null {
const match = url.match(/\/api\/missions\/([^/?]+)/);
return match ? decodeURIComponent(match[1]) : null;
}
function parseMissionEventsResponse(url: string, events = mockMissionEvents) {
const parsed = new URL(url, "http://localhost");
const offset = Number(parsed.searchParams.get("offset") ?? "0");
const limit = Number(parsed.searchParams.get("limit") ?? "25");
const eventType = parsed.searchParams.get("eventType");
const filtered = eventType
? events.filter((event) => event.eventType === eventType)
: events;
return {
events: filtered.slice(offset, offset + limit),
total: filtered.length,
limit,
offset,
};
}
function getValidationApiMock(url: string, telemetryOverride?: unknown): unknown | null {
const telemetry = telemetryOverride ?? mockMilestoneValidationTelemetry;
if (url.includes("/validation-telemetry")) {
return telemetry;
}
if (url.includes("/validation-runs")) {
return { runs: [], total: 0, limit: 10, offset: 0 };
}
if (url.includes("/validation-loop")) {
return {
featureId: "F-001",
feature: mockMissionDetail.milestones[0].slices[0].features[0],
loopState: "idle",
implementationAttemptCount: 0,
validatorAttemptCount: 0,
retryBudgetRemaining: 3,
};
}
if (url.includes("/validation")) {
return mockMilestoneValidationRollup;
}
if (url.includes("/assertions")) {
return [];
}
return null;
}
class MockEventSource {
static instances: MockEventSource[] = [];
private readonly listeners = new Map<string, Set<(event: MessageEvent<string>) => void>>();
constructor(public readonly url: string) {
MockEventSource.instances.push(this);
}
addEventListener(type: string, callback: (event: MessageEvent<string>) => void) {
const existing = this.listeners.get(type) ?? new Set();
existing.add(callback);
this.listeners.set(type, existing);
}
removeEventListener(type: string, callback: (event: MessageEvent<string>) => void) {
this.listeners.get(type)?.delete(callback);
}
close() {
this.listeners.clear();
}
emit(type: string, payload: unknown) {
const event = { data: JSON.stringify(payload) } as MessageEvent<string>;
for (const callback of this.listeners.get(type) ?? []) {
callback(event);
}
}
static reset() {
MockEventSource.instances = [];
}
}
/** Fetch mock that returns mission list, detail, health, autopilot, and events endpoints. */
function createFetchMock() {
return vi.fn().mockImplementation((url: string) => {
// Handle batched health endpoint before individual health endpoint
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse(mockMissions));
});
}
/** Fetch mock for navigating into a mission detail */
function createDetailFetchMock(events = mockMissionEvents) {
return vi.fn().mockImplementation((url: string) => {
// Handle batched health endpoint before individual health endpoint
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, events)));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
const missionId = extractMissionId(url);
if (missionId === "M-001") {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
}
return Promise.resolve(mockApiResponse(mockMissions));
});
}
function createFetchMockWithTelemetry(telemetryOverride: unknown) {
return vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url, telemetryOverride);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse(mockMissions));
});
}
function createDetailFetchMockWithTelemetry(events: unknown[], telemetryOverride: unknown) {
return vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, events as typeof mockMissionEvents)));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url, telemetryOverride);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
const missionId = extractMissionId(url);
if (missionId === "M-001") {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
}
return Promise.resolve(mockApiResponse(mockMissions));
});
}
function createFetchMockWithHealth(
missions: Array<Record<string, unknown>>,
healthByMissionId: Record<string, unknown>,
) {
return vi.fn().mockImplementation((url: string) => {
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
// Handle batched health endpoint before individual health endpoint
// /api/missions/health returns all mission health data
// /api/missions/:id/health returns individual mission health
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(healthByMissionId));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "";
return Promise.resolve(mockApiResponse(healthByMissionId[missionId] ?? getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(mockMissionDetail));
}
return Promise.resolve(mockApiResponse(missions));
});
}
describe("MissionManager", () => {
let originalFetch: typeof globalThis.fetch;
let originalEventSource: typeof globalThis.EventSource | undefined;
beforeEach(() => {
originalFetch = globalThis.fetch;
originalEventSource = globalThis.EventSource;
mockFetchAiSession.mockReset();
mockFetchAiSessions.mockReset();
mockCancelMissionInterview.mockReset();
mockConnectMissionInterviewStream.mockReset();
mockFetchAiSession.mockResolvedValue(null);
mockFetchAiSessions.mockResolvedValue([]);
mockCancelMissionInterview.mockResolvedValue(undefined);
mockConnectMissionInterviewStream.mockReturnValue({
close: vi.fn(),
isConnected: () => true,
});
MockEventSource.reset();
});
afterEach(() => {
globalThis.fetch = originalFetch;
globalThis.EventSource = originalEventSource as typeof globalThis.EventSource;
vi.restoreAllMocks();
});
it("renders nothing when isOpen is false", () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={false} onClose={vi.fn()} addToast={vi.fn()} />);
expect(screen.queryByTestId("mission-manager-dialog")).toBeNull();
});
it("renders the dialog with accessible attributes when open", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const dialog = screen.getByTestId("mission-manager-dialog");
expect(dialog).toBeDefined();
expect(dialog.getAttribute("role")).toBe("dialog");
expect(dialog.getAttribute("aria-modal")).toBe("true");
expect(dialog.getAttribute("aria-label")).toBe("Mission Manager");
});
});
it("renders the modal overlay with open class", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const overlay = screen.getByTestId("mission-manager-overlay");
expect(overlay).toBeDefined();
expect(overlay.className).toContain("open");
});
});
it("shows the Missions title in list view", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Missions")).toBeDefined();
});
});
it("renders mission items in the list", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
expect(screen.getByText("API Redesign")).toBeDefined();
});
});
it("shows mission status badges", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("planning")).toBeDefined();
expect(screen.getByText("active")).toBeDefined();
});
});
it("shows summary stats when mission has summary data", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
// M-002 has summary: { totalMilestones: 2, completedMilestones: 1, totalFeatures: 5, completedFeatures: 3 }
expect(screen.getByText("1/2 milestones")).toBeDefined();
expect(screen.getByText("3/5 features")).toBeDefined();
});
});
it("hides summary section for missions without summary data", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
// M-001 has no summary — no stats should appear for it
expect(screen.queryByText("0/0 milestones")).toBeNull();
});
// M-002 has summary so these should exist
expect(screen.getByText("1/2 milestones")).toBeDefined();
});
it("renders progress bar for missions with summary", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
// Progress bar element should exist for M-002 (has summary with progressPercent: 60)
const progressBar = document.querySelector(".mission-list__item-progress-bar") as HTMLElement;
expect(progressBar).toBeDefined();
expect(progressBar?.style.width).toBe("60%");
});
});
it("renders healthy, warning, and error health badges based on mission health", async () => {
const missions = [
{ id: "M-H1", title: "Healthy Mission", status: "planning", milestones: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
{ id: "M-H2", title: "Warning Mission", status: "active", milestones: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
{ id: "M-H3", title: "Error Mission", status: "active", milestones: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
];
globalThis.fetch = createFetchMockWithHealth(missions as Array<Record<string, unknown>>, {
"M-H1": {
missionId: "M-H1",
status: "planning",
tasksCompleted: 2,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 2,
estimatedCompletionPercent: 100,
autopilotState: "inactive",
autopilotEnabled: false,
},
"M-H2": {
missionId: "M-H2",
status: "active",
tasksCompleted: 1,
tasksFailed: 1,
tasksInFlight: 1,
totalTasks: 4,
estimatedCompletionPercent: 25,
autopilotState: "watching",
autopilotEnabled: true,
},
"M-H3": {
missionId: "M-H3",
status: "active",
tasksCompleted: 3,
tasksFailed: 4,
tasksInFlight: 0,
totalTasks: 10,
estimatedCompletionPercent: 30,
lastErrorAt: new Date().toISOString(),
autopilotState: "activating",
autopilotEnabled: true,
},
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-health-badge-M-H1").className).toContain("mission-health-badge--healthy");
expect(screen.getByTestId("mission-health-badge-M-H2").className).toContain("mission-health-badge--warning");
expect(screen.getByTestId("mission-health-badge-M-H3").className).toContain("mission-health-badge--error");
});
});
it("shows task progress stats and failed-task indicator", async () => {
const missions = [
{
id: "M-TASKS",
title: "Task Stats Mission",
status: "active",
summary: {
totalMilestones: 2,
completedMilestones: 1,
totalFeatures: 5,
completedFeatures: 2,
progressPercent: 40,
},
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
];
globalThis.fetch = createFetchMockWithHealth(missions as Array<Record<string, unknown>>, {
"M-TASKS": {
missionId: "M-TASKS",
status: "active",
tasksCompleted: 3,
tasksFailed: 1,
tasksInFlight: 1,
totalTasks: 5,
estimatedCompletionPercent: 60,
autopilotState: "watching",
autopilotEnabled: true,
lastActivityAt: new Date().toISOString(),
},
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-task-stats-M-TASKS")).toHaveTextContent("3/5 tasks");
expect(screen.getByTestId("mission-failed-M-TASKS")).toHaveTextContent("1 failed");
});
});
it("formats mission relative activity time", async () => {
const missions = [
{ id: "M-TIME", title: "Relative Time Mission", status: "active", milestones: [], createdAt: "2026-01-01T00:00:00.000Z", updatedAt: "2026-01-01T00:00:00.000Z" },
];
globalThis.fetch = createFetchMockWithHealth(missions as Array<Record<string, unknown>>, {
"M-TIME": {
missionId: "M-TIME",
status: "active",
tasksCompleted: 0,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 1,
estimatedCompletionPercent: 0,
autopilotState: "inactive",
autopilotEnabled: false,
lastActivityAt: new Date(Date.now() - 2 * 60 * 1000).toISOString(),
},
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-last-activity-M-TIME").textContent).toMatch(/Activity\s+\d+m ago|Activity just now/);
});
});
it("renders mission activity tab with filter and metadata toggle", async () => {
globalThis.fetch = createDetailFetchMock(mockMissionEvents);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
await waitFor(() => {
expect(screen.getByTestId("mission-activity-events")).toBeDefined();
expect(screen.getByText("Mission started")).toBeDefined();
expect(screen.getByText("Task queue is delayed")).toBeDefined();
});
fireEvent.change(screen.getByTestId("mission-activity-filter"), {
target: { value: "tasks" },
});
await waitFor(() => {
expect(screen.getByText("Feature F-001 completed")).toBeDefined();
expect(screen.queryByText("Mission started")).toBeNull();
});
fireEvent.change(screen.getByTestId("mission-activity-filter"), {
target: { value: "errors" },
});
await waitFor(() => {
expect(screen.getByText("Task queue is delayed")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-event-metadata-E-002"));
expect(screen.getByText(/"queueDepth": 4/)).toBeDefined();
fireEvent.click(screen.getByTestId("mission-event-metadata-E-002"));
expect(screen.queryByText(/"queueDepth": 4/)).toBeNull();
});
it("loads more mission activity events", async () => {
globalThis.fetch = createDetailFetchMock(mockMissionEventsPaged as unknown as typeof mockMissionEvents);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
await waitFor(() => {
expect(screen.getByText("Mission event 50")).toBeDefined();
expect(screen.queryByText("Mission event 51")).toBeNull();
expect(screen.getByTestId("mission-activity-load-more")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-activity-load-more"));
await screen.findByText("Mission event 65", undefined, { timeout: 10_000 });
await waitFor(() => {
expect(screen.queryByTestId("mission-activity-load-more")).toBeNull();
});
}, 15000);
it("auto-scrolls to latest mission activity on initial load", async () => {
globalThis.fetch = createDetailFetchMock(mockMissionEvents);
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
const scrollIntoViewSpy = vi.fn();
Object.defineProperty(HTMLElement.prototype, "scrollIntoView", {
configurable: true,
value: scrollIntoViewSpy,
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
await waitFor(() => {
expect(screen.getByText("Mission started")).toBeDefined();
expect(scrollIntoViewSpy).toHaveBeenCalled();
});
});
it("prepends real-time mission events and scrolls to top when near bottom", async () => {
globalThis.fetch = createDetailFetchMock(mockMissionEvents);
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
const eventsContainer = await screen.findByTestId("mission-activity-events");
Object.defineProperty(eventsContainer, "scrollHeight", { configurable: true, value: 1000 });
Object.defineProperty(eventsContainer, "clientHeight", { configurable: true, value: 300 });
Object.defineProperty(eventsContainer, "scrollTop", { configurable: true, value: 650, writable: true });
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("mission:event", {
id: "E-REALTIME",
missionId: "M-001",
eventType: "warning",
description: "Real-time warning event",
metadata: { source: "sse" },
timestamp: "2026-01-03T11:00:00.000Z",
});
}
});
await waitFor(() => {
expect(screen.getByText("Real-time warning event")).toBeDefined();
expect(eventsContainer.scrollTop).toBe(0);
});
const eventDescriptions = Array.from(eventsContainer.querySelectorAll(".mission-event__description"));
expect(eventDescriptions[0]?.textContent).toBe("Real-time warning event");
});
it("ignores real-time mission events for non-selected missions", async () => {
globalThis.fetch = createDetailFetchMock(mockMissionEvents);
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
await screen.findByTestId("mission-activity-events");
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("mission:event", {
id: "E-OTHER",
missionId: "M-999",
eventType: "warning",
description: "Other mission warning",
metadata: null,
timestamp: "2026-01-03T11:00:00.000Z",
});
}
});
await waitFor(() => {
expect(screen.queryByText("Other mission warning")).toBeNull();
});
});
it("reloads selected mission detail when feature:updated SSE event arrives", async () => {
const fetchMock = createDetailFetchMock(mockMissionEvents);
globalThis.fetch = fetchMock;
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Click on the mission to open detail view
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Back button should appear in detail view
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Record initial fetch calls for mission detail
const initialFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(initialFetchCount).toBeGreaterThan(0);
// Emit a feature:updated SSE event
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("feature:updated", {
featureId: "F-001",
missionId: "M-001",
sliceId: "SL-001",
previousStatus: "triaged",
newStatus: "in-progress",
});
}
});
// Verify mission detail was reloaded (fetch was called again for the mission)
await waitFor(() => {
const updatedFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(updatedFetchCount).toBeGreaterThan(initialFetchCount);
});
});
it("fetches milestone validation telemetry when mission detail opens", async () => {
const fetchMock = createDetailFetchMock(mockMissionEvents);
globalThis.fetch = fetchMock;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
const telemetryCalls = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/milestones/MS-001/validation-telemetry")
).length;
expect(telemetryCalls).toBeGreaterThan(0);
});
});
it("refreshes validation telemetry when validator-run SSE event targets selected milestone", async () => {
const fetchMock = createDetailFetchMock(mockMissionEvents);
globalThis.fetch = fetchMock;
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
const initialTelemetryCalls = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/milestones/MS-001/validation-telemetry")
).length;
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("validator-run:started", {
id: "VR-001",
featureId: "F-001",
milestoneId: "MS-001",
status: "running",
});
}
});
await waitFor(() => {
const updatedTelemetryCalls = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/milestones/MS-001/validation-telemetry")
).length;
expect(updatedTelemetryCalls).toBeGreaterThan(initialTelemetryCalls);
});
});
it("reloads selected mission detail when mission:updated SSE event arrives", async () => {
const fetchMock = createDetailFetchMock(mockMissionEvents);
globalThis.fetch = fetchMock;
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Click on the mission to open detail view
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Record initial fetch calls for mission detail
const initialFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(initialFetchCount).toBeGreaterThan(0);
// Emit a mission:updated SSE event for the selected mission
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("mission:updated", {
id: "M-001",
title: "Build Auth System",
status: "active",
autopilotEnabled: true,
autopilotState: "watching",
lastAutopilotActivityAt: new Date().toISOString(),
});
}
});
// Verify mission detail was reloaded (fetch was called again for the mission)
await waitFor(() => {
const updatedFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(updatedFetchCount).toBeGreaterThan(initialFetchCount);
});
});
it("updates mission status badge when mission:updated SSE event arrives", async () => {
globalThis.fetch = createFetchMock();
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Wait for initial render — M-001 has status "planning"
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Verify initial status badge shows "planning"
const missionItem = screen.getByText("Build Auth System").closest(".mission-list__item");
expect(missionItem).toBeDefined();
const planningBadges = missionItem!.querySelectorAll(".mission-status-badge");
expect([...planningBadges].some((b) => b.textContent === "planning")).toBe(true);
// Emit mission:updated SSE event changing M-001 to "active"
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("mission:updated", {
id: "M-001",
title: "Build Auth System",
status: "active",
});
}
});
// Verify the badge now shows "active" instead of "planning"
await waitFor(() => {
const updatedBadges = missionItem!.querySelectorAll(".mission-status-badge");
expect([...updatedBadges].some((b) => b.textContent === "active")).toBe(true);
expect([...updatedBadges].some((b) => b.textContent === "planning")).toBe(false);
});
});
it("reloads selected mission detail when slice:updated SSE event arrives", async () => {
const fetchMock = createDetailFetchMock(mockMissionEvents);
globalThis.fetch = fetchMock;
globalThis.EventSource = MockEventSource as unknown as typeof globalThis.EventSource;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Click on the mission to open detail view
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Record initial fetch calls for mission detail
const initialFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(initialFetchCount).toBeGreaterThan(0);
// Emit a slice:updated SSE event for a slice in the selected mission
await act(async () => {
for (const source of MockEventSource.instances) {
source.emit("slice:updated", {
id: "SL-001",
milestoneId: "MS-001",
status: "active",
});
}
});
// Verify mission detail was reloaded (fetch was called again for the mission)
await waitFor(() => {
const updatedFetchCount = fetchMock.mock.calls.filter(
(call) => typeof call[0] === "string" && call[0].includes("/api/missions/M-001")
).length;
expect(updatedFetchCount).toBeGreaterThan(initialFetchCount);
});
});
it("shows empty state when no missions exist", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("No missions yet. Create one to start planning.")).toBeDefined();
});
});
it("calls onClose when close button is clicked", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-close-btn")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-close-btn"));
expect(onClose).toHaveBeenCalledOnce();
});
it("calls onClose when overlay background is clicked", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-manager-overlay")).toBeDefined();
});
const overlay = screen.getByTestId("mission-manager-overlay");
fireEvent.click(overlay);
expect(onClose).toHaveBeenCalledOnce();
});
it("navigates to detail view when a mission is clicked", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Wait for list to load
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
// Click on a mission to open detail
fireEvent.click(screen.getByText("Build Auth System"));
// Wait for detail view to render
await waitFor(() => {
// Back button should appear in detail view
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
// Milestone should be visible (auto-expanded)
expect(screen.getByText("Database Schema")).toBeDefined();
});
});
it("navigates back to list view when back button is clicked", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click back
fireEvent.click(screen.getByTestId("mission-back-btn"));
// Should return to list view
await waitFor(() => {
expect(screen.getByText("Missions")).toBeDefined();
expect(screen.getByText("API Redesign")).toBeDefined();
});
});
it("calls onClose on Escape key press", async () => {
globalThis.fetch = createFetchMock();
const onClose = vi.fn();
render(<MissionManager isOpen={true} onClose={onClose} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-manager-dialog")).toBeDefined();
});
fireEvent.keyDown(document, { key: "Escape" });
expect(onClose).toHaveBeenCalled();
});
it("shows close button with accessible label", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByLabelText("Close Mission Manager")).toBeDefined();
});
});
it("shows back button with accessible label in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByLabelText("Back to missions list")).toBeDefined();
});
});
it("shows New Mission button in list view", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("New Mission")).toBeDefined();
});
});
// ── Inline vs Modal Header Behavior ──────────────────────────────
describe("inline vs modal header behavior", () => {
it("renders with page-style header class when isInline is true", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const header = document.querySelector(".mission-manager__header--inline");
expect(header).toBeDefined();
});
});
it("does not show modal close button in inline mode", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
// The modal close button should not be present in inline mode
expect(screen.queryByTestId("mission-close-btn")).toBeNull();
});
});
it("shows modal close button in modal mode (isInline=false)", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} isInline={false} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByTestId("mission-close-btn")).toBeDefined();
});
});
it("does not show refresh button in inline mode", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.queryByTestId("mission-refresh-btn")).toBeNull();
});
});
it("does not show refresh button in modal mode", async () => {
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} isInline={false} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.queryByTestId("mission-refresh-btn")).toBeNull();
});
});
it("inline mode header has inline class modifier for styling parity with agents view", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
const dialog = screen.getByTestId("mission-manager-dialog");
expect(dialog.className).toContain("mission-manager--inline");
});
});
it("detail view in inline mode still shows back button", async () => {
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/health")) {
return Promise.resolve(mockApiResponse(getMockMissionHealth("M-001")));
}
callCount++;
if (callCount <= 1) {
return Promise.resolve(mockApiResponse(mockMissions));
}
return Promise.resolve(mockApiResponse(mockMissionDetail));
});
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
// Close button should still be absent in inline mode even in detail view
expect(screen.queryByTestId("mission-close-btn")).toBeNull();
});
});
});
it("hides send to background button when mission interview is in initial state", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse([]));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Plan with AI")).toBeInTheDocument();
});
fireEvent.click(screen.getByText("Plan with AI"));
await waitFor(() => {
expect(screen.getByText("Plan Mission with AI")).toBeInTheDocument();
});
expect(screen.queryByLabelText("Send to background")).not.toBeInTheDocument();
});
it("sends mission interview to background without canceling the session", async () => {
const closeSpy = vi.fn();
mockConnectMissionInterviewStream.mockReturnValueOnce({
close: closeSpy,
isConnected: () => true,
});
mockFetchAiSession.mockResolvedValueOnce({
id: "session-bg-1",
type: "mission_interview",
status: "generating",
title: "Background mission",
inputPayload: JSON.stringify({ missionTitle: "Background mission" }),
conversationHistory: "[]",
currentQuestion: null,
result: null,
thinkingOutput: "",
error: null,
projectId: null,
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
globalThis.fetch = createFetchMock();
render(
<MissionManager
isOpen={true}
onClose={vi.fn()}
addToast={vi.fn()}
resumeSessionId="session-bg-1"
/>,
);
await waitFor(() => {
expect(screen.getByText("Plan Mission with AI")).toBeInTheDocument();
expect(screen.getByText("Preparing next question...")).toBeInTheDocument();
});
fireEvent.click(screen.getByLabelText("Send to background"));
expect(closeSpy).toHaveBeenCalledTimes(1);
expect(mockCancelMissionInterview).not.toHaveBeenCalled();
await waitFor(() => {
expect(screen.queryByText("Plan Mission with AI")).not.toBeInTheDocument();
});
});
it("logs a warning when pending interview session fetch fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const pendingFetchError = new Error("Pending sessions failed");
mockFetchAiSessions.mockRejectedValueOnce(pendingFetchError);
globalThis.fetch = createFetchMock();
render(<MissionManager isOpen={true} isInline={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(warnSpy).toHaveBeenCalledWith(
"[MissionManager] Failed to fetch pending interview sessions:",
pendingFetchError,
);
});
expect(screen.getByText("Missions")).toBeInTheDocument();
warnSpy.mockRestore();
});
it("logs a warning when milestone/slice resume session fetch fails", async () => {
const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => {});
const resumeFetchError = new Error("Resume session failed");
const onResumeFetchError = vi.fn();
mockFetchAiSession.mockRejectedValueOnce(resumeFetchError);
globalThis.fetch = createFetchMock();
render(
<MissionManager
isOpen={true}
isInline={true}
onClose={vi.fn()}
addToast={vi.fn()}
milestoneSliceResumeSessionId="sess-resume-1"
onMilestoneSliceResumeFetchError={onResumeFetchError}
/>,
);
await waitFor(() => {
expect(warnSpy).toHaveBeenCalledWith(
"[MissionManager] Failed to fetch session for milestone/slice resume:",
resumeFetchError,
);
});
expect(onResumeFetchError).toHaveBeenCalledTimes(1);
warnSpy.mockRestore();
});
it("shows milestone hierarchy in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone is auto-expanded, slice and feature visible
expect(screen.getByText("Database Schema")).toBeDefined();
expect(screen.getByText("User Tables")).toBeDefined();
expect(screen.getByText("User model")).toBeDefined();
});
});
// ── Regression: Generated mission ID format in edit/delete flows ──────────
//
// MissionStore generates IDs like M-LZ7DN0-A2B5 (base36 timestamp + random).
// The MissionManager must successfully edit and delete missions with these IDs
// without surfacing "invalid ID format" errors.
describe("generated mission ID format regression", () => {
// Use realistic generated-style IDs matching what MissionStore produces
const generatedMissionId = "M-LZ7DN0-A2B5";
const generatedMilestoneId = "MS-M3N8QR-C9F1";
const generatedSliceId = "SL-P4T2WX-D5E8";
const generatedFeatureId = "F-J6K9AB-G7H3";
const generatedMockMissions = [
{
id: generatedMissionId,
title: "Generated Mission",
description: "Mission with realistic generated ID",
status: "planning",
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
];
const generatedMockDetail = {
id: generatedMissionId,
title: "Generated Mission",
description: "Mission with realistic generated ID",
status: "planning",
milestones: [
{
id: generatedMilestoneId,
title: "Generated Milestone",
description: "Milestone with generated ID",
status: "planning",
dependencies: [] as string[],
slices: [
{
id: generatedSliceId,
title: "Generated Slice",
description: "Slice with generated ID",
status: "pending",
features: [
{
id: generatedFeatureId,
title: "Generated Feature",
description: "Feature with generated ID",
acceptanceCriteria: "Works correctly",
status: "defined",
taskId: null,
sliceId: generatedSliceId,
missionId: generatedMissionId,
},
],
milestoneId: generatedMilestoneId,
missionId: generatedMissionId,
},
],
missionId: generatedMissionId,
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
it("renders missions with generated IDs in the list", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse(generatedMockMissions));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Generated Mission")).toBeDefined();
});
});
it("navigates to detail view for a mission with generated ID", async () => {
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/health")) {
return Promise.resolve(mockApiResponse(getMockMissionHealth(generatedMissionId)));
}
callCount++;
if (callCount === 1) {
return Promise.resolve(mockApiResponse(generatedMockMissions));
}
return Promise.resolve(mockApiResponse(generatedMockDetail));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Generated Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Generated Mission"));
await waitFor(() => {
expect(screen.getByText("Generated Milestone")).toBeDefined();
expect(screen.getByText("Generated Slice")).toBeDefined();
expect(screen.getByText("Generated Feature")).toBeDefined();
});
});
it("edits a mission with generated ID without error", async () => {
const addToast = vi.fn();
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation((_url: string) => {
if (_url.includes("/health")) {
return Promise.resolve(mockApiResponse(getMockMissionHealth(generatedMissionId)));
}
callCount++;
if (callCount <= 1) {
// Initial list load
return Promise.resolve(mockApiResponse(generatedMockMissions));
}
if (_url && _url.includes("/api/missions/" + generatedMissionId) && !_url.includes("milestones")) {
// Detail or PATCH for the generated ID mission
if (_url.includes("/api/missions/" + generatedMissionId) && callCount > 2) {
// PATCH response — return updated mission
return Promise.resolve(mockApiResponse({
...generatedMockDetail,
title: "Updated Generated Mission",
status: "active",
}));
}
return Promise.resolve(mockApiResponse(generatedMockDetail));
}
return Promise.resolve(mockApiResponse(generatedMockMissions));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={addToast} />);
// Wait for list, click to enter detail
await waitFor(() => {
expect(screen.getByText("Generated Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Generated Mission"));
await waitFor(() => {
expect(screen.getByText("Generated Milestone")).toBeDefined();
});
});
it("deletes a mission with generated ID without surfacing invalid-ID error", async () => {
const addToast = vi.fn();
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation((_url: string, options?: RequestInit) => {
if (_url.includes("/health")) {
return Promise.resolve(mockApiResponse(getMockMissionHealth(generatedMissionId)));
}
callCount++;
// DELETE request — return 204 empty
if (options?.method === "DELETE") {
return Promise.resolve({
ok: true,
headers: new Headers(),
text: () => Promise.resolve(""),
});
}
// Initial list load and subsequent reloads
return Promise.resolve(mockApiResponse(generatedMockMissions));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={addToast} />);
await waitFor(() => {
expect(screen.getByText("Generated Mission")).toBeDefined();
});
// Click the delete button for the mission (uses title attribute)
const deleteButton = screen.getByTitle("Delete mission");
fireEvent.click(deleteButton);
// After clicking delete, a confirmation dialog should appear
await waitFor(() => {
// Find and click the confirm delete button
const confirmBtn = screen.getByText("Delete");
fireEvent.click(confirmBtn);
});
// Verify no "invalid ID format" toast was shown
await waitFor(() => {
const errorToasts = addToast.mock.calls.filter(
(call: any[]) => call[1] === "error" && typeof call[0] === "string" && call[0].toLowerCase().includes("invalid")
);
expect(errorToasts).toHaveLength(0);
});
});
});
// ── Step 2: Detail hierarchy, action layout, confirm panels ──────────
describe("detail view hierarchy and action layout", () => {
it("renders full milestone → slice → feature hierarchy in detail", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone auto-expanded
expect(screen.getByText("Database Schema")).toBeDefined();
// Slice auto-expanded
expect(screen.getByText("User Tables")).toBeDefined();
// Feature visible
expect(screen.getByText("User model")).toBeDefined();
// Feature status badge
expect(screen.getByText("defined")).toBeDefined();
// Acceptance criteria
expect(screen.getByText(/Model exists with required fields/)).toBeDefined();
});
});
it("shows edit and delete mission buttons in detail header", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Detail header should have edit/delete buttons
const editBtns = screen.getAllByLabelText("Edit mission");
const deleteBtns = screen.getAllByLabelText("Delete mission");
// At least one of each in the detail header area
expect(editBtns.length).toBeGreaterThanOrEqual(1);
expect(deleteBtns.length).toBeGreaterThanOrEqual(1);
});
it("opens inline edit form when edit mission is clicked in detail view", async () => {
let callCount = 0;
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
callCount++;
if (callCount === 1) return Promise.resolve(mockApiResponse(mockMissions));
return Promise.resolve(mockApiResponse(mockMissionDetail));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click edit mission in detail header
const editBtns = screen.getAllByLabelText("Edit mission");
fireEvent.click(editBtns[0]);
// Should show inline form with pre-filled title
await waitFor(() => {
const input = screen.getByDisplayValue("Build Auth System");
expect(input).toBeDefined();
expect(screen.getByText("Update")).toBeDefined();
expect(screen.getByText("Cancel")).toBeDefined();
});
});
it("shows delete confirmation with danger variant class", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Click delete mission in detail header
const deleteBtns = screen.getAllByLabelText("Delete mission");
fireEvent.click(deleteBtns[0]);
// Confirmation panel should show
await waitFor(() => {
const confirmPanel = screen.getByText(/Delete this mission/).closest(".mission-confirm-panel");
expect(confirmPanel).toBeDefined();
expect(confirmPanel!.className).toContain("mission-confirm-panel--danger");
});
});
it("shows milestone count in detail header meta", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("1 milestones")).toBeDefined();
});
});
it("shows slice and feature counts in hierarchy headers", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("1 slices")).toBeDefined();
expect(screen.getByText("1 features")).toBeDefined();
});
});
it("renders milestone expand/collapse chevrons", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
// Milestone is auto-expanded — should see the title visible
expect(screen.getByText("Database Schema")).toBeDefined();
// Slice visible (auto-expanded)
expect(screen.getByText("User Tables")).toBeDefined();
});
});
it("shows add milestone button in detail view", async () => {
globalThis.fetch = createDetailFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByText("Add Milestone")).toBeDefined();
});
});
});
// ── 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 = [
{
id: "M-AUTO1",
title: "Autopilot Mission",
description: "Mission with autopilot enabled",
status: "active",
autopilotEnabled: true,
autopilotState: "watching",
lastAutopilotActivityAt: "2026-01-01T00:00:00.000Z",
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
{
id: "M-AUTO2",
title: "Normal Mission",
description: "Mission without autopilot",
status: "planning",
autopilotEnabled: false,
milestones: [],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
},
];
const autopilotMockDetail = {
id: "M-AUTO1",
title: "Autopilot Mission",
description: "Mission with autopilot enabled",
status: "active",
autopilotEnabled: true,
autopilotState: "watching",
lastAutopilotActivityAt: "2026-01-01T00:00:00.000Z",
milestones: [
{
id: "MS-001",
title: "Phase 1",
description: "First phase",
status: "active",
dependencies: [] as string[],
slices: [],
missionId: "M-AUTO1",
},
],
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
};
function createAutopilotFetchMock() {
return vi.fn().mockImplementation((url: string, options?: RequestInit) => {
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-AUTO1";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
if (options?.method === "PATCH") {
return Promise.resolve(mockApiResponse({
enabled: true,
state: "watching",
watched: true,
lastActivityAt: "2026-01-01T12:00:00.000Z",
nextScheduledCheck: "2026-01-01T12:05:00.000Z",
}));
}
return Promise.resolve(mockApiResponse({
enabled: true,
state: "watching",
watched: true,
lastActivityAt: "2026-01-01T12:00:00.000Z",
nextScheduledCheck: "2026-01-01T12:05:00.000Z",
}));
}
if (url.includes("/api/missions/M-AUTO1") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(autopilotMockDetail));
}
return Promise.resolve(mockApiResponse(autopilotMockMissions));
});
}
it("shows autopilot icon for missions with autopilotEnabled in list view", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse(autopilotMockMissions));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
// Autopilot icon should have title attribute
expect(screen.getByTitle("Autopilot enabled")).toBeDefined();
});
});
it("does not show autopilot icon for missions without autopilot", async () => {
globalThis.fetch = vi.fn().mockResolvedValue(mockApiResponse(autopilotMockMissions));
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Normal Mission")).toBeDefined();
});
// There should be only one autopilot icon (for Autopilot Mission)
const autopilotIcons = screen.queryAllByTitle("Autopilot enabled");
expect(autopilotIcons).toHaveLength(1);
});
it("shows autopilot toggle and status badge in detail view", async () => {
globalThis.fetch = createAutopilotFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Autopilot Mission"));
await waitFor(() => {
// Should show Autopilot label
expect(screen.getByText("Autopilot")).toBeDefined();
// Should show status badge with "watching" state
expect(screen.getByTestId("autopilot-state-badge")).toBeDefined();
});
});
it("shows autopilot toggle and status badge", async () => {
globalThis.fetch = createAutopilotFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Autopilot Mission"));
await waitFor(() => {
// Should show autopilot toggle and state badge
expect(screen.getByLabelText("Autopilot")).toBeDefined();
expect(screen.getByTestId("autopilot-state-badge")).toBeDefined();
expect(screen.getByText(/Watching since/)).toBeDefined();
});
// Verify no action buttons exist (they were removed)
expect(screen.queryByTestId("mission-autopilot-start")).toBeNull();
expect(screen.queryByTestId("mission-autopilot-stop")).toBeNull();
expect(screen.queryByTestId("mission-autopilot-refresh")).toBeNull();
});
it("toggles autopilot with a PATCH request", async () => {
const fetchMock = createAutopilotFetchMock();
globalThis.fetch = fetchMock;
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Autopilot Mission"));
const toggle = await screen.findByLabelText("Autopilot");
fireEvent.click(toggle);
await waitFor(() => {
const patchCall = fetchMock.mock.calls.find((call) => {
const [url, options] = call as [string, RequestInit | undefined];
return url.includes("/api/missions/M-AUTO1/autopilot") && options?.method === "PATCH";
});
expect(patchCall).toBeDefined();
expect((patchCall?.[1] as RequestInit | undefined)?.body).toContain('"enabled":false');
});
});
it("shows pulse indicator in the autopilot state badge for active states", async () => {
globalThis.fetch = createAutopilotFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Autopilot Mission"));
await waitFor(() => {
const badge = screen.getByTestId("autopilot-state-badge");
expect(badge.querySelector(".mission-detail__autopilot-pulse")).not.toBeNull();
});
});
it("shows pulsing dot when autopilot is watching in detail view", async () => {
globalThis.fetch = createAutopilotFetchMock();
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
// Navigate to detail
await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined();
});
fireEvent.click(screen.getByText("Autopilot Mission"));
await waitFor(() => {
const dot = document.querySelector(".mission-detail__autopilot-dot");
expect(dot).toBeDefined();
});
});
});
// ── Step 2: Factory parity — contract/telemetry/fix-feature coverage ────────
//
// Validates FN-1569 schema parity from API telemetry payloads through UI rendering.
// Extends test fixtures with validationContract, validationTelemetry, and fixFeatures
// mirroring the exact schema fields used by MissionManager.tsx telemetry section.
describe("Factory parity — contract/telemetry/fix-feature coverage", () => {
it("renders validation telemetry section in detail view after API response", async () => {
globalThis.fetch = createDetailFetchMockWithTelemetry(mockMissionEvents, mockMilestoneValidationTelemetryWithRounds);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// After async telemetry loads, validation telemetry section should appear
await waitFor(() => {
expect(screen.getByText("Validation Telemetry")).toBeDefined();
}, { timeout: 3000 });
// Total runs shown in header meta
await waitFor(() => {
expect(screen.getByText(/2 rounds/)).toBeDefined();
}, { timeout: 3000 });
// Last validator status shown in header meta
await waitFor(() => {
expect(screen.getByText(/Last failed/)).toBeDefined();
}, { timeout: 3000 });
});
it("shows blocked reason surface when validation round is blocked", async () => {
globalThis.fetch = createDetailFetchMockWithTelemetry(mockMissionEvents, mockBlockedMilestoneTelemetry);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
// Wait for telemetry to load
await waitFor(() => {
expect(screen.getByText("Validation Telemetry")).toBeDefined();
}, { timeout: 3000 });
// Last validator status shows blocked
await waitFor(() => {
expect(screen.getByText(/Last blocked/)).toBeDefined();
}, { timeout: 3000 });
// Blocked reason surface should appear (.mission-blocked-reason class)
await waitFor(() => {
expect(document.querySelector(".mission-blocked-reason")).not.toBeNull();
}, { timeout: 3000 });
// Blocked reason text should be visible (use getAllByText since it may appear in both milestone-blocked-reason and round-blocked-reason)
await waitFor(() => {
const matches = screen.getAllByText(/External API unavailable/);
expect(matches.length).toBeGreaterThan(0);
}, { timeout: 3000 });
});
it("does not show blocked-reason surface for failed (non-blocked) rounds", async () => {
// Regression: failed rounds should NOT show blocked-reason surface
globalThis.fetch = createDetailFetchMockWithTelemetry(mockMissionEvents, mockMilestoneValidationTelemetryWithRounds);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
// Wait for telemetry to load
await waitFor(() => {
expect(screen.getByText("Validation Telemetry")).toBeDefined();
}, { timeout: 3000 });
// Blocked reason text from the blocked telemetry should NOT appear
// (the mockMissionDetail has a milestone without blocked telemetry)
expect(screen.queryByText(/External API unavailable/)).toBeNull();
});
it("displays fix-features with source linkage in telemetry section", async () => {
globalThis.fetch = createDetailFetchMockWithTelemetry(mockMissionEvents, mockMilestoneValidationTelemetryWithRounds);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
// Wait for telemetry to load
await waitFor(() => {
expect(screen.getByText(/Validation Telemetry/)).toBeDefined();
}, { timeout: 3000 });
// Fix features should appear with their source linkage
await waitFor(() => {
expect(screen.getByText("Fix: token refresh")).toBeDefined();
}, { timeout: 3000 });
// Source feature ID should be visible (clickable link to source feature)
await waitFor(() => {
expect(screen.getByText("F-001")).toBeDefined();
}, { timeout: 3000 });
});
it("blocked mission exposes resume affordance with aria-label", async () => {
// Test that a mission with blocked status shows the Resume button
const blockedMission = {
...mockMissionDetail,
status: "blocked" as const,
};
globalThis.fetch = vi.fn().mockImplementation((url: string) => {
if (url.includes("/missions/health")) {
return Promise.resolve(mockApiResponse(mockMissionHealthById));
}
if (url.includes("/events")) {
return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url)));
}
if (url.includes("/health")) {
const missionId = extractMissionId(url) ?? "M-001";
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
}
if (url.includes("/autopilot")) {
return Promise.resolve(mockApiResponse(mockAutopilotStatus));
}
const validationResponse = getValidationApiMock(url);
if (validationResponse !== null) {
return Promise.resolve(mockApiResponse(validationResponse));
}
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
return Promise.resolve(mockApiResponse(blockedMission));
}
return Promise.resolve(mockApiResponse(mockMissions));
});
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-back-btn")).toBeDefined();
});
// Resume button with aria-label="Resume mission" should appear for blocked mission
await waitFor(() => {
const resumeButton = screen.getByLabelText("Resume mission");
expect(resumeButton).toBeDefined();
}, { timeout: 3000 });
});
it("activity tab metadata toggle still works after telemetry changes", async () => {
// Regression: mission events metadata toggle (mission-event-metadata-*) must remain functional
// Uses same pattern as existing passing test (lines ~912-923)
globalThis.fetch = createDetailFetchMockWithTelemetry(mockMissionEvents, mockMilestoneValidationTelemetryWithRounds);
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
await waitFor(() => {
expect(screen.getByText("Build Auth System")).toBeDefined();
});
fireEvent.click(screen.getByText("Build Auth System"));
await waitFor(() => {
expect(screen.getByTestId("mission-tab-activity")).toBeDefined();
});
fireEvent.click(screen.getByTestId("mission-tab-activity"));
await waitFor(() => {
expect(screen.getByTestId("mission-activity-events")).toBeDefined();
expect(screen.getByText("Mission started")).toBeDefined();
});
// Toggle metadata for event E-002 which has metadata { queueDepth: 4 }
fireEvent.click(screen.getByTestId("mission-event-metadata-E-002"));
expect(screen.getByText(/"queueDepth": 4/)).toBeDefined();
});
});
});