Z ZPI 文档 v0.3.8
介绍页

源码

packages/desktop/test/pane-isolation.test.ts

typescript925 行查看原文
import { afterEach, beforeEach, expect, it, vi } from "vitest";
import {
  activatePaneTab,
  closePaneTabs,
  closeTab,
  deleteSessionWithPanes,
  listenPanes,
  navigateFileManager,
  openBrowser,
  openChanges,
  openFile,
  openFileManager,
  openNewPaneTab,
  openReview,
  openTerminal,
  openWebLink,
  reopenPaneTab,
  reorderPaneTab,
  selectReviewSource,
  setPaneOpen,
  terminalExited,
  terminalOutput,
  togglePaneFullView,
  updatePaneFullView,
  usePane,
  visiblePaneTabs,
} from "../src/renderer/pane-store.ts";
import { report, useStore } from "../src/renderer/store.ts";
import type { BrowserState, FilePreview, PaneEvent, Result } from "../src/shared/bridge.ts";

vi.mock("../src/renderer/store.ts", async () => {
  const { create } = await import("zustand");
  const actual = await vi.importActual<typeof import("../src/renderer/store.ts")>("../src/renderer/store.ts");
  return {
    useStore: create(() => ({ selected: undefined, sessions: new Map(), views: new Map(), projects: [] })),
    report: vi.fn(),
    closeSideChat: actual.closeSideChat,
    retainedSessionViews: actual.retainedSessionViews,
  };
});
const ok = <T>(value: T): Result<T> => ({ ok: true, value });
const browser = (id: string, url = "https://example.test/"): BrowserState => ({
  id,
  url,
  title: id,
  loading: false,
  back: false,
  forward: false,
});
const file: FilePreview = { kind: "text", path: "/shared/readme.md", text: "document", truncated: false };
let off: () => void;
let event: (event: PaneEvent) => void;
let serial = 0;
const bridge = {
  onPaneEvent: vi.fn((listener: typeof event) => {
    event = listener;
    return vi.fn();
  }),
  createBrowser: vi.fn(async () => ok(browser(`browser-${++serial}`))),
  readFilePreview: vi.fn(async (): Promise<Result<FilePreview>> => ok(file)),
  getWorkspaceInfo: vi.fn(async () => ok({ cwd: "/shared" })),
  createTerminal: vi.fn(async () => ok({ id: `terminal-${++serial}`, shell: "zsh" })),
  closeBrowser: vi.fn(async (): Promise<Result<void>> => ok(undefined)),
  closeTerminal: vi.fn(async (): Promise<Result<void>> => ok(undefined)),
  deleteSession: vi.fn(async (): Promise<Result<void>> => ok(undefined)),
  closeSideChat: vi.fn(async (): Promise<Result<void>> => ok(undefined)),
};
function select(selected: string | undefined) {
  useStore.setState({ selected });
}
beforeEach(() => {
  vi.clearAllMocks();
  serial = 0;
  vi.stubGlobal("window", { zpi: bridge });
  usePane.setState({
    open: false,
    active: undefined,
    tabs: [],
    recentClosed: [],
    fullView: null,
    preferences: new Map(),
    deletedSessionIds: new Set(),
  });
  useStore.setState({
    selected: "A",
    sessions: new Map(
      ["A", "B", "C"].map((id) => [
        id,
        {
          id,
          title: id,
          cwd: id === "C" ? "/other" : "/shared",
          projectId: null,
          createdAt: 0,
          updatedAt: 0,
        },
      ]),
    ),
    projects: [],
    views: new Map(),
  });
  off = listenPanes();
});
afterEach(() => {
  off();
  terminalOutput.clear();
  terminalExited.clear();
  vi.unstubAllGlobals();
});

it("restores task tabs, active tab and explicit collapse preferences while retaining native instances", async () => {
  await openFile("A", file.path);
  await openBrowser();
  await openTerminal("A");
  const aTabs = visiblePaneTabs("A");
  activatePaneTab(aTabs[0].id);
  setPaneOpen(false);
  select("B");
  expect(visiblePaneTabs("B")).toEqual([]);
  expect(usePane.getState()).toMatchObject({ open: false, active: undefined });
  setPaneOpen(true);
  select("A");
  expect(usePane.getState()).toMatchObject({ open: false, active: aTabs[0].id });
  expect(visiblePaneTabs("A")).toEqual(aTabs);
  select("B");
  const launcher = visiblePaneTabs("B")[0];
  expect(launcher).toMatchObject({ type: "new-tab" });
  expect(usePane.getState()).toMatchObject({ open: true, active: launcher.id });
  await closeTab(launcher.id);
  await openFile("B", file.path);
  expect(visiblePaneTabs("B")[0].id).not.toBe(aTabs[0].id);
  await openFile("B", file.path);
  expect(visiblePaneTabs("B")).toHaveLength(1);
  expect(bridge.closeBrowser).not.toHaveBeenCalled();
  expect(bridge.closeTerminal).not.toHaveBeenCalled();
});

it("isolates task and run changes even within the same workspace", async () => {
  openChanges("A", null);
  const taskId = usePane.getState().active;
  openChanges("A", "run-a");
  const runId = usePane.getState().active;
  select("B");
  expect(visiblePaneTabs("B")).toEqual([]);
  openChanges("B", null);
  expect(usePane.getState().active).not.toBe(taskId);
  await openBrowser();
  const bActive = usePane.getState().active;
  select("A");
  expect(usePane.getState().active).toBe(runId);
  select("C");
  expect(visiblePaneTabs("C")).toEqual([]);
  select("B");
  expect(usePane.getState().active).toBe(bActive);
  await closeTab(taskId ?? "");
  expect(visiblePaneTabs("A").map((tab) => tab.id)).toEqual([runId]);
  expect(usePane.getState().active).toBe(bActive);
});

it("keeps individual operation previews distinct from cumulative changes and repeated tool IDs in other runs", () => {
  openChanges("A", "first", "/shared/a.ts", "edit");
  const first = usePane.getState().active;
  openChanges("A", "first", "/shared/a.ts", "edit");
  expect(visiblePaneTabs("A")).toHaveLength(1);
  openChanges("A", "second", "/shared/a.ts", "edit");
  expect(usePane.getState().active).not.toBe(first);
  openChanges("A", "first", "/shared/a.ts");
  expect(visiblePaneTabs("A")).toHaveLength(3);
  expect(visiblePaneTabs("A")[0]).toMatchObject({ title: "a.ts", runId: "first", toolCallId: "edit" });
  select("B");
  expect(visiblePaneTabs("B")).toEqual([]);
});

it("routes delayed browser, HTML and terminal opens to their source task without stealing focus", async () => {
  const browserResult = Promise.withResolvers<Result<BrowserState>>();
  const fileResult = Promise.withResolvers<Result<FilePreview>>();
  const terminalResult = Promise.withResolvers<Result<{ id: string; shell: string }>>();
  bridge.createBrowser.mockReturnValueOnce(browserResult.promise);
  bridge.readFilePreview.mockReturnValueOnce(fileResult.promise);
  bridge.createTerminal.mockReturnValueOnce(terminalResult.promise);
  const pendingBrowser = openBrowser();
  const pendingFile = openFile("A", "/shared/page.html");
  const pendingTerminal = openTerminal("A");
  await Promise.resolve();
  select("B");
  await openBrowser();
  const bActive = usePane.getState().active;
  browserResult.resolve(ok(browser("delayed-browser")));
  fileResult.resolve(ok({ ...file, path: "/shared/page.html" }));
  terminalResult.resolve(ok({ id: "delayed-terminal", shell: "zsh" }));
  await Promise.all([pendingBrowser, pendingFile, pendingTerminal]);
  expect(visiblePaneTabs("B")).toHaveLength(1);
  expect(usePane.getState().active).toBe(bActive);
  expect(visiblePaneTabs("A")).toHaveLength(3);
  expect(visiblePaneTabs("A").every((tab) => tab.sessionId === "A")).toBe(true);
  select("A");
  expect(usePane.getState().open).toBe(true);
});

it("updates inactive browser state and terminal output without revealing that task", async () => {
  await openBrowser();
  const id = usePane.getState().active ?? "";
  select("B");
  event({ type: "browser", state: browser(id, "https://example.test/next") });
  event({ type: "terminal", id: "terminal-a", data: "still running" });
  expect(usePane.getState()).toMatchObject({ open: false, active: undefined });
  expect(terminalOutput.get("terminal-a")).toBe("still running");
  event({ type: "terminal", id: "terminal-a", data: "exited", exited: true });
  expect(terminalExited.has("terminal-a")).toBe(true);
  expect(terminalOutput.get("terminal-a")).toBe("still runningexited");
  select("A");
  expect(visiblePaneTabs("A")[0]).toMatchObject({ state: { url: "https://example.test/next" } });
});

it.each([file.path, "./readme.md", "/shared/readme-link.md"])(
  "keeps the newer file contents and line target when an earlier read of %s finishes last",
  async (path) => {
    const earlier = Promise.withResolvers<Result<FilePreview>>();
    const later = Promise.withResolvers<Result<FilePreview>>();
    bridge.readFilePreview.mockReturnValueOnce(earlier.promise).mockReturnValueOnce(later.promise);
    const first = openFile("A", path, { line: 10 });
    const second = openFile("A", file.path, { line: 100 });
    const latest = { ...file, text: "new contents", location: { line: 100 } };
    later.resolve(ok(latest));
    await second;
    earlier.resolve(ok({ ...file, text: "old contents", location: { line: 10 } }));
    await first;
    expect(visiblePaneTabs("A")).toEqual([expect.objectContaining({ type: "file", preview: latest })]);
  },
);

it("closing a file invalidates pending refreshes but allows a later explicit reopen", async () => {
  await openFile("A", file.path);
  const id = usePane.getState().active ?? "";
  const earlier = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(earlier.promise);
  const refresh = openFile("A", file.path);
  await closeTab(id);
  earlier.resolve(ok({ ...file, text: "late refresh" }));
  await refresh;
  expect(visiblePaneTabs("A")).toEqual([]);
  expect(usePane.getState()).toMatchObject({ open: false, active: undefined });
  await openFile("A", file.path);
  expect(visiblePaneTabs("A")).toHaveLength(1);
  expect(usePane.getState().open).toBe(true);
});

it("closing a file also invalidates an in-flight read through a symlink alias", async () => {
  await openFile("A", file.path);
  const earlier = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(earlier.promise);
  const opening = openFile("A", "/shared/readme-link.md");
  await closeTab(usePane.getState().active ?? "");
  earlier.resolve(ok(file));
  await opening;
  expect(visiblePaneTabs("A")).toEqual([]);
});

it("ignores a stale file failure and still allows independent file opens", async () => {
  const earlier = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(earlier.promise);
  const opening = openFile("A", file.path);
  await openFile("A", file.path);
  const other = { ...file, path: "/shared/other.md" };
  bridge.readFilePreview.mockResolvedValueOnce(ok(other));
  await openFile("A", other.path);
  earlier.resolve({ ok: false, error: { code: "not_found", message: "old read failed" } });
  await expect(opening).resolves.toBeUndefined();
  expect(visiblePaneTabs("A")).toHaveLength(2);
  expect(visiblePaneTabs("A")[1]).toMatchObject({ preview: other });
});

it("closes and reorders only the intended tabs even when selection changes during native cleanup", async () => {
  await openBrowser();
  const aFirst = usePane.getState().active ?? "";
  select("B");
  await openBrowser();
  const bId = usePane.getState().active ?? "";
  select("A");
  await openBrowser();
  const aLast = usePane.getState().active ?? "";
  reorderPaneTab(aLast, aFirst);
  expect(visiblePaneTabs("A").map((tab) => tab.id)).toEqual([aLast, aFirst]);
  reorderPaneTab(aLast, bId);
  expect(visiblePaneTabs("A").map((tab) => tab.id)).toEqual([aLast, aFirst]);
  const closing = Promise.withResolvers<Result<void>>();
  bridge.closeBrowser.mockReturnValueOnce(closing.promise);
  const pending = closeTab(aLast);
  select("B");
  closing.resolve(ok(undefined));
  await pending;
  expect(usePane.getState()).toMatchObject({ open: true, active: bId });
  select("A");
  expect(usePane.getState().active).toBe(aFirst);
  await closeTab(aFirst);
  expect(usePane.getState()).toMatchObject({ open: false, active: undefined });
  expect(visiblePaneTabs("B")).toHaveLength(1);
});

it("does not remove a tab when native cleanup fails", async () => {
  await openBrowser();
  const id = usePane.getState().active ?? "";
  bridge.closeBrowser.mockResolvedValueOnce({
    ok: false,
    error: { code: "storage", message: "close failed" },
  });
  await expect(closeTab(id)).rejects.toMatchObject({
    cause: { code: "storage", message: "close failed" },
  });
  expect(usePane.getState()).toMatchObject({ open: true, active: id });
  expect(visiblePaneTabs("A")).toHaveLength(1);
});

it("retains ordinary panes when a session is archived or omitted from a refreshed list", async () => {
  await openTerminal("A");
  await openBrowser();
  await openFile("A", file.path);
  const tabs = visiblePaneTabs("A");
  const record = useStore.getState().sessions.get("A");
  if (!record) throw new Error("missing session");
  useStore.setState({
    sessions: new Map(useStore.getState().sessions).set("A", { ...record, archivedAt: 1 }),
  });
  const sessions = new Map(useStore.getState().sessions);
  sessions.delete("A");
  useStore.setState({ sessions });
  select("B");
  expect(visiblePaneTabs("A")).toEqual(tabs);
  expect(bridge.closeTerminal).not.toHaveBeenCalled();
  expect(bridge.closeBrowser).not.toHaveBeenCalled();
  useStore.setState({ sessions: new Map(sessions).set("A", record) });
  select("A");
  expect(visiblePaneTabs("A")).toEqual(tabs);
});

it("retains a child pane's owner across restoration, refresh, navigation and permanent deletion", async () => {
  const parent = useStore.getState().sessions.get("A");
  if (!parent) throw new Error("missing session");
  useStore.setState({
    sessions: new Map(useStore.getState().sessions).set("child-a", {
      ...parent,
      id: "child-a",
      parentSessionId: "A",
    }),
  });
  await openFile("child-a", file.path);
  const fileId = usePane.getState().active;
  const sessions = new Map(useStore.getState().sessions);
  sessions.delete("A");
  sessions.delete("child-a");
  useStore.setState({ sessions });
  select("B");
  // Restoring reloads the top-level task, not its archived child records.
  useStore.setState({ sessions: new Map(sessions).set("A", parent) });
  select("A");
  expect(visiblePaneTabs("A")).toHaveLength(1);
  await openFile("child-a", file.path);
  expect(visiblePaneTabs("A")).toEqual([
    expect.objectContaining({ id: fileId, ownerSessionId: "A", sessionId: "child-a" }),
  ]);
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path: "/shared/linked.md" }));
  await openFile("child-a", "/shared/linked.md");
  await openWebLink("http://localhost:3000/", undefined, "child-a");
  const browserId = usePane.getState().active;
  expect(visiblePaneTabs("A")).toHaveLength(3);
  expect(visiblePaneTabs("child-a")).toEqual([]);
  expect(visiblePaneTabs("A").every((tab) => tab.ownerSessionId === "A")).toBe(true);
  useStore.setState({ sessions });
  select("B");
  await deleteSessionWithPanes("A");
  expect(usePane.getState().tabs).toEqual([]);
  expect(bridge.closeBrowser).toHaveBeenCalledExactlyOnceWith(browserId);
});

it.each(["B", undefined])(
  "uses live child ownership instead of stale retained tabs: %s",
  async (parentId) => {
    const parent = useStore.getState().sessions.get("A");
    if (!parent) throw new Error("missing session");
    const child = { ...parent, id: "child-a", parentSessionId: "A" };
    useStore.setState({ sessions: new Map(useStore.getState().sessions).set(child.id, child) });
    await openFile(child.id, file.path);
    useStore.setState({
      sessions: new Map(useStore.getState().sessions).set(child.id, {
        ...child,
        parentSessionId: parentId,
      }),
    });
    const owner = parentId ?? child.id;
    select(owner);
    await openFile(child.id, file.path);
    await openWebLink("http://localhost:3000/", undefined, child.id);
    expect(visiblePaneTabs(owner)).toHaveLength(2);
    expect(visiblePaneTabs("A")).toEqual([]);
    expect(visiblePaneTabs(owner).every((tab) => tab.ownerSessionId === owner)).toBe(true);
  },
);

it("closes permanently deleted task panes and their retained child panes without touching another task", async () => {
  await openTerminal("A");
  const terminalId = visiblePaneTabs("A")[0].id;
  event({ type: "terminal", id: terminalId, data: "retained output" });
  await openBrowser();
  const browserId = usePane.getState().active;
  await openFile("A", file.path);
  const record = useStore.getState().sessions.get("A");
  if (!record) throw new Error("missing session");
  useStore.setState({
    sessions: new Map(useStore.getState().sessions).set("child-a", {
      ...record,
      id: "child-a",
      parentSessionId: "A",
    }),
  });
  await openBrowser("https://example.test/child", "child-a");
  const childBrowserId = usePane.getState().active;
  const sessions = new Map(useStore.getState().sessions);
  sessions.delete("A");
  sessions.delete("child-a");
  useStore.setState({ sessions });
  select("B");
  await openBrowser();
  const otherTabs = visiblePaneTabs("B");
  await deleteSessionWithPanes("A");
  expect(bridge.deleteSession).toHaveBeenCalledWith("A");
  expect(bridge.closeTerminal).toHaveBeenCalledExactlyOnceWith(terminalId);
  expect(bridge.closeBrowser).toHaveBeenCalledTimes(2);
  expect(bridge.closeBrowser).toHaveBeenCalledWith(browserId);
  expect(bridge.closeBrowser).toHaveBeenCalledWith(childBrowserId);
  expect(visiblePaneTabs("A")).toEqual([]);
  expect(usePane.getState().tabs).toEqual(otherTabs);
  expect(terminalOutput.has(terminalId)).toBe(false);
  expect(usePane.getState().preferences.has("A")).toBe(false);
  expect(usePane.getState().active).toBe(otherTabs[0].id);
});

it("does not close task panes when permanent deletion fails", async () => {
  await openTerminal("A");
  await openBrowser();
  const tabs = visiblePaneTabs("A");
  bridge.deleteSession.mockResolvedValueOnce({
    ok: false,
    error: { code: "storage", message: "delete failed" },
  });
  await expect(deleteSessionWithPanes("A")).rejects.toMatchObject({
    cause: { code: "storage", message: "delete failed" },
  });
  expect(visiblePaneTabs("A")).toEqual(tabs);
  expect(usePane.getState().deletedSessionIds.has("A")).toBe(false);
  expect(bridge.closeTerminal).not.toHaveBeenCalled();
  expect(bridge.closeBrowser).not.toHaveBeenCalled();
});

it("cleans side-chat and terminal panes when the deleted child already returns not_found", async () => {
  await openTerminal("A");
  usePane.setState({
    tabs: [
      ...usePane.getState().tabs,
      {
        type: "side-chat",
        id: "side-chat:deleted-child",
        sessionId: "A",
        childSessionId: "deleted-child",
        title: "Side chat",
        ordinal: 1,
      },
    ],
  });
  bridge.closeSideChat.mockResolvedValueOnce({
    ok: false,
    error: { code: "not_found", message: "child already deleted" },
  });
  await deleteSessionWithPanes("A");
  expect(bridge.closeSideChat).toHaveBeenCalledExactlyOnceWith("deleted-child");
  expect(bridge.closeTerminal).toHaveBeenCalledTimes(1);
  expect(visiblePaneTabs("A")).toEqual([]);
  expect(report).not.toHaveBeenCalled();
});

it("reports cleanup failures separately after successful deletion and still closes other panes", async () => {
  await openTerminal("A");
  await openBrowser();
  bridge.closeBrowser.mockResolvedValueOnce({
    ok: false,
    error: { code: "storage", message: "native close failed" },
  });
  await expect(deleteSessionWithPanes("A")).resolves.toBeUndefined();
  expect(bridge.closeTerminal).toHaveBeenCalledTimes(1);
  expect(report).toHaveBeenCalledWith(
    expect.objectContaining({ message: expect.stringContaining("任务已删除") }),
  );
});

it("disposes native panes whose creation finishes after their task is deleted", async () => {
  const browserResult = Promise.withResolvers<Result<BrowserState>>();
  const terminalResult = Promise.withResolvers<Result<{ id: string; shell: string }>>();
  const fileResult = Promise.withResolvers<Result<FilePreview>>();
  bridge.createBrowser.mockReturnValueOnce(browserResult.promise);
  bridge.createTerminal.mockReturnValueOnce(terminalResult.promise);
  bridge.readFilePreview.mockReturnValueOnce(fileResult.promise);
  const pendingBrowser = openBrowser();
  const pendingTerminal = openTerminal("A");
  const pendingFile = openFile("A", "/shared/page.html");
  await Promise.resolve();
  expect(bridge.createTerminal).toHaveBeenCalledWith("A");
  await deleteSessionWithPanes("A");
  browserResult.resolve(ok(browser("late-browser")));
  terminalResult.resolve(ok({ id: "late-terminal", shell: "zsh" }));
  fileResult.resolve(ok({ ...file, path: "/shared/page.html" }));
  await Promise.all([pendingBrowser, pendingTerminal, pendingFile]);
  expect(bridge.closeBrowser).toHaveBeenCalledExactlyOnceWith("late-browser");
  expect(bridge.closeTerminal).toHaveBeenCalledExactlyOnceWith("late-terminal");
  expect(bridge.createBrowser).toHaveBeenCalledTimes(1);
  expect(visiblePaneTabs("A")).toEqual([]);
});

it("opens streamed file operations from their parameters without reading an unfinished file", async () => {
  useStore.setState({
    views: new Map([
      [
        "A",
        {
          sessionId: "A",
          title: "A",
          seq: 1,
          runs: [
            {
              runId: "writing",
              status: "running",
              userMessage: "write",
              startedAt: 0,
              finalAnswerBlockIds: [],
              orderedBlocks: [
                {
                  type: "tool",
                  id: "write",
                  messageId: "assistant",
                  toolCallId: "write",
                  name: "write",
                  status: "preparing",
                  argsText: '{"path":"page.html","content":"partial',
                  output: "",
                },
              ],
            },
          ],
        },
      ],
    ]),
  });
  await openFile("A", "/shared/page.html");
  expect(bridge.readFilePreview).not.toHaveBeenCalled();
  expect(bridge.createBrowser).not.toHaveBeenCalled();
  expect(visiblePaneTabs("A")).toEqual([
    expect.objectContaining({
      type: "changes",
      runId: "writing",
      toolCallId: "write",
      path: "page.html",
    }),
  ]);
  await openFile("B", "/shared/page.html");
  expect(bridge.readFilePreview).toHaveBeenCalledWith("B", "/shared/page.html", undefined);
});

it("still reports missing files when there is no pending matching write", async () => {
  bridge.readFilePreview.mockResolvedValueOnce({
    ok: false,
    error: {
      code: "not_found",
      message: "文件不存在",
    },
  });
  await expect(openFile("A", "/shared/missing.html")).rejects.toThrow("文件不存在");
  expect(visiblePaneTabs("A")).toHaveLength(0);
});

it.each(["pdf", "pptx", "docx", "doc", "xlsx", "html", "svg"])(
  "opens %s in the file pane without spawning a browser or external app",
  async (extension) => {
    const kind = extension === "html" || extension === "svg" ? "text" : extension;
    const path = `/shared/document.${extension}`;
    const preview =
      kind === "text" ? { ...file, path } : ({ kind, path, bytes: new Uint8Array([1, 2]) } as FilePreview);
    bridge.readFilePreview.mockResolvedValueOnce(ok(preview));
    await openFile("A", path);
    expect(visiblePaneTabs("A")).toEqual([
      expect.objectContaining({ type: "file", sessionId: "A", preview }),
    ]);
    expect(bridge.createBrowser).not.toHaveBeenCalled();
  },
);

it("names terminals by workspace with per-owner ordinals rather than shell executable", async () => {
  await openTerminal("A");
  await openTerminal("A");
  await openTerminal("B");
  expect(usePane.getState().tabs.map((tab) => tab.title)).toEqual(["shared", "shared 2", "shared"]);
  const first = usePane.getState().tabs[0];
  if (!first) throw new Error("missing terminal tab");
  await closeTab(first.id);
  await openTerminal("A");
  expect(usePane.getState().tabs.at(-1)?.title).toBe("shared");
});

it("keeps native profile metadata on its terminal tab without altering the shell process title", async () => {
  const profile = { fontFamily: "Menlo", fontSize: 15, theme: { green: "#12ab34" } };
  bridge.createTerminal.mockResolvedValueOnce(ok({ id: "native-profile", shell: "zsh", profile }));
  await openTerminal("A");
  expect(usePane.getState().tabs[0]).toMatchObject({
    id: "native-profile",
    title: "shared",
    cwd: "/shared",
    profile,
  });
  expect(bridge.createTerminal).toHaveBeenCalledOnce();
  expect(bridge.closeTerminal).not.toHaveBeenCalled();
});

it.each(["html", "HTM"])(
  "opens explicit Markdown %s links in the browser, after validating the file",
  async (extension) => {
    const path = `/shared/page.${extension}`;
    const location = { fileUrl: `file://${path}?preview=1#intro` };
    bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path, location }));
    await openFile("A", path, location, "markdown-link");
    expect(bridge.readFilePreview).toHaveBeenCalledWith("A", path, location);
    expect(bridge.createBrowser).toHaveBeenCalledWith(`file://${path}?preview=1#intro`);
    expect(visiblePaneTabs("A")[0]).toMatchObject({ type: "browser", sessionId: "A", ownerSessionId: "A" });
  },
);

it.each(["/shared/page.html", "/shared/report.pdf"])(
  "keeps source line links and PDF Markdown links in the file pane: %s",
  async (path) => {
    const location = path.endsWith("html") ? { line: 2 } : undefined;
    bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path, location }));
    await openFile("A", path, location, "markdown-link");
    expect(visiblePaneTabs("A")[0]).toMatchObject({ type: "file" });
    expect(bridge.createBrowser).not.toHaveBeenCalled();
  },
);

it("deduplicates repeated HTML Markdown opens and does not resurrect a closed browser after a delayed read", async () => {
  const path = "/shared/page.html";
  const url = "file:///shared/page.html";
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path }));
  bridge.createBrowser.mockResolvedValueOnce(ok(browser("html-browser", url)));
  await openFile("A", path, undefined, "markdown-link");
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path }));
  await openFile("A", path, undefined, "markdown-link");
  expect(bridge.createBrowser).toHaveBeenCalledOnce();
  const delayed = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(delayed.promise);
  const pending = openFile("A", path, undefined, "markdown-link");
  await closeTab("html-browser");
  delayed.resolve(ok({ ...file, path }));
  await pending;
  expect(visiblePaneTabs("A")).toEqual([]);
  expect(bridge.createBrowser).toHaveBeenCalledOnce();
});

it("discards an older native HTML browser creation after a newer source open", async () => {
  const path = "/shared/page.html";
  const delayed = Promise.withResolvers<Result<BrowserState>>();
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path }));
  bridge.createBrowser.mockReturnValueOnce(delayed.promise);
  const pending = openFile("A", path, undefined, "markdown-link");
  await vi.waitFor(() => expect(bridge.createBrowser).toHaveBeenCalledOnce());
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path }));
  await openFile("A", path);
  delayed.resolve(ok(browser("stale-html", "file:///shared/page.html")));
  await pending;
  expect(bridge.closeBrowser).toHaveBeenCalledWith("stale-html");
  expect(visiblePaneTabs("A")).toHaveLength(1);
  expect(visiblePaneTabs("A")[0].type).toBe("file");
});

it("keeps an auxiliary chat HTML browser owned by its parent when selection changes", async () => {
  useStore.setState({
    sessions: new Map([
      ...useStore.getState().sessions,
      [
        "side",
        {
          id: "side",
          title: "side",
          cwd: "/shared",
          parentSessionId: "A",
          projectId: null,
          createdAt: 0,
          updatedAt: 0,
        },
      ],
    ]),
  });
  const delayed = Promise.withResolvers<Result<BrowserState>>();
  bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path: "/shared/page.html" }));
  bridge.createBrowser.mockReturnValueOnce(delayed.promise);
  const pending = openFile("side", "/shared/page.html", undefined, "markdown-link");
  await vi.waitFor(() => expect(bridge.createBrowser).toHaveBeenCalledOnce());
  select("B");
  delayed.resolve(ok(browser("side-html", "file:///shared/page.html")));
  await pending;
  expect(visiblePaneTabs("A")[0]).toMatchObject({ type: "browser", sessionId: "side", ownerSessionId: "A" });
  expect(visiblePaneTabs("B")).toEqual([]);
  expect(usePane.getState().open).toBe(false);
});

it("does not create an HTML browser when validation fails", async () => {
  bridge.readFilePreview.mockResolvedValueOnce({
    ok: false,
    error: { code: "not_found", message: "missing" },
  });
  await expect(openFile("A", "/shared/page.html", undefined, "markdown-link")).rejects.toMatchObject({
    cause: { code: "not_found", message: "missing" },
  });
  expect(bridge.createBrowser).not.toHaveBeenCalled();
});

it.each([undefined, "file:A:/shared/old.html"])(
  "binds the current file key when reusing a browser with previous key %s",
  async (fileKey) => {
    const path = "/shared/page.html",
      url = "file:///shared/page.html";
    usePane.setState({
      tabs: [
        {
          type: "browser",
          id: "existing",
          title: "browser",
          sessionId: "A",
          ownerSessionId: "A",
          state: browser("existing", url),
          fileKey,
        },
      ],
    });
    bridge.readFilePreview.mockResolvedValueOnce(ok({ ...file, path }));
    await openFile("A", path, undefined, "markdown-link");
    expect(bridge.createBrowser).not.toHaveBeenCalled();
    expect(visiblePaneTabs("A")[0]).toMatchObject({ fileKey: "file:A:/shared/page.html" });
    const delayed = Promise.withResolvers<Result<FilePreview>>();
    bridge.readFilePreview.mockReturnValueOnce(delayed.promise);
    const pending = openFile("A", path, undefined, "markdown-link");
    await closeTab("existing");
    delayed.resolve(ok({ ...file, path }));
    await pending;
    expect(visiblePaneTabs("A")).toEqual([]);
    expect(bridge.createBrowser).not.toHaveBeenCalled();
  },
);

it("replaces only the active launcher in place and gives each file manager its own navigation", async () => {
  openNewPaneTab();
  await openFileManager("A");
  const first = visiblePaneTabs("A")[0];
  expect(first.type).toBe("file-manager");
  expect(visiblePaneTabs("A")).toHaveLength(1);
  openNewPaneTab();
  await openFileManager("A");
  const second = visiblePaneTabs("A")[1];
  expect(second.id).not.toBe(first.id);
  const active = usePane.getState().active;
  await navigateFileManager(first.id, file.path);
  expect(visiblePaneTabs("A")[0]).toMatchObject({ preview: file, title: "readme.md" });
  expect(visiblePaneTabs("A")[1]).not.toHaveProperty("preview");
  expect(usePane.getState().active).toBe(active);
});

it.each(["file", "changes"] as const)(
  "navigates a %s tab in place and preserves it when reopening its original source",
  async (kind) => {
    if (kind === "file") await openFile("A", file.path);
    else openChanges("A", "run-a", file.path);
    const original = visiblePaneTabs("A")[0];
    const nextFile = { ...file, path: "/shared/next.md", text: "next" };
    bridge.readFilePreview.mockResolvedValueOnce(ok(nextFile));
    select("B");
    openNewPaneTab();
    const active = usePane.getState().active;
    await navigateFileManager(original.id, nextFile.path);
    expect(visiblePaneTabs("A")[0]).toMatchObject({
      id: original.id,
      type: "file",
      title: "next.md",
      preview: nextFile,
      ownerSessionId: "A",
      cwd: "/shared",
    });
    expect(usePane.getState().active).toBe(active);
    if (kind === "file") await openFile("A", file.path);
    else openChanges("A", "run-a", file.path);
    expect(visiblePaneTabs("A")).toHaveLength(2);
    expect(visiblePaneTabs("A")[0]).toMatchObject({ id: original.id, preview: nextFile });
    expect(visiblePaneTabs("A")[1].id).not.toBe(original.id);
    if (kind === "file") await openFile("A", file.path);
    else openChanges("A", "run-a", file.path);
    expect(visiblePaneTabs("A")).toHaveLength(2);
    bridge.readFilePreview.mockResolvedValueOnce(ok(nextFile));
    await openFile("A", nextFile.path);
    expect(visiblePaneTabs("A")).toHaveLength(2);
    expect(visiblePaneTabs("A")[0]).toMatchObject({ id: original.id, preview: nextFile });
  },
);

it("does not let an earlier file refresh overwrite directory navigation or revive a closed navigated tab", async () => {
  await openFile("A", file.path);
  const id = usePane.getState().active ?? "";
  const delayed = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(delayed.promise);
  const refresh = openFile("A", file.path);
  const nextFile = { ...file, path: "/shared/next.md" };
  bridge.readFilePreview.mockResolvedValueOnce(ok(nextFile));
  await navigateFileManager(id, nextFile.path);
  delayed.resolve(ok(file));
  await refresh;
  expect(visiblePaneTabs("A")).toMatchObject([{ id, preview: nextFile }]);
  const closedRead = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(closedRead.promise);
  const pending = openFile("A", nextFile.path);
  await closeTab(id);
  closedRead.resolve(ok(nextFile));
  await pending;
  expect(visiblePaneTabs("A")).toEqual([]);
});

it.each(["file", "changes"] as const)(
  "reopening the current %s source cancels an older directory read",
  async (kind) => {
    if (kind === "file") await openFile("A", file.path);
    else openChanges("A", "run-a", file.path);
    const id = usePane.getState().active ?? "";
    const delayed = Promise.withResolvers<Result<FilePreview>>();
    bridge.readFilePreview.mockReturnValueOnce(delayed.promise);
    const navigating = navigateFileManager(id, "/shared/next.md");
    if (kind === "file") await openFile("A", file.path);
    else openChanges("A", "run-a", file.path);
    delayed.resolve(ok({ ...file, path: "/shared/next.md" }));
    await navigating;
    expect(visiblePaneTabs("A")).toHaveLength(1);
    expect(visiblePaneTabs("A")[0]).toMatchObject({ id, type: kind, title: "readme.md" });
  },
);

it("ignores old file-manager reads and failures and does not reopen a closed tab", async () => {
  await openFileManager("A");
  const id = usePane.getState().active ?? "";
  const older = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(older.promise);
  const opening = navigateFileManager(id, "/shared/old.ts");
  await navigateFileManager(id, file.path);
  older.resolve({ ok: false, error: { code: "not_found", message: "stale" } });
  await expect(opening).resolves.toBeUndefined();
  expect(visiblePaneTabs("A")[0]).toMatchObject({ preview: file });
  const pending = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(pending.promise);
  const refresh = navigateFileManager(id, file.path);
  await closeTab(id);
  pending.resolve(ok(file));
  await refresh;
  expect(visiblePaneTabs("A")).toHaveLength(0);
});

it("a file-manager response for a background task cannot activate another task's pane", async () => {
  await openFileManager("A");
  const id = usePane.getState().active ?? "";
  const pending = Promise.withResolvers<Result<FilePreview>>();
  bridge.readFilePreview.mockReturnValueOnce(pending.promise);
  const opening = navigateFileManager(id, file.path);
  select("B");
  openNewPaneTab();
  const active = usePane.getState().active;
  pending.resolve(ok(file));
  await opening;
  expect(usePane.getState().active).toBe(active);
  expect(visiblePaneTabs("A")[0]).toMatchObject({ preview: file });
});

it("restores review source from recent closures, keeps owner boundaries and closes only the current owner's tabs", async () => {
  openReview("A");
  const review = usePane.getState().tabs[0];
  selectReviewSource(review.id, "staged");
  await openFileManager("A");
  await closeTab(review.id);
  expect(usePane.getState().recentClosed[0].tab).toMatchObject({ reviewSource: "staged" });
  select("B");
  await reopenPaneTab(review.id);
  expect(visiblePaneTabs("B")).toHaveLength(0);
  openReview("B");
  select("A");
  await reopenPaneTab(review.id);
  expect(usePane.getState().active).toBe(review.id);
  expect(usePane.getState().tabs.find((tab) => tab.id === review.id)).toMatchObject({
    reviewSource: "staged",
  });
  expect(usePane.getState().recentClosed).toHaveLength(0);
  await closePaneTabs(review.id);
  expect(visiblePaneTabs("A").map((tab) => tab.id)).toEqual([review.id]);
  expect(visiblePaneTabs("B")).toHaveLength(1);
  await closePaneTabs();
  expect(usePane.getState().open).toBe(false);
  expect(visiblePaneTabs("B")).toHaveLength(1);
});
it("full view belongs to its owner, restores its surface and floating mode, and clears when the pane closes", () => {
  openReview("A");
  const review = usePane.getState().active;
  const ratio = usePane.getState().ratio;
  togglePaneFullView();
  expect(usePane.getState().fullView).toEqual({ surface: "side-pane", floating: "collapsed" });
  updatePaneFullView({ surface: "conversation", floating: "expanded" });
  select("B");
  expect(usePane.getState().fullView).toBeNull();
  openReview("B");
  select("A");
  expect(usePane.getState().fullView).toEqual({ surface: "conversation", floating: "expanded" });
  if (!review) throw Error("Missing review");
  activatePaneTab(review);
  expect(usePane.getState().fullView).toEqual({ surface: "side-pane", floating: "expanded" });
  setPaneOpen(false);
  setPaneOpen(true);
  expect(usePane.getState().fullView).toBeNull();
  expect(usePane.getState().ratio).toBe(ratio);
});