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:
@@ -51,6 +51,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`, or `error`) before a mission record is created
|
- Resume in-progress mission interview sessions directly from separate transient session rows in the main missions list (`mission_interview` sessions in `generating`, `awaiting_input`, or `error`) before a mission record is created
|
||||||
|
- Banner-driven mission interview resumes are one-shot: if you close or send the interview to background, Missions re-fetches project-scoped `mission_interview` sessions and re-surfaces the transient row (including on the mobile stacked Missions view) so resume/retry remains discoverable without losing persisted `interviewState: "in_progress"` mission cards
|
||||||
|
|
||||||
### Auto-Generated Assertions
|
### Auto-Generated Assertions
|
||||||
|
|
||||||
|
|||||||
@@ -566,7 +566,10 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
// Pending mission interview sessions (for resume prompt after page reload)
|
// Pending mission interview sessions (for resume prompt after page reload)
|
||||||
const [pendingInterviewSessions, setPendingInterviewSessions] = useState<AiSessionSummary[]>([]);
|
const [pendingInterviewSessions, setPendingInterviewSessions] = useState<AiSessionSummary[]>([]);
|
||||||
const [localResumeSessionId, setLocalResumeSessionId] = useState<string | undefined>(undefined);
|
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
|
// Milestone/Slice interview modal
|
||||||
const [interviewTarget, setInterviewTarget] = useState<{
|
const [interviewTarget, setInterviewTarget] = useState<{
|
||||||
@@ -590,6 +593,13 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
}
|
}
|
||||||
}, [isActive, effectiveResumeSessionId]);
|
}, [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
|
// Detect pending mission interview sessions for resume prompt
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isActive || effectiveResumeSessionId) return;
|
if (!isActive || effectiveResumeSessionId) return;
|
||||||
@@ -3504,6 +3514,12 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
setShowInterviewModal(true);
|
setShowInterviewModal(true);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleInterviewModalClose = () => {
|
||||||
|
dismissedResumeSessionIdRef.current = effectiveResumeSessionId ?? null;
|
||||||
|
setLocalResumeSessionId(undefined);
|
||||||
|
setShowInterviewModal(false);
|
||||||
|
};
|
||||||
|
|
||||||
const renderInterviewSessionItems = () => pendingInterviewSessions.map((session) => {
|
const renderInterviewSessionItems = () => pendingInterviewSessions.map((session) => {
|
||||||
const isErrored = session.status === "error";
|
const isErrored = session.status === "error";
|
||||||
const isGenerating = session.status === "generating";
|
const isGenerating = session.status === "generating";
|
||||||
@@ -4035,7 +4051,7 @@ export function MissionManager({ isOpen, isInline = false, onClose, addToast, pr
|
|||||||
const interviewModal = (
|
const interviewModal = (
|
||||||
<MissionInterviewModal
|
<MissionInterviewModal
|
||||||
isOpen={showInterviewModal}
|
isOpen={showInterviewModal}
|
||||||
onClose={() => setShowInterviewModal(false)}
|
onClose={handleInterviewModalClose}
|
||||||
onMissionCreated={() => {
|
onMissionCreated={() => {
|
||||||
loadMissions();
|
loadMissions();
|
||||||
addToast("Mission created from AI interview", "success");
|
addToast("Mission created from AI interview", "success");
|
||||||
|
|||||||
@@ -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 () => {
|
it("keeps persisted interview-stage missions visible with interview styling and mission selection behavior", async () => {
|
||||||
const missionsWithInterview = [
|
const missionsWithInterview = [
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user