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:
@@ -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">
|
||||
|
||||
@@ -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");
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user