diff --git a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx index c87c1e534a..88b90339c6 100644 --- a/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/MilestoneSliceInterviewModal.test.tsx @@ -931,6 +931,43 @@ describe("MilestoneSliceInterviewModal", () => { }); }); + /* + FNXC:PlanningMultiTab 2026-07-16-17:35: + A legacy persisted lock holder must never gate either milestone or slice interview resumption. + The multi-tab contract opens the shared session directly without a Take Control affordance. + */ + it.each([ + ["milestone", "MS-001", "Test Milestone", "milestone_interview"], + ["slice", "SL-001", "Test Slice", "slice_interview"], + ] as const)("resumes a legacy other-tab-owned %s session without a lock affordance", async (targetType, targetId, targetTitle, type) => { + mockFetchAiSession.mockResolvedValue({ + ...mockSessionAwaitingInput, + type, + lockedByTab: "tab-other", + lockedAt: new Date().toISOString(), + }); + mockAcquireSessionLock.mockResolvedValue({ acquired: false, currentHolder: "tab-other" }); + + render( + , + ); + + expect(await screen.findByText("What is the target scope?")).toBeDefined(); + expect(screen.queryByRole("button", { name: /take control/i })).toBeNull(); + expect(screen.queryByText(/active in another tab|live heartbeat/i)).toBeNull(); + expect(mockAcquireSessionLock).not.toHaveBeenCalled(); + expect(mockForceAcquireSessionLock).not.toHaveBeenCalled(); + }); + it("reconnects to stream for generating session when resumeSessionId is provided", async () => { mockFetchAiSession.mockResolvedValue(mockSessionGenerating); diff --git a/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx b/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx index 65550efd72..3f1cb88aeb 100644 --- a/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx +++ b/packages/dashboard/app/components/__tests__/utility-mobile.test.tsx @@ -120,6 +120,66 @@ describe("Utility component mobile adaptations", () => { expect(screen.getByText("Break down API tasks")).toBeTruthy(); }); + /* + FNXC:PlanningMultiTab 2026-07-16-17:35: + Background task rows open directly on mobile for every session state, even when a legacy + session payload reports another tab as its prior lock holder. No confirm gate or lock banner + may be reintroduced because interviews are intentionally multi-tab. + */ + it("opens legacy other-tab-owned sessions directly without a lock affordance", () => { + const onOpenSession = vi.fn(); + const sessions = [ + { + id: "sess-generating-other-tab", + type: "planning", + status: "generating", + title: "Generating plan", + projectId: "proj-1", + updatedAt: new Date().toISOString(), + lockedByTab: "tab-other", + }, + { + id: "sess-awaiting-other-tab", + type: "mission_interview", + status: "awaiting_input", + title: "Awaiting mission input", + projectId: "proj-1", + updatedAt: new Date().toISOString(), + lockedByTab: "tab-other", + }, + { + id: "sess-failed-other-tab", + type: "slice_interview", + status: "error", + title: "Failed slice interview", + projectId: "proj-1", + updatedAt: new Date().toISOString(), + lockedByTab: "tab-other", + }, + ] as unknown as AiSessionSummary[]; + + render( + , + ); + + for (const session of sessions) { + fireEvent.click(screen.getByRole("button", { name: /AI 3/i })); + fireEvent.click(screen.getByText(session.title)); + } + + expect(onOpenSession).toHaveBeenNthCalledWith(1, sessions[0]); + expect(onOpenSession).toHaveBeenNthCalledWith(2, sessions[1]); + expect(onOpenSession).toHaveBeenNthCalledWith(3, sessions[2]); + expect(screen.queryByRole("button", { name: /take control/i })).toBeNull(); + expect(screen.queryByText(/active in another tab|live heartbeat/i)).toBeNull(); + }); + it("returns null for BackgroundTasksIndicator with no sessions", () => { const { container } = render(