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(