FN-5895: fix MissionManager interview resume behavior

Keep MissionManager mission interview state and detail rendering in sync while stabilizing the failing test coverage.

- reload assertion-linked feature data when mission details or milestone panels load
- keep selected mission detail state synchronized after mission update events instead of forcing a full mission reload
- hide the sidebar edit form while the selected mission is already being edited in the detail pane
- add a non-destructive Send to background action for resume-launched mission interview modals
- tighten MissionManager tests to target the detail pane and slice-scoped controls that now render alongside sidebar content

Files changed:
docs/missions.md                                   |  2 +-
 .../app/components/MissionInterviewModal.tsx       | 30 ++++++++
 .../dashboard/app/components/MissionManager.tsx    | 77 +++++++++++--------
 .../components/__tests__/MissionManager.test.tsx   | 88 +++++++++++++++-------
 4 files changed, 137 insertions(+), 60 deletions(-)

Fusion-Task-Id: FN-5895

Fusion-Task-Lineage: d018a945-33a7-4471-9636-35bc36dc88a9
This commit is contained in:
gsxdsm
2026-06-02 14:30:35 -07:00
parent b95e7f7575
commit 8d9bbc1906
4 changed files with 137 additions and 60 deletions

View File

@@ -159,7 +159,7 @@ The dashboard supports mission planning workflows where you can:
- Track progress at each layer - Track progress at each layer
- Persisted missions with `interviewState: "in_progress"` remain visible as interview-styled mission cards in the main mission list so planning work does not disappear after reloads - Persisted missions with `interviewState: "in_progress"` remain visible as interview-styled mission cards in the main mission list so planning work does not disappear after reloads
- Resume in-progress mission interview sessions directly from separate transient session rows in the main missions list (`mission_interview` sessions in `generating`, `awaiting_input`, `error`, or `complete`) before a mission record is created; `complete` means the plan summary is ready for review/approval but has not been converted into a mission yet - Resume in-progress mission interview sessions directly from separate transient session rows in the main missions list (`mission_interview` sessions in `generating`, `awaiting_input`, `error`, or `complete`) before a mission record is created; `complete` means the plan summary is ready for review/approval but has not been converted into a mission yet
- Mission interview closes are non-destructive: the modal now uses a single close action for header close, backdrop click, and Escape. Closing preserves the in-progress `mission_interview` session, and Missions re-fetches project-scoped transient rows (including on the mobile stacked Missions view) so resume/retry remains discoverable without losing persisted `interviewState: "in_progress"` mission cards. Deletion remains an explicit sidebar action. - Mission interview closes are non-destructive: the modal now uses a single close action for header close, backdrop click, and Escape. Closing preserves the in-progress `mission_interview` session, and Missions re-fetches project-scoped transient rows (including on the mobile stacked Missions view) so resume/retry remains discoverable without losing persisted `interviewState: "in_progress"` mission cards. Resume-launched interview modals also expose a **Send to background** action that performs the same non-destructive park without cancelling the session. Deletion remains an explicit sidebar action.
- Mission interview, milestone interview, and slice interview agents have read-only board visibility via `fn_task_list` and `fn_task_get`, so they can reference active backlog context and avoid duplicating in-flight tasks while asking planning questions - Mission interview, milestone interview, and slice interview agents have read-only board visibility via `fn_task_list` and `fn_task_get`, so they can reference active backlog context and avoid duplicating in-flight tasks while asking planning questions
### Mission Interview Drafts ### Mission Interview Drafts

View File

@@ -41,6 +41,7 @@ import {
Trash2, Trash2,
RefreshCw, RefreshCw,
Lock, Lock,
Minimize2,
} from "lucide-react"; } from "lucide-react";
import { ConversationHistory } from "./ConversationHistory"; import { ConversationHistory } from "./ConversationHistory";
import { CustomModelDropdown } from "./CustomModelDropdown"; import { CustomModelDropdown } from "./CustomModelDropdown";
@@ -78,6 +79,8 @@ interface MissionInterviewModalProps {
projectId?: string; projectId?: string;
initialGoal?: string; initialGoal?: string;
resumeSessionId?: string; resumeSessionId?: string;
onSendToBackground?: () => void;
showSendToBackgroundButton?: boolean;
} }
interface QuestionResponse { interface QuestionResponse {
@@ -105,6 +108,8 @@ export function MissionInterviewModal({
projectId, projectId,
initialGoal: initialGoalProp, initialGoal: initialGoalProp,
resumeSessionId, resumeSessionId,
onSendToBackground,
showSendToBackgroundButton = false,
}: MissionInterviewModalProps) { }: MissionInterviewModalProps) {
useMobileScrollLock(isOpen); useMobileScrollLock(isOpen);
const [missionGoal, setMissionGoal] = useState(""); const [missionGoal, setMissionGoal] = useState("");
@@ -127,6 +132,7 @@ export function MissionInterviewModal({
const trackedLockSessionRef = useRef<string | null>(null); const trackedLockSessionRef = useRef<string | null>(null);
const [lockSessionId, setLockSessionId] = useState<string | null>(resumeSessionId ?? null); const [lockSessionId, setLockSessionId] = useState<string | null>(resumeSessionId ?? null);
const sessionTabId = useMemo(() => getSessionTabId(), []); const sessionTabId = useMemo(() => getSessionTabId(), []);
const canSendToBackground = showSendToBackgroundButton && view.type !== "initial";
const { const {
isLockedByOther, isLockedByOther,
takeControl, takeControl,
@@ -529,6 +535,20 @@ export function MissionInterviewModal({
onClose(); onClose();
}, [missionGoal, onClose, projectId, view.type]); }, [missionGoal, onClose, projectId, view.type]);
const handleSendToBackground = useCallback(() => {
streamConnectionRef.current?.close();
streamConnectionRef.current = null;
overlayMouseDownOnSelfRef.current = false;
setIsReconnecting(false);
setIsRetrying(false);
setIsCreating(false);
if (onSendToBackground) {
onSendToBackground();
return;
}
onClose();
}, [onClose, onSendToBackground]);
// Escape key handler // Escape key handler
useEffect(() => { useEffect(() => {
if (!isOpen) return; if (!isOpen) return;
@@ -736,6 +756,16 @@ export function MissionInterviewModal({
<h3>Plan Mission with AI</h3> <h3>Plan Mission with AI</h3>
</div> </div>
<div className="modal-header-actions"> <div className="modal-header-actions">
{canSendToBackground && (
<button
className="modal-send-to-background"
onClick={handleSendToBackground}
title="Send to background"
aria-label="Send to background"
>
<Minimize2 size={16} />
</button>
)}
<button className="modal-close" onClick={handleClose} aria-label="Close"> <button className="modal-close" onClick={handleClose} aria-label="Close">
<X size={20} /> <X size={20} />
</button> </button>

View File

@@ -783,6 +783,37 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
}); });
}, [loadMissionInterviewDraftRows, loadPendingInterviewSessions]); }, [loadMissionInterviewDraftRows, loadPendingInterviewSessions]);
const loadAssertionsForMilestone = useCallback(async (milestoneId: string) => {
try {
const assertions = await fetchAssertions(milestoneId, projectId);
const linkedFeatureEntries = await Promise.all(
assertions.map(async (assertion): Promise<readonly [string, MissionFeature[]]> => {
try {
const features = await fetchFeaturesForAssertion(assertion.id, projectId);
return [assertion.id, features] as const;
} catch {
return [assertion.id, []] as const;
}
}),
);
setAssertionsByMilestone((prev) => {
const next = new Map(prev);
next.set(milestoneId, assertions);
return next;
});
setLinkedFeaturesByAssertion((prev) => {
const next = new Map(prev);
for (const [assertionId, features] of linkedFeatureEntries) {
next.set(assertionId, features);
}
return next;
});
} catch {
// Silently fail - assertions are optional
}
}, [projectId]);
// Detect pending mission interview sessions for resume prompt // Detect pending mission interview sessions for resume prompt
useEffect(() => { useEffect(() => {
if (!isActive) return; if (!isActive) return;
@@ -1034,13 +1065,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
}); });
// Load assertions and validation rollup for the selected milestone. // Load assertions and validation rollup for the selected milestone.
fetchAssertions(nextSelectedMilestoneId, projectId).then((assertions) => { void loadAssertionsForMilestone(nextSelectedMilestoneId);
setAssertionsByMilestone((prev) => {
const next = new Map(prev);
next.set(nextSelectedMilestoneId, assertions);
return next;
});
}).catch(() => { /* silently fail */ });
fetchMilestoneValidation(nextSelectedMilestoneId, projectId).then((rollup) => { fetchMilestoneValidation(nextSelectedMilestoneId, projectId).then((rollup) => {
setValidationRollupByMilestone((prev) => { setValidationRollupByMilestone((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -1058,7 +1083,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
} finally { } finally {
setDetailLoading(false); setDetailLoading(false);
} }
}, [addToast, projectId]); }, [addToast, loadAssertionsForMilestone, projectId]);
useEffect(() => { useEffect(() => {
if (!isActive || !selectedMilestoneId) { if (!isActive || !selectedMilestoneId) {
@@ -1297,24 +1322,27 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const handleMissionUpdated = (rawEvent: Event) => { const handleMissionUpdated = (rawEvent: Event) => {
refreshHealth(); refreshHealth();
// Update mission status in the list to keep badges in sync
const messageEvent = rawEvent as MessageEvent<string>; const messageEvent = rawEvent as MessageEvent<string>;
if (messageEvent.data) { if (messageEvent.data) {
try { try {
const updatedMission = JSON.parse(messageEvent.data); const updatedMission = JSON.parse(messageEvent.data) as Partial<MissionWithSummary> & { id?: string };
if (updatedMission?.id) { if (updatedMission?.id) {
setMissions((prev) => setMissions((prev) =>
prev.map((m) => prev.map((m) =>
m.id === updatedMission.id ? { ...m, ...updatedMission } : m m.id === updatedMission.id ? { ...m, ...updatedMission } : m
) )
); );
setSelectedMission((prev) =>
prev && prev.id === updatedMission.id
? normalizeMissionHierarchy({ ...prev, ...updatedMission })
: prev
);
} }
} catch { } catch {
// ignore invalid payloads // ignore invalid payloads
} }
} }
void loadMissions();
refreshMissionSidebar(); refreshMissionSidebar();
// Reload the selected mission detail to reflect updated mission state (autopilot, status, etc.) // Reload the selected mission detail to reflect updated mission state (autopilot, status, etc.)
@@ -1754,13 +1782,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
if (isExpanding) { if (isExpanding) {
next.add(milestoneId); next.add(milestoneId);
// Load assertions and validation rollup when expanding milestone // Load assertions and validation rollup when expanding milestone
fetchAssertions(milestoneId, projectId).then((assertions) => { void loadAssertionsForMilestone(milestoneId);
setAssertionsByMilestone((prev) => {
const next = new Map(prev);
next.set(milestoneId, assertions);
return next;
});
}).catch(() => { /* silently fail */ });
fetchMilestoneValidation(milestoneId, projectId).then((rollup) => { fetchMilestoneValidation(milestoneId, projectId).then((rollup) => {
setValidationRollupByMilestone((prev) => { setValidationRollupByMilestone((prev) => {
const next = new Map(prev); const next = new Map(prev);
@@ -1773,7 +1795,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
} }
return next; return next;
}); });
}, [projectId]); }, [loadAssertionsForMilestone, projectId]);
// Slice handlers // Slice handlers
const handleCreateSlice = useCallback((milestoneId: string) => { const handleCreateSlice = useCallback((milestoneId: string) => {
@@ -2017,19 +2039,6 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
// ── Assertion handlers ── // ── Assertion handlers ──
const loadAssertionsForMilestone = useCallback(async (milestoneId: string) => {
try {
const assertions = await fetchAssertions(milestoneId, projectId);
setAssertionsByMilestone((prev) => {
const next = new Map(prev);
next.set(milestoneId, assertions);
return next;
});
} catch {
// Silently fail - assertions are optional
}
}, [projectId]);
const loadValidationRollup = useCallback(async (milestoneId: string) => { const loadValidationRollup = useCallback(async (milestoneId: string) => {
try { try {
const rollup = await fetchMilestoneValidation(milestoneId, projectId); const rollup = await fetchMilestoneValidation(milestoneId, projectId);
@@ -4455,7 +4464,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
{renderMissionListItems(standardMissions)} {renderMissionListItems(standardMissions)}
{/* Edit mission form */} {/* Edit mission form */}
{editingMissionId && ( {editingMissionId && selectedMission?.id !== editingMissionId && (
<div className="mission-form-card"> <div className="mission-form-card">
<input <input
type="text" type="text"
@@ -4805,6 +4814,8 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
key={interviewModalKey} key={interviewModalKey}
isOpen={showInterviewModal} isOpen={showInterviewModal}
onClose={handleInterviewModalClose} onClose={handleInterviewModalClose}
onSendToBackground={handleInterviewModalClose}
showSendToBackgroundButton={interviewLaunchMode === "resume"}
onMissionCreated={() => { onMissionCreated={() => {
loadMissions(); loadMissions();
addToast("Mission created from AI interview", "success"); addToast("Mission created from AI interview", "success");

View File

@@ -2928,9 +2928,12 @@ describe("MissionManager", () => {
fireEvent.click(screen.getByText("Generated Mission")); fireEvent.click(screen.getByText("Generated Mission"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Generated Milestone")).toBeDefined(); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
expect(screen.getByText("Generated Slice")).toBeDefined(); expect(detailPane).toBeTruthy();
expect(screen.getByText("Generated Feature")).toBeDefined(); expect(within(detailPane as HTMLElement).getByText("Generated Milestone")).toBeDefined();
const generatedSlice = within(detailPane as HTMLElement).getByText("Generated Slice").closest(".mission-slice");
expect(generatedSlice).toBeTruthy();
expect(within(generatedSlice as HTMLElement).getByText("Generated Feature")).toBeDefined();
}); });
}); });
@@ -3173,7 +3176,8 @@ describe("MissionManager", () => {
await waitForDetailLoaded(); await waitForDetailLoaded();
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
const targetBranchInput = await screen.findByLabelText("Mission target branch"); const targetBranchInput = await screen.findByLabelText("Mission target branch");
expect(targetBranchInput).toHaveValue("develop"); expect(targetBranchInput).toHaveValue("develop");
@@ -3207,11 +3211,12 @@ describe("MissionManager", () => {
await waitForDetailLoaded(); await waitForDetailLoaded();
fireEvent.click(screen.getAllByLabelText("Edit mission")[0]); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
const targetBranchInput = await screen.findByLabelText("Mission target branch"); const targetBranchInput = await screen.findByLabelText("Mission target branch");
fireEvent.change(targetBranchInput, { target: { value: " release/2026.05 " } }); fireEvent.change(targetBranchInput, { target: { value: " release/2026.05 " } });
fireEvent.click(screen.getByRole("button", { name: "Update" })); fireEvent.click(screen.getByRole("button", { name: /Update/ }));
await waitFor(() => { await waitFor(() => {
const patchCall = fetchMock.mock.calls.find( const patchCall = fetchMock.mock.calls.find(
@@ -3633,12 +3638,18 @@ describe("MissionManager", () => {
}); });
fireEvent.click(screen.getByText("Triage Test Mission")); fireEvent.click(screen.getByText("Triage Test Mission"));
let featureRow: HTMLElement;
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined(); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
expect(detailPane).toBeTruthy();
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
expect(testSlice).toBeTruthy();
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
expect(featureRow).toBeTruthy();
}); });
// Click triage button // Click triage button
fireEvent.click(screen.getByTitle("Triage — create task")); fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
// Preview should appear // Preview should appear
await waitFor(() => { await waitFor(() => {
@@ -3657,12 +3668,18 @@ describe("MissionManager", () => {
}); });
fireEvent.click(screen.getByText("Triage Test Mission")); fireEvent.click(screen.getByText("Triage Test Mission"));
let featureRow: HTMLElement;
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined(); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
expect(detailPane).toBeTruthy();
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
expect(testSlice).toBeTruthy();
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
expect(featureRow).toBeTruthy();
}); });
// Click triage button to show preview // Click triage button to show preview
fireEvent.click(screen.getByTitle("Triage — create task")); fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Enriched Description Preview")).toBeDefined(); expect(screen.getByText("Enriched Description Preview")).toBeDefined();
@@ -3687,12 +3704,18 @@ describe("MissionManager", () => {
}); });
fireEvent.click(screen.getByText("Triage Test Mission")); fireEvent.click(screen.getByText("Triage Test Mission"));
let featureRow: HTMLElement;
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined(); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
expect(detailPane).toBeTruthy();
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
expect(testSlice).toBeTruthy();
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
expect(featureRow).toBeTruthy();
}); });
// Click triage button to show preview // Click triage button to show preview
fireEvent.click(screen.getByTitle("Triage — create task")); fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Enriched Description Preview")).toBeDefined(); expect(screen.getByText("Enriched Description Preview")).toBeDefined();
@@ -3723,12 +3746,18 @@ describe("MissionManager", () => {
}); });
fireEvent.click(screen.getByText("Triage Test Mission")); fireEvent.click(screen.getByText("Triage Test Mission"));
let featureRow: HTMLElement;
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Test Feature")).toBeDefined(); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement | null;
expect(detailPane).toBeTruthy();
const testSlice = within(detailPane as HTMLElement).getByText("Test Slice").closest(".mission-slice");
expect(testSlice).toBeTruthy();
featureRow = within(testSlice as HTMLElement).getByText("Test Feature").closest(".mission-feature") as HTMLElement;
expect(featureRow).toBeTruthy();
}); });
// Click triage button - should fall back to direct triage // Click triage button - should fall back to direct triage
fireEvent.click(screen.getByTitle("Triage — create task")); fireEvent.click(within(featureRow!).getByTitle("Triage — create task"));
// Should call triageFeature directly // Should call triageFeature directly
await waitFor(() => { await waitFor(() => {
@@ -3938,7 +3967,8 @@ describe("MissionManager", () => {
await waitFor(() => { await waitFor(() => {
expect(screen.getByText("Autopilot Mission")).toBeDefined(); expect(screen.getByText("Autopilot Mission")).toBeDefined();
}); });
fireEvent.click(screen.getByText("Autopilot Mission")); const sidebar = document.querySelector(".mission-manager__sidebar") as HTMLElement;
fireEvent.click(within(sidebar).getByText("Autopilot Mission"));
await waitFor(() => { await waitFor(() => {
expect(screen.getByLabelText("Autopilot")).toBeDefined(); expect(screen.getByLabelText("Autopilot")).toBeDefined();
@@ -4292,8 +4322,9 @@ describe("MissionManager", () => {
await waitFor(() => expect(screen.getByText("Build Auth System")).toBeDefined()); await waitFor(() => expect(screen.getByText("Build Auth System")).toBeDefined());
const sidebar = document.querySelector(".mission-manager__sidebar") as HTMLElement; const sidebar = document.querySelector(".mission-manager__sidebar") as HTMLElement;
fireEvent.click(within(sidebar).getByText("Build Auth System")); fireEvent.click(within(sidebar).getByText("Build Auth System"));
await waitFor(() => expect(screen.getByLabelText("Delete mission")).toBeDefined()); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(screen.getByLabelText("Delete mission")); await waitFor(() => expect(within(detailPane).getAllByLabelText("Delete mission")[0]).toBeDefined());
fireEvent.click(within(detailPane).getAllByLabelText("Delete mission")[0]);
await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy()); await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy());
}); });
@@ -4453,8 +4484,9 @@ describe("MissionManager", () => {
await waitFor(() => expect(screen.getByText("Build Auth System")).toBeInTheDocument()); await waitFor(() => expect(screen.getByText("Build Auth System")).toBeInTheDocument());
fireEvent.click(within(document.querySelector(".mission-manager__sidebar") as HTMLElement).getByText("Build Auth System")); fireEvent.click(within(document.querySelector(".mission-manager__sidebar") as HTMLElement).getByText("Build Auth System"));
await waitFor(() => expect(screen.getByLabelText("Delete mission")).toBeInTheDocument()); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(screen.getByLabelText("Delete mission")); await waitFor(() => expect(within(detailPane).getAllByLabelText("Delete mission")[0]).toBeInTheDocument());
fireEvent.click(within(detailPane).getAllByLabelText("Delete mission")[0]);
await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy()); await waitFor(() => expect(document.querySelector(".mission-manager__detail-pane .mission-confirm-panel")).toBeTruthy());
}); });
@@ -5058,7 +5090,7 @@ describe("MissionManager", () => {
if (url.includes("/events")) return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, mockMissionEvents))); if (url.includes("/events")) return Promise.resolve(mockApiResponse(parseMissionEventsResponse(url, mockMissionEvents)));
if (url.includes("/health")) return Promise.resolve(mockApiResponse(getMockMissionHealth(extractMissionId(url) ?? "M-001"))); if (url.includes("/health")) return Promise.resolve(mockApiResponse(getMockMissionHealth(extractMissionId(url) ?? "M-001")));
if (url.includes("/autopilot")) return Promise.resolve(mockApiResponse(mockAutopilotStatus)); if (url.includes("/autopilot")) return Promise.resolve(mockApiResponse(mockAutopilotStatus));
if (url.includes("/milestones/MS-001/assertions/CA-ENF-1/features")) { if (url.includes("/missions/assertions/CA-ENF-1/features")) {
return Promise.resolve(mockApiResponse([{ id: "F-001", title: "User model" }])); return Promise.resolve(mockApiResponse([{ id: "F-001", title: "User model" }]));
} }
if (url.includes("/milestones/MS-001/assertions")) return Promise.resolve(mockApiResponse([assertion])); if (url.includes("/milestones/MS-001/assertions")) return Promise.resolve(mockApiResponse([assertion]));
@@ -5074,7 +5106,9 @@ describe("MissionManager", () => {
fireEvent.click(await screen.findByText("Build Auth System")); fireEvent.click(await screen.findByText("Build Auth System"));
await waitForDetailLoaded(); await waitForDetailLoaded();
expect(await screen.findByTestId("mission-assertion-enforcement-CA-ENF-1")).toHaveTextContent("Enforced gate"); await waitFor(() => {
expect(screen.getByTestId("mission-assertion-enforcement-CA-ENF-1")).toHaveTextContent("Enforced gate");
});
const noAssertionMission = JSON.parse(JSON.stringify(missionDetail)) as typeof missionDetail; const noAssertionMission = JSON.parse(JSON.stringify(missionDetail)) as typeof missionDetail;
noAssertionMission.milestones[0].slices[0].features[0].id = "F-INFO-1"; noAssertionMission.milestones[0].slices[0].features[0].id = "F-INFO-1";
@@ -5169,7 +5203,7 @@ describe("MissionManager", () => {
const milestone = screen.getByText("Database Schema").closest(".mission-milestone"); const milestone = screen.getByText("Database Schema").closest(".mission-milestone");
expect(milestone).toBeTruthy(); expect(milestone).toBeTruthy();
const acceptanceLabel = within(milestone as HTMLElement).getByText("Acceptance:"); const acceptanceLabel = within(milestone as HTMLElement).getAllByText("Acceptance:")[0];
expect(acceptanceLabel.tagName).toBe("STRONG"); expect(acceptanceLabel.tagName).toBe("STRONG");
expect(within(milestone as HTMLElement).getByText("MILESTONE_BOLD").tagName).toBe("STRONG"); expect(within(milestone as HTMLElement).getByText("MILESTONE_BOLD").tagName).toBe("STRONG");
@@ -5179,7 +5213,7 @@ describe("MissionManager", () => {
expect((slice as HTMLElement).querySelectorAll("li")).toHaveLength(1); expect((slice as HTMLElement).querySelectorAll("li")).toHaveLength(1);
expect(within(slice as HTMLElement).getByText("VERIFY_BULLET")).toBeInTheDocument(); expect(within(slice as HTMLElement).getByText("VERIFY_BULLET")).toBeInTheDocument();
const feature = screen.getByText("User model").closest(".mission-feature"); const feature = within(slice as HTMLElement).getByText("User model").closest(".mission-feature");
expect(feature).toBeTruthy(); expect(feature).toBeTruthy();
expect(within(feature as HTMLElement).getByText("FEATURE_DESC_BOLD").tagName).toBe("STRONG"); expect(within(feature as HTMLElement).getByText("FEATURE_DESC_BOLD").tagName).toBe("STRONG");
expect(within(feature as HTMLElement).getByText("FEATURE_AC_BOLD").tagName).toBe("STRONG"); expect(within(feature as HTMLElement).getByText("FEATURE_AC_BOLD").tagName).toBe("STRONG");
@@ -5201,7 +5235,8 @@ describe("MissionManager", () => {
fireEvent.click(screen.getByText("Build Auth System")); fireEvent.click(screen.getByText("Build Auth System"));
await waitForDetailLoaded(); await waitForDetailLoaded();
fireEvent.click(screen.getByLabelText("Edit mission")); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
const strategySelect = await screen.findByLabelText("Mission branch strategy"); const strategySelect = await screen.findByLabelText("Mission branch strategy");
expect(strategySelect).toBeInTheDocument(); expect(strategySelect).toBeInTheDocument();
@@ -5229,12 +5264,13 @@ describe("MissionManager", () => {
fireEvent.click(screen.getByText("Build Auth System")); fireEvent.click(screen.getByText("Build Auth System"));
await waitForDetailLoaded(); await waitForDetailLoaded();
fireEvent.click(screen.getByLabelText("Edit mission")); const detailPane = document.querySelector(".mission-manager__detail-pane") as HTMLElement;
fireEvent.click(within(detailPane).getAllByLabelText("Edit mission")[0]);
fireEvent.change(screen.getByLabelText("Mission target branch"), { target: { value: "release/2026" } }); fireEvent.change(screen.getByLabelText("Mission target branch"), { target: { value: "release/2026" } });
fireEvent.change(screen.getByLabelText("Mission branch strategy"), { target: { value: "custom-new" } }); fireEvent.change(screen.getByLabelText("Mission branch strategy"), { target: { value: "custom-new" } });
fireEvent.change(await screen.findByLabelText("Mission branch name"), { target: { value: "feature/mission-custom" } }); fireEvent.change(await screen.findByLabelText("Mission branch name"), { target: { value: "feature/mission-custom" } });
fireEvent.click(screen.getByRole("button", { name: "Update" })); fireEvent.click(screen.getByRole("button", { name: /Update/ }));
await waitFor(() => { await waitFor(() => {
const patchCall = fetchSpy.mock.calls.find(([input, init]) => const patchCall = fetchSpy.mock.calls.find(([input, init]) =>