feat(FN-4038): repair interview resume latch and document mission resume di

Fixed mission resume behavior in MissionManager by repairing the interview resume latch and added tests covering the feature, with a one-line documentation update to clarify resume discoverability.

Fusion-Task-Id: FN-4038
This commit is contained in:
Fusion
2026-05-11 15:43:04 -07:00
committed by gsxdsm
parent 554d260581
commit 5fd1e19687
3 changed files with 119 additions and 2 deletions

View File

@@ -566,7 +566,10 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
// Pending mission interview sessions (for resume prompt after page reload)
const [pendingInterviewSessions, setPendingInterviewSessions] = useState<AiSessionSummary[]>([]);
const [localResumeSessionId, setLocalResumeSessionId] = useState<string | undefined>(undefined);
const effectiveResumeSessionId = localResumeSessionId ?? resumeSessionId;
const dismissedResumeSessionIdRef = useRef<string | null>(null);
const effectiveResumeSessionId =
localResumeSessionId ??
(resumeSessionId && dismissedResumeSessionIdRef.current === resumeSessionId ? undefined : resumeSessionId);
// Milestone/Slice interview modal
const [interviewTarget, setInterviewTarget] = useState<{
@@ -590,6 +593,13 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
}
}, [isActive, effectiveResumeSessionId]);
// If parent requests a different resume session, allow it to open again.
useEffect(() => {
if (resumeSessionId && dismissedResumeSessionIdRef.current !== resumeSessionId) {
dismissedResumeSessionIdRef.current = null;
}
}, [resumeSessionId]);
// Detect pending mission interview sessions for resume prompt
useEffect(() => {
if (!isActive || effectiveResumeSessionId) return;
@@ -3504,6 +3514,12 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
setShowInterviewModal(true);
};
const handleInterviewModalClose = () => {
dismissedResumeSessionIdRef.current = effectiveResumeSessionId ?? null;
setLocalResumeSessionId(undefined);
setShowInterviewModal(false);
};
const renderInterviewSessionItems = () => pendingInterviewSessions.map((session) => {
const isErrored = session.status === "error";
const isGenerating = session.status === "generating";
@@ -4035,7 +4051,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
const interviewModal = (
<MissionInterviewModal
isOpen={showInterviewModal}
onClose={() => setShowInterviewModal(false)}
onClose={handleInterviewModalClose}
onMissionCreated={() => {
loadMissions();
addToast("Mission created from AI interview", "success");

View File

@@ -1653,6 +1653,106 @@ describe("MissionManager", () => {
});
});
it("re-shows project-scoped transient interview rows after banner resume is backgrounded on mobile", async () => {
mockViewport("mobile");
const missionsWithPersistedInterview = [
{
id: "M-PERSISTED-INTERVIEW",
title: "Persisted mission interview",
description: "Persisted mission row",
status: "planning",
interviewState: "in_progress",
milestones: [],
createdAt: "2026-01-06T00:00:00.000Z",
updatedAt: "2026-01-06T00:00:00.000Z",
},
...mockMissions,
];
mockFetchAiSession.mockResolvedValueOnce({
id: "session-bg-1",
type: "mission_interview",
status: "generating",
title: "Project A transient interview",
inputPayload: JSON.stringify({ missionTitle: "Project A transient interview" }),
conversationHistory: "[]",
currentQuestion: null,
result: null,
thinkingOutput: "",
error: null,
projectId: "project-a",
createdAt: "2026-01-01T00:00:00.000Z",
updatedAt: "2026-01-01T00:00:00.000Z",
});
mockFetchAiSessions.mockResolvedValueOnce([
{
id: "session-bg-1",
type: "mission_interview",
status: "awaiting_input",
title: "Project A transient interview",
projectId: "project-a",
lockedByTab: null,
updatedAt: "2026-01-03T00:00:00.000Z",
},
{
id: "session-other-project",
type: "mission_interview",
status: "awaiting_input",
title: "Project B transient interview",
projectId: "project-b",
lockedByTab: null,
updatedAt: "2026-01-04T00:00:00.000Z",
},
]);
globalThis.fetch = createFetchMockWithHealth(missionsWithPersistedInterview as Array<Record<string, unknown>>, {
...mockMissionHealthById,
"M-PERSISTED-INTERVIEW": {
missionId: "M-PERSISTED-INTERVIEW",
status: "planning",
tasksCompleted: 0,
tasksFailed: 0,
tasksInFlight: 0,
totalTasks: 0,
estimatedCompletionPercent: 0,
autopilotState: "inactive",
autopilotEnabled: false,
},
});
render(
<MissionManager
isOpen={true}
onClose={vi.fn()}
addToast={vi.fn()}
projectId="project-a"
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"));
await waitFor(() => {
expect(screen.queryByText("Plan Mission with AI")).not.toBeInTheDocument();
});
await waitFor(() => {
expect(screen.getByText("Project A transient interview")).toBeInTheDocument();
expect(screen.getByText("Persisted mission interview")).toBeInTheDocument();
});
expect(screen.queryByText("Project B transient interview")).not.toBeInTheDocument();
expect(screen.getByLabelText("Resume interview")).toBeInTheDocument();
expect(mockFetchAiSessions).toHaveBeenCalledWith("project-a");
});
it("keeps persisted interview-stage missions visible with interview styling and mission selection behavior", async () => {
const missionsWithInterview = [
{