FN-5681: clarify mission run controls and autopilot states
Improve Mission Manager run-state UX by separating mission run controls from autopilot behavior. - Group mission run settings in the detail view and add explicit Start mission / Stop mission / Resume mission buttons with contextual helper text. - Humanize autopilot state labels (Off, Watching, Activating slice, Completing) and add explanatory autopilot description copy. - Update Mission Manager styling to support the new run-settings layout and helper text blocks in list/detail views. - Extend MissionManager tests to verify labeled run controls, endpoint wiring for start/stop/resume, and humanized autopilot badge labels. - Update mission docs to clarify the distinction between mission run-state controls and the autopilot toggle. Files changed: docs/missions.md | 2 +- packages/dashboard/app/components/MissionManager.css | 62 ++++- packages/dashboard/app/components/MissionManager.tsx | 272 +++++++++++++-------- packages/dashboard/app/components/__tests__/MissionManager.test.tsx | 107 +++++++- 4 files changed, 330 insertions(+), 113 deletions(-) Fusion-Task-Id: FN-5681 Fusion-Task-Lineage: 0dcac659-2eff-435b-a176-b66e7646b054
This commit is contained in:
@@ -213,7 +213,7 @@ If validation cannot run (unexpected loop state, duplicate trigger, blocked vali
|
||||
- `autopilotEnabled=false`, `autoAdvance=true` → next pending slice is activated (legacy compat)
|
||||
- `autopilotEnabled=false`, `autoAdvance=false` → manual activation required
|
||||
|
||||
**Dashboard UI:** The Mission Manager shows `autopilotEnabled` as the primary control. When enabling autopilot on an already-active mission, the system automatically checks whether recovery is needed (no active slice or completed active slice) and progresses accordingly.
|
||||
**Dashboard UI:** The Mission Manager groups mission run settings together: explicit **Start mission / Stop mission / Resume mission** actions control mission run-state, while the **Autopilot** toggle controls automatic slice advancement and feature planning. The autopilot badge uses human-readable states (`Off`, `Watching`, `Activating slice`, `Completing`). When enabling autopilot on an already-active mission, the system automatically checks whether recovery is needed (no active slice or completed active slice) and progresses accordingly.
|
||||
|
||||
## Autopilot API Endpoints
|
||||
|
||||
|
||||
@@ -866,6 +866,19 @@
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.mission-list__item-run-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.mission-list__item-run-help {
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||
color: var(--text-muted);
|
||||
max-width: calc(var(--space-3xl) * 5);
|
||||
}
|
||||
|
||||
.mission-list__item-summary {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
@@ -1031,17 +1044,50 @@
|
||||
/* ── Detail-level action bar (edit / delete the mission itself) ── */
|
||||
.mission-detail__actions {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
gap: var(--space-md);
|
||||
padding-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.mission-detail__run-controls {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: calc(var(--space-xs) / 2);
|
||||
}
|
||||
|
||||
.mission-detail__run-help {
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mission-detail__management-actions {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.mission-detail__run-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--space-xs);
|
||||
padding-top: var(--space-xs);
|
||||
}
|
||||
|
||||
.mission-detail__run-settings-title {
|
||||
margin: 0;
|
||||
font-size: calc(var(--space-sm) + var(--space-xs));
|
||||
color: var(--text-muted);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
/* ── Autopilot section ── */
|
||||
.mission-detail__autopilot {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--space-md);
|
||||
flex-wrap: wrap;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: var(--space-xs);
|
||||
padding: var(--space-xs) 0;
|
||||
}
|
||||
|
||||
@@ -1055,6 +1101,11 @@
|
||||
color: var(--color-warning);
|
||||
}
|
||||
|
||||
.mission-detail__autopilot-description {
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.mission-detail__autopilot-activity {
|
||||
font-size: calc(var(--space-sm) + var(--space-xs) * 0.75);
|
||||
color: var(--text-muted);
|
||||
@@ -2442,6 +2493,11 @@
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.mission-detail__run-help,
|
||||
.mission-list__item-run-help {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
/* Milestone headers wrap actions below title row */
|
||||
.mission-milestone__header {
|
||||
padding: var(--space-sm) var(--space-md);
|
||||
|
||||
@@ -162,6 +162,13 @@ const autopilotStateColors: Record<AutopilotState, { bg: string; text: string }>
|
||||
completing: { bg: "var(--autopilot-completing-bg)", text: "var(--autopilot-completing-text)" },
|
||||
};
|
||||
|
||||
const autopilotStateLabels: Record<AutopilotState, string> = {
|
||||
inactive: "Off",
|
||||
watching: "Watching",
|
||||
activating: "Activating slice",
|
||||
completing: "Completing",
|
||||
};
|
||||
|
||||
/** Assertion status colors */
|
||||
const assertionStatusColors: Record<MissionAssertionStatus, { bg: string; text: string }> = {
|
||||
pending: { bg: "var(--assertion-pending-bg)", text: "var(--assertion-pending-text)" },
|
||||
@@ -210,6 +217,26 @@ function getInterviewActionLabel(status: AiSessionSummary["status"]): string {
|
||||
}
|
||||
}
|
||||
|
||||
function getMissionRunHelperText(status: MissionStatus): string | null {
|
||||
switch (status) {
|
||||
case "planning":
|
||||
return "Starting activates the first slice so work can begin.";
|
||||
case "active":
|
||||
return "Stopping pauses linked tasks and marks the mission blocked.";
|
||||
case "blocked":
|
||||
return "Resuming re-activates the mission and continues execution.";
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function getAutopilotStateLabel(state: string): string {
|
||||
if (state in autopilotStateLabels) {
|
||||
return autopilotStateLabels[state as AutopilotState];
|
||||
}
|
||||
return state ? state.replace(/_/g, " ") : "Unknown";
|
||||
}
|
||||
|
||||
/** Get the plan state for a milestone (derived from interviewState) */
|
||||
function getMilestonePlanState(interviewState?: string): "not_started" | "planned" | "needs_update" {
|
||||
if (interviewState === "completed") return "planned";
|
||||
@@ -2407,91 +2434,107 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* ── Autopilot section ── */}
|
||||
<div className="mission-detail__autopilot">
|
||||
<div className="mission-detail__autopilot-toggle">
|
||||
<label className="mission-toggle" data-testid="mission-autopilot-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMission.autopilotEnabled ?? false}
|
||||
onChange={(e) => handleToggleAutopilot(selectedMission.id, e.target.checked)}
|
||||
aria-label="Autopilot"
|
||||
/>
|
||||
<span className="mission-toggle__track" aria-hidden="true">
|
||||
<span className="mission-toggle__thumb" />
|
||||
<section className="mission-detail__run-settings" aria-label="Mission run settings">
|
||||
<h4 className="mission-detail__run-settings-title">Mission run settings</h4>
|
||||
{/* ── Autopilot section ── */}
|
||||
<div className="mission-detail__autopilot">
|
||||
<div className="mission-detail__autopilot-toggle">
|
||||
<label className="mission-toggle" data-testid="mission-autopilot-toggle">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={selectedMission.autopilotEnabled ?? false}
|
||||
onChange={(e) => handleToggleAutopilot(selectedMission.id, e.target.checked)}
|
||||
aria-label="Autopilot"
|
||||
/>
|
||||
<span className="mission-toggle__track" aria-hidden="true">
|
||||
<span className="mission-toggle__thumb" />
|
||||
</span>
|
||||
<span className="mission-toggle__label">
|
||||
<Zap size={14} className="mission-detail__autopilot-icon" />
|
||||
Autopilot
|
||||
</span>
|
||||
</label>
|
||||
<span
|
||||
className="mission-status-badge mission-status-badge--sm"
|
||||
style={{
|
||||
backgroundColor: (autopilotStateColors[autopilotState] || autopilotStateColors.inactive).bg,
|
||||
color: (autopilotStateColors[autopilotState] || autopilotStateColors.inactive).text,
|
||||
}}
|
||||
data-testid="autopilot-state-badge"
|
||||
>
|
||||
{autopilotPulseActive && <span className="mission-detail__autopilot-pulse" />}
|
||||
{getAutopilotStateLabel(autopilotState)}
|
||||
</span>
|
||||
<span className="mission-toggle__label">
|
||||
<Zap size={14} className="mission-detail__autopilot-icon" />
|
||||
Autopilot
|
||||
</span>
|
||||
</label>
|
||||
<span
|
||||
className="mission-status-badge mission-status-badge--sm"
|
||||
style={{
|
||||
backgroundColor: (autopilotStateColors[autopilotState] || autopilotStateColors.inactive).bg,
|
||||
color: (autopilotStateColors[autopilotState] || autopilotStateColors.inactive).text,
|
||||
}}
|
||||
data-testid="autopilot-state-badge"
|
||||
>
|
||||
{autopilotPulseActive && <span className="mission-detail__autopilot-pulse" />}
|
||||
{autopilotState}
|
||||
</div>
|
||||
<span className="mission-detail__autopilot-description">
|
||||
When on, Fusion automatically activates the next slice and plans its features as work completes.
|
||||
</span>
|
||||
{autopilotActivitySummary && (
|
||||
<span className="mission-detail__autopilot-activity mission-relative-time">
|
||||
{autopilotActivitySummary}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{autopilotActivitySummary && (
|
||||
<span className="mission-detail__autopilot-activity mission-relative-time">
|
||||
{autopilotActivitySummary}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="mission-detail__actions">
|
||||
{selectedMission.status === "active" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--danger"
|
||||
onClick={() => handleStopMission(selectedMission.id)}
|
||||
title="Stop mission"
|
||||
aria-label="Stop mission"
|
||||
>
|
||||
<Square size={14} />
|
||||
</button>
|
||||
)}
|
||||
{selectedMission.status === "blocked" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--success"
|
||||
onClick={() => handleResumeMission(selectedMission.id)}
|
||||
title="Resume mission"
|
||||
aria-label="Resume mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
</button>
|
||||
)}
|
||||
{selectedMission.status === "planning" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--success"
|
||||
onClick={() => handleStartMission(selectedMission.id)}
|
||||
title="Start mission"
|
||||
aria-label="Start mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="mission-icon-btn"
|
||||
onClick={() => handleEditMission(selectedMission)}
|
||||
title="Edit mission"
|
||||
aria-label="Edit mission"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--danger"
|
||||
onClick={() => setDeleteConfirmId({ type: "mission", id: selectedMission.id })}
|
||||
title="Delete mission"
|
||||
aria-label="Delete mission"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
<div className="mission-detail__actions">
|
||||
<div className="mission-detail__run-controls">
|
||||
{selectedMission.status === "active" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--danger"
|
||||
onClick={() => handleStopMission(selectedMission.id)}
|
||||
title="Stop mission"
|
||||
aria-label="Stop mission"
|
||||
>
|
||||
<Square size={14} />
|
||||
<span>Stop mission</span>
|
||||
</button>
|
||||
)}
|
||||
{selectedMission.status === "blocked" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--primary"
|
||||
onClick={() => handleResumeMission(selectedMission.id)}
|
||||
title="Resume mission"
|
||||
aria-label="Resume mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
<span>Resume mission</span>
|
||||
</button>
|
||||
)}
|
||||
{selectedMission.status === "planning" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--primary"
|
||||
onClick={() => handleStartMission(selectedMission.id)}
|
||||
title="Start mission"
|
||||
aria-label="Start mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
<span>Start mission</span>
|
||||
</button>
|
||||
)}
|
||||
{getMissionRunHelperText(selectedMission.status) && (
|
||||
<span className="mission-detail__run-help">{getMissionRunHelperText(selectedMission.status)}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="mission-detail__management-actions">
|
||||
<button
|
||||
className="mission-icon-btn"
|
||||
onClick={() => handleEditMission(selectedMission)}
|
||||
title="Edit mission"
|
||||
aria-label="Edit mission"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--danger"
|
||||
onClick={() => setDeleteConfirmId({ type: "mission", id: selectedMission.id })}
|
||||
title="Delete mission"
|
||||
aria-label="Delete mission"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
{/* Inline edit mission form (detail view) */}
|
||||
@@ -4087,37 +4130,49 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
)}
|
||||
</div>
|
||||
<div className="mission-list__item-actions" onClick={(e) => e.stopPropagation()}>
|
||||
{m.status === "active" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--danger"
|
||||
onClick={() => handleStopMission(m.id)}
|
||||
title="Stop mission"
|
||||
>
|
||||
<Square size={14} />
|
||||
</button>
|
||||
)}
|
||||
{m.status === "blocked" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--success"
|
||||
onClick={() => handleResumeMission(m.id)}
|
||||
title="Resume mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
</button>
|
||||
)}
|
||||
{m.status === "planning" && (
|
||||
<button
|
||||
className="mission-icon-btn mission-icon-btn--success"
|
||||
onClick={() => handleStartMission(m.id)}
|
||||
title="Start mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
</button>
|
||||
)}
|
||||
<div className="mission-list__item-run-controls">
|
||||
{m.status === "active" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--danger mission-btn--sm"
|
||||
onClick={() => handleStopMission(m.id)}
|
||||
title="Stop mission"
|
||||
aria-label="Stop mission"
|
||||
>
|
||||
<Square size={14} />
|
||||
<span>Stop mission</span>
|
||||
</button>
|
||||
)}
|
||||
{m.status === "blocked" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--primary mission-btn--sm"
|
||||
onClick={() => handleResumeMission(m.id)}
|
||||
title="Resume mission"
|
||||
aria-label="Resume mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
<span>Resume mission</span>
|
||||
</button>
|
||||
)}
|
||||
{m.status === "planning" && (
|
||||
<button
|
||||
className="mission-btn mission-btn--primary mission-btn--sm"
|
||||
onClick={() => handleStartMission(m.id)}
|
||||
title="Start mission"
|
||||
aria-label="Start mission"
|
||||
>
|
||||
<Play size={14} />
|
||||
<span>Start mission</span>
|
||||
</button>
|
||||
)}
|
||||
{getMissionRunHelperText(m.status) && (
|
||||
<span className="mission-list__item-run-help">{getMissionRunHelperText(m.status)}</span>
|
||||
)}
|
||||
</div>
|
||||
<button
|
||||
className="mission-icon-btn"
|
||||
onClick={() => handleEditMission(mission)}
|
||||
title="Edit mission"
|
||||
aria-label="Edit mission"
|
||||
>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
@@ -4125,6 +4180,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
||||
className="mission-icon-btn mission-icon-btn--danger"
|
||||
onClick={() => setDeleteConfirmId({ type: "mission", id: m.id })}
|
||||
title="Delete mission"
|
||||
aria-label="Delete mission"
|
||||
>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { render, screen, fireEvent, waitFor, act, within } from "@testing-library/react";
|
||||
import { render, screen, fireEvent, waitFor, act, within, cleanup } from "@testing-library/react";
|
||||
import { MissionManager } from "../MissionManager";
|
||||
import { loadAllAppCssBaseOnly } from "../../test/cssFixture";
|
||||
|
||||
@@ -3588,6 +3588,57 @@ describe("MissionManager", () => {
|
||||
});
|
||||
}
|
||||
|
||||
it("renders labeled run controls and calls matching mission status endpoints", async () => {
|
||||
const runControlMissions = [
|
||||
{ ...autopilotMockMissions[0], id: "M-RUN-ACTIVE", title: "Run Active", status: "active" },
|
||||
{ ...autopilotMockMissions[1], id: "M-RUN-PLANNING", title: "Run Planning", status: "planning" },
|
||||
{ ...autopilotMockMissions[1], id: "M-RUN-BLOCKED", title: "Run Blocked", status: "blocked" },
|
||||
];
|
||||
|
||||
const fetchMock = vi.fn().mockImplementation((url: string, options?: RequestInit) => {
|
||||
if (url.includes("/health")) {
|
||||
const missionId = extractMissionId(url) ?? "M-RUN-PLANNING";
|
||||
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
|
||||
}
|
||||
|
||||
if (url.includes("/start") || url.includes("/stop") || url.includes("/resume")) {
|
||||
return Promise.resolve(mockApiResponse({ ok: true }));
|
||||
}
|
||||
|
||||
if (url.includes("/api/missions/") && !url.includes("/milestones") && !url.includes("/status")) {
|
||||
const mission = runControlMissions.find((item) => url.includes(item.id)) ?? runControlMissions[0];
|
||||
return Promise.resolve(mockApiResponse({ ...mission, milestones: [] }));
|
||||
}
|
||||
|
||||
return Promise.resolve(mockApiResponse(runControlMissions));
|
||||
});
|
||||
|
||||
globalThis.fetch = fetchMock;
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByText("Run Active")).toBeDefined();
|
||||
});
|
||||
|
||||
const startButtons = screen.getAllByRole("button", { name: "Start mission" });
|
||||
const stopButtons = screen.getAllByRole("button", { name: "Stop mission" });
|
||||
const resumeButtons = screen.getAllByRole("button", { name: "Resume mission" });
|
||||
|
||||
expect(startButtons.length).toBeGreaterThan(0);
|
||||
expect(stopButtons.length).toBeGreaterThan(0);
|
||||
expect(resumeButtons.length).toBeGreaterThan(0);
|
||||
|
||||
fireEvent.click(startButtons[0]);
|
||||
fireEvent.click(stopButtons[0]);
|
||||
fireEvent.click(resumeButtons[0]);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(fetchMock.mock.calls.some(([url]) => String(url).includes("/api/missions/M-RUN-PLANNING/start"))).toBe(true);
|
||||
expect(fetchMock.mock.calls.some(([url]) => String(url).includes("/api/missions/M-RUN-ACTIVE/stop"))).toBe(true);
|
||||
expect(fetchMock.mock.calls.some(([url]) => String(url).includes("/api/missions/M-RUN-BLOCKED/resume"))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
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()} />);
|
||||
@@ -3612,6 +3663,60 @@ describe("MissionManager", () => {
|
||||
expect(autopilotIcons).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("shows autopilot toggle, helper copy, and humanized state labels", async () => {
|
||||
const stateCases: Array<{ state: "inactive" | "watching" | "activating" | "completing"; label: string }> = [
|
||||
{ state: "inactive", label: "Off" },
|
||||
{ state: "watching", label: "Watching" },
|
||||
{ state: "activating", label: "Activating slice" },
|
||||
{ state: "completing", label: "Completing" },
|
||||
];
|
||||
|
||||
for (const stateCase of stateCases) {
|
||||
const fetchMock = vi.fn().mockImplementation((url: string) => {
|
||||
if (url.includes("/health")) {
|
||||
const missionId = extractMissionId(url) ?? "M-AUTO1";
|
||||
return Promise.resolve(mockApiResponse(getMockMissionHealth(missionId)));
|
||||
}
|
||||
|
||||
if (url.includes("/autopilot")) {
|
||||
return Promise.resolve(mockApiResponse({
|
||||
enabled: true,
|
||||
state: stateCase.state,
|
||||
watched: stateCase.state !== "inactive",
|
||||
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,
|
||||
autopilotState: stateCase.state,
|
||||
}));
|
||||
}
|
||||
|
||||
return Promise.resolve(mockApiResponse([{ ...autopilotMockMissions[0], autopilotState: stateCase.state }]));
|
||||
});
|
||||
|
||||
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"));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByLabelText("Autopilot")).toBeDefined();
|
||||
expect(screen.getByText("When on, Fusion automatically activates the next slice and plans its features as work completes.")).toBeDefined();
|
||||
expect(screen.getByTestId("autopilot-state-badge").textContent).toContain(stateCase.label);
|
||||
});
|
||||
|
||||
expect(screen.queryByText(stateCase.state)).toBeNull();
|
||||
cleanup();
|
||||
}
|
||||
});
|
||||
|
||||
it("shows autopilot toggle and status badge in detail view", async () => {
|
||||
globalThis.fetch = createAutopilotFetchMock();
|
||||
render(<MissionManager isOpen={true} onClose={vi.fn()} addToast={vi.fn()} />);
|
||||
|
||||
Reference in New Issue
Block a user