feat(FN-3419): remove inline lock indicator from PlanningModeModal

Removed the inline lock indicator from PlanningModeModal and added a new test file covering the planning flow behavior.

Fusion-Task-Id: FN-3419
This commit is contained in:
Fusion
2026-05-04 17:55:34 -07:00
committed by gsxdsm
parent 2b809fc99e
commit e71e848059
3 changed files with 65 additions and 13 deletions

View File

@@ -1519,7 +1519,6 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
const activeLockInfo = lockSessionId ? activeTabMap.get(lockSessionId) : null;
const activeRemoteTab = activeLockInfo && activeLockInfo.tabId !== sessionTabId;
const activeInAnotherTab = Boolean(activeRemoteTab && !activeLockInfo.stale);
const allowTakeover = isLockedByOther && (!activeRemoteTab || activeLockInfo.stale);
if (!isOpen) return null;
@@ -1585,11 +1584,6 @@ export function PlanningModeModal({ isOpen, onClose, onTaskCreated, onTasksCreat
<div className="planning-detail">
{error && <div className="form-error planning-error">{error}</div>}
{isReconnecting && <div className="form-hint text-muted">Reconnecting</div>}
{activeInAnotherTab && (
<div className="form-hint text-muted" data-testid="session-active-another-tab-banner">
Session is active in another tab.
</div>
)}
{view.type === "initial" && (
<div className="planning-initial">

View File

@@ -6,6 +6,8 @@ import { TaskDetailModal } from "../TaskDetailModal";
import { useSessionLock } from "../../hooks/useSessionLock";
import { getSessionTabId } from "../../utils/getSessionTabId";
import type { MergeResult } from "@fusion/core";
const mockUseAiSessionSync = vi.fn();
import {
mockStartPlanning,
mockStartPlanningStreaming,
@@ -99,6 +101,10 @@ vi.mock("../../hooks/useMobileKeyboard", () => ({
useMobileKeyboard: (...args: any[]) => mockUseMobileKeyboard(...args),
}));
vi.mock("../../hooks/useAiSessionSync", () => ({
useAiSessionSync: (...args: any[]) => mockUseAiSessionSync(...args),
}));
describe("PlanningModeModal", () => {
const mockOnClose = vi.fn();
const mockOnTaskCreated = vi.fn();
@@ -147,6 +153,14 @@ describe("PlanningModeModal", () => {
mockCancelPlanning.mockResolvedValue(undefined);
mockUpdatePlanningSessionDraft.mockResolvedValue({ ok: true });
mockStopPlanningGeneration.mockResolvedValue({ success: true });
mockUseAiSessionSync.mockReturnValue({
activeTabMap: new Map(),
broadcastUpdate: vi.fn(),
broadcastCompleted: vi.fn(),
broadcastLock: vi.fn(),
broadcastUnlock: vi.fn(),
broadcastHeartbeat: vi.fn(),
});
// Default: simulate receiving a question after a brief delay
mockConnectPlanningStream.mockImplementation((_sessionId: string, _projectId: string | undefined, handlers: any) => {
@@ -228,6 +242,50 @@ describe("PlanningModeModal", () => {
});
});
it("does not render duplicate inline lock text while takeover overlay handles lock state", async () => {
window.sessionStorage.setItem("fusion-tab-id", "tab-self");
mockAcquireSessionLock.mockResolvedValueOnce({ acquired: false, currentHolder: "tab-other" });
mockUseAiSessionSync.mockReturnValueOnce({
activeTabMap: new Map([
[
"session-123",
{
tabId: "tab-other",
stale: false,
},
],
]),
broadcastUpdate: vi.fn(),
broadcastCompleted: vi.fn(),
broadcastLock: vi.fn(),
broadcastUnlock: vi.fn(),
broadcastHeartbeat: vi.fn(),
});
render(
<PlanningModeModal
isOpen={true}
onClose={mockOnClose}
onTaskCreated={mockOnTaskCreated}
onTasksCreated={vi.fn()}
tasks={mockTasks}
/>,
);
fireEvent.change(screen.getByPlaceholderText(/e.g., Build a user authentication/), {
target: { value: "Build auth system" },
});
fireEvent.click(screen.getByText("Start Planning"));
await waitFor(() => {
expect(screen.getByTestId("session-lock-overlay")).toBeDefined();
});
expect(screen.getByText("This session is active in another tab")).toBeDefined();
expect(screen.queryByText("Session is active in another tab.")).toBeNull();
expect(screen.getByRole("button", { name: "Take Control" })).toBeDefined();
});
it("allows normal question interaction when lock is acquired", async () => {
window.sessionStorage.setItem("fusion-tab-id", "tab-self");

View File

@@ -2582,6 +2582,13 @@ export async function aiMergeTask(
): Promise<MergeResult> {
throwIfAborted(options.signal, taskId);
// 1. Validate task state
const task = await store.getTask(taskId);
const mergeBlocker = getTaskMergeBlocker(task);
if (mergeBlocker) {
throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
}
// Pre-merge guard against the common single-checkout setup where rootDir
// is the developer's working tree. The merge flow below issues several
// `git reset --hard/--merge` calls and forced checkouts that would
@@ -2591,13 +2598,6 @@ export async function aiMergeTask(
const autostashRef = await stashUnrelatedRootDirChanges(rootDir, taskId);
try {
// 1. Validate task state
const task = await store.getTask(taskId);
const mergeBlocker = getTaskMergeBlocker(task);
if (mergeBlocker) {
throw new Error(`Cannot merge ${taskId}: ${mergeBlocker}`);
}
const branch = task.branch || `fusion/${taskId.toLowerCase()}`;
const sourceIssueRef = buildSourceIssueRef(task.sourceIssue);
const worktreePath = task.worktree;