Files
PaperClipAI/ui/src/components/WorkspaceFileBrowser.test.tsx
T
DottaandPaperclip 8087661bb8 fix: bound workspace Git scans (#11572)
## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Workspaces let users and agents inspect files that belong to an
issue
> - Changed-file views use full-tree Git status scans
> - Many issue views could start those scans at the same time and make
the server unresponsive
> - Route-level limits did not protect the process or coalesce work for
one repository
> - This pull request adds one bounded scheduler for every expensive
workspace Git scan
> - It also starts browser scans only when the file panel is open and
visible
> - The benefit is bounded child-process use and responsive health
checks during request storms

## Linked Issues or Issue Description

**What happened?**

Many changed-file requests could start full `git status --porcelain=v1
-z --untracked-files=all` scans at the same time. One production
incident produced about 270 direct Git child processes. The Node process
stayed alive but stopped answering health requests in time.

**Expected behavior**

Paperclip must bound expensive Git work across all companies, actors,
issues, repositories, and browser tabs. Duplicate requests for one
worktree must share work. Excess requests must fail fast with a
retryable response. Hidden or closed file panels must not start scans.

**Steps to reproduce**

1. Open changed-file views for many issue and actor keys.
2. Send requests for two large workspace roots at the same time.
3. Observe that route-level limiter keys allow many full Git scans to
run together.
4. Observe delayed health responses and accumulated Git children.

**Paperclip version or commit**

Reproduced on master before commit `43ab441f0f`.

**Deployment mode**

Self-hosted server with local workspace repositories.

## What Changed

- Add a process-wide scheduler with configurable concurrency, queue
capacity, timeout, and cache TTL.
- Add fair admission, a bounded queue, canonical worktree keys,
single-flight joins, and bounded result caching.
- Add subprocess timeouts, TERM-to-KILL escalation, bounded output,
waiter cancellation, and slot cleanup.
- Route full-tree status work from file resources, workspace runtime,
execution workspaces, and adapter overlay sync through the scheduler.
- Return stable retryable `503` and `504` error codes for saturation and
timeout.
- Add structured logs with safe workspace hashes, durations, queue
state, cache use, joins, and terminal outcomes.
- Gate UI queries on panel and document visibility. Cancel queries on
close, hide, unmount, and workspace change.
- Disable focus and reconnect bursts. Keep one explicit refresh action
and a retryable unavailable state.
- Document the 10-second default freshness tradeoff and all
configuration variables.
- Add unit, route, UI, adapter, and deterministic 500-request load
coverage.

## Verification

- `pnpm -r typecheck`
- `pnpm build`
- `pnpm check:token-gates`
- `pnpm --filter @paperclipai/server exec vitest run
src/services/workspace-git-operation-scheduler.test.ts
src/__tests__/file-resources-git-scan-load.test.ts --reporter=dot` — 16
tests passed.
- `pnpm --filter @paperclipai/ui exec vitest run
src/components/WorkspaceFileBrowser.test.tsx
src/lib/page-visibility.test.ts --reporter=dot` — 38 tests passed.
- `pnpm --filter @paperclipai/adapter-utils exec vitest run
src/git-workspace-sync.test.ts --reporter=dot` — 16 tests passed.
- Existing file-resource, workspace-runtime, and execution-workspace
regression selections passed.
- Two cleanup safety regressions prove failed scans preserve the
worktree before archive and at the final deletion fence.
- Before: the incident produced about 270 Git children and health
requests timed out.
- After: 500 concurrent requests across 500 issue keys, 73 actors, and
two roots started two underlying scans. Peak scan concurrency was 2. All
500 requests succeeded. Health p99 was 4.94 ms. The harness found zero
unreaped children.
- The full local Vitest run passed 4,267 tests. Ten existing fixed-port
HTTPS exposure tests could not run because this host already owns
Tailnet listeners on ports 42000 and 52000. Clean GitHub CI is the final
full-suite result.
- Latest-head GitHub CI passed all required test, typecheck, build,
canary, e2e, policy, and security gates.
- Greptile completed at 5/5 with zero unresolved comments,
recommendations, or follow-ups.

## Risks

- Changed-file results can be up to 10 seconds old by default. Explicit
refresh remains available.
- A full queue returns a retryable `503` instead of waiting without a
bound.
- A scan that exceeds the default 8-second deadline returns a retryable
`504` and terminates its process group.
- Operators can tune all limits with documented environment variables.
Safe defaults protect local and shared servers.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

- OpenAI Codex, GPT-5 family. The runtime does not expose the exact
deployment ID or context-window size. High reasoning, tool use, and code
execution were enabled.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
2026-08-17 22:11:30 -05:00

943 lines
34 KiB
TypeScript

// @vitest-environment jsdom
import type { ComponentProps } from "react";
import { flushSync } from "react-dom";
import { createRoot } from "react-dom/client";
import type { Root } from "react-dom/client";
import type { Project, ProjectWorkspace, WorkspaceFileListDirectoryItem, WorkspaceFileListFileItem, WorkspaceFileListItem, WorkspaceFileListResponse } from "@paperclipai/shared";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import { WorkspaceFileBrowser, describeUnavailable } from "./WorkspaceFileBrowser";
import { ApiError } from "@/api/client";
function act(callback: () => void | Promise<void>) {
let result: void | Promise<void> | undefined;
flushSync(() => {
result = callback();
});
return result;
}
/**
* Waits on the condition, not on a fixed number of turns. A hand-rolled retry
* loop is ample on an idle machine and not when the suite runs many workers in
* parallel: it gives up after N turns and reports a failure on behaviour that
* works. `vi.waitFor` retries against a time budget, so a loaded worker gets
* more turns instead.
*
* Same replacement as #11499 and #11521, which fixed the shorter-budget
* instances of this in the routing tests.
*
* This was a reimplementation of `vi.waitFor` down to rethrowing the last
* error, differing only in bounding on turns rather than on time.
*/
async function waitForExpectation(assertion: () => void) {
await vi.waitFor(assertion);
}
const useQueryMock = vi.fn();
const cancelQueriesMock = vi.fn(() => Promise.resolve());
const LIST_LIMIT = 100;
vi.mock("@tanstack/react-query", async () => {
const actual = await vi.importActual<typeof import("@tanstack/react-query")>("@tanstack/react-query");
return {
...actual,
useQuery: (options: unknown) => useQueryMock(options),
useQueries: ({ queries }: { queries: unknown[] }) => queries.map((options) => useQueryMock(options)),
useQueryClient: () => ({ cancelQueries: cancelQueriesMock }),
};
});
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(globalThis as any).IS_REACT_ACT_ENVIRONMENT = true;
function createItem(overrides: Partial<WorkspaceFileListFileItem> = {}): WorkspaceFileListFileItem {
return {
kind: "file",
provider: "git_worktree",
title: "IssueDetail.tsx",
relativePath: "ui/src/pages/IssueDetail.tsx",
displayPath: "ui/src/pages/IssueDetail.tsx",
workspaceLabel: "Isolated workspace",
workspaceKind: "execution_workspace",
workspaceId: "ws-1",
contentType: "text/plain; charset=utf-8",
byteSize: 2048,
modifiedAt: new Date(Date.now() - 120_000).toISOString(),
previewKind: "text",
capabilities: { preview: true, download: true, listChildren: false },
...overrides,
};
}
function createDirectoryItem(overrides: Partial<WorkspaceFileListDirectoryItem> = {}): WorkspaceFileListDirectoryItem {
return {
kind: "directory",
provider: "git_worktree",
title: "src",
relativePath: "ui/src",
displayPath: "ui/src/",
workspaceLabel: "Isolated workspace",
workspaceKind: "execution_workspace",
workspaceId: "ws-1",
contentType: null,
byteSize: null,
modifiedAt: null,
previewKind: "unsupported",
capabilities: { preview: false, download: false, listChildren: true },
...overrides,
};
}
function availableResponse(items: WorkspaceFileListItem[], truncated = false): WorkspaceFileListResponse {
return {
kind: "workspace_file_list",
state: "available",
workspace: {
provider: "git_worktree",
workspaceLabel: "Isolated workspace",
workspaceKind: "execution_workspace",
workspaceId: "ws-1",
},
query: { workspace: "auto", mode: "changed", q: null, limit: 100, offset: 0 },
items,
scannedCount: items.length,
truncated,
};
}
function availableAllResponse(
items: WorkspaceFileListItem[],
path: string | null,
truncated = false,
offset = 0,
): WorkspaceFileListResponse {
const response = availableResponse(items, truncated);
response.query = { ...response.query, mode: "all", path, offset };
return response;
}
function createWorkspace(overrides: Partial<ProjectWorkspace> = {}): ProjectWorkspace {
return {
id: "workspace-content",
companyId: "company-1",
projectId: "project-content",
name: "Paperclip Content",
sourceType: "local_path",
cwd: "/srv/paperclip/home/paperclipai/paperclip-content",
repoUrl: null,
repoRef: null,
defaultRef: null,
visibility: "default",
setupCommand: null,
cleanupCommand: null,
remoteProvider: null,
remoteWorkspaceRef: null,
sharedWorkspaceKey: null,
metadata: null,
runtimeConfig: null,
isPrimary: true,
createdAt: new Date("2026-06-08T00:00:00.000Z"),
updatedAt: new Date("2026-06-08T00:00:00.000Z"),
...overrides,
};
}
function createProject(overrides: Partial<Project> = {}): Project {
const workspace = createWorkspace();
return {
id: "project-content",
companyId: "company-1",
urlKey: "paperclip-content",
goalId: null,
goalIds: [],
goals: [],
name: "Paperclip Content",
description: null,
status: "in_progress",
leadAgentId: null,
targetDate: null,
color: null,
icon: null,
env: null,
pauseReason: null,
pausedAt: null,
executionWorkspacePolicy: null,
codebase: {
workspaceId: workspace.id,
repoUrl: null,
repoRef: null,
defaultRef: null,
repoName: null,
localFolder: workspace.cwd,
managedFolder: "",
effectiveLocalFolder: workspace.cwd ?? "",
origin: "local_folder",
},
workspaces: [workspace],
primaryWorkspace: workspace,
archivedAt: null,
createdAt: new Date("2026-06-08T00:00:00.000Z"),
updatedAt: new Date("2026-06-08T00:00:00.000Z"),
...overrides,
};
}
function unavailableResponse(reason: string): WorkspaceFileListResponse {
return {
kind: "workspace_file_list",
state: "unavailable",
unavailableReason: reason,
workspace: null,
query: { workspace: "auto", mode: "changed", q: null, limit: 100, offset: 0 },
items: [],
scannedCount: 0,
truncated: false,
};
}
function ok<T>(data: T) {
return { data, isFetching: false, isError: false, error: null, refetch: vi.fn() };
}
describe("WorkspaceFileBrowser", () => {
let container: HTMLDivElement;
const roots: Root[] = [];
beforeEach(() => {
container = document.createElement("div");
document.body.appendChild(container);
useQueryMock.mockReset();
cancelQueriesMock.mockClear();
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "visible",
});
Object.defineProperty(Element.prototype, "scrollIntoView", {
configurable: true,
value: vi.fn(),
});
});
afterEach(() => {
for (const root of roots.splice(0)) {
act(() => {
root.unmount();
});
}
container.remove();
vi.restoreAllMocks();
});
function renderBrowser(onOpen = vi.fn(), props: Partial<ComponentProps<typeof WorkspaceFileBrowser>> = {}) {
const root = createRoot(container);
roots.push(root);
act(() => {
root.render(<WorkspaceFileBrowser issueId="issue-1" onOpen={onOpen} {...props} />);
});
return { root, onOpen };
}
function changedListQueryOptions() {
return useQueryMock.mock.calls
.map(([options]) => options as {
queryKey?: readonly unknown[];
enabled?: boolean;
refetchOnWindowFocus?: boolean;
refetchOnReconnect?: boolean;
})
.filter((options) => options.queryKey?.[0] === "issues" && options.queryKey?.[3] === "list")
.at(-1);
}
it("does not enable changed-file enumeration before the Files panel opens", () => {
useQueryMock.mockReturnValue(ok(availableResponse([])));
renderBrowser(vi.fn(), { active: false });
expect(changedListQueryOptions()).toMatchObject({
enabled: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
expect(cancelQueriesMock).toHaveBeenCalledWith({
queryKey: ["issues", "file-resources", "issue-1", "list"],
});
});
it("disables and cancels changed-file enumeration while the page is hidden", () => {
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});
useQueryMock.mockReturnValue(ok(availableResponse([])));
renderBrowser();
expect(changedListQueryOptions()?.enabled).toBe(false);
expect(cancelQueriesMock).toHaveBeenCalledTimes(1);
});
it("cancels an active changed-file query when the page becomes hidden", () => {
useQueryMock.mockReturnValue(ok(availableResponse([])));
renderBrowser();
expect(changedListQueryOptions()?.enabled).toBe(true);
Object.defineProperty(document, "visibilityState", {
configurable: true,
value: "hidden",
});
act(() => {
document.dispatchEvent(new Event("visibilitychange"));
});
expect(changedListQueryOptions()?.enabled).toBe(false);
expect(cancelQueriesMock).toHaveBeenCalledWith({
queryKey: ["issues", "file-resources", "issue-1", "list"],
});
});
it("runs one explicit refresh without enabling focus or reconnect bursts", () => {
const refetch = vi.fn();
useQueryMock.mockReturnValue({
...ok(availableResponse([createItem()])),
refetch,
});
renderBrowser();
const refresh = container.querySelector<HTMLButtonElement>('button[aria-label="Refresh workspace files"]');
expect(refresh).not.toBeNull();
act(() => refresh!.click());
expect(refetch).toHaveBeenCalledTimes(1);
expect(changedListQueryOptions()).toMatchObject({
enabled: true,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
});
});
it.each([
[503, "workspace_git_scan_saturated"],
[504, "workspace_git_scan_timeout"],
] as const)("renders %s changed-file failures as a retryable state", (status, code) => {
const refetch = vi.fn();
useQueryMock.mockReturnValue({
data: undefined,
isFetching: false,
isError: true,
error: new ApiError("temporarily unavailable", status, { code }),
refetch,
});
renderBrowser();
expect(container.textContent).toContain("Changed files temporarily unavailable");
const retry = Array.from(container.querySelectorAll("button")).find((button) => button.textContent?.includes("Retry"));
expect(retry).not.toBeUndefined();
act(() => retry!.click());
expect(refetch).toHaveBeenCalledTimes(1);
});
it("renders the Recently changed files as a tree and opens a row with its relative path", () => {
useQueryMock.mockReturnValue(
ok(availableResponse([createItem(), createItem({ relativePath: "README.md", displayPath: "README.md" })])),
);
const { onOpen } = renderBrowser();
expect(container.querySelector('[role="tree"]')).not.toBeNull();
expect(container.textContent).toContain("Isolated workspace");
expect(container.textContent).not.toContain("Recently changed");
expect(container.textContent).not.toContain("From Isolated workspace");
const option = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === "ui/src/pages/IssueDetail.tsx",
);
expect(option).not.toBeUndefined();
const download = option!.querySelector<HTMLAnchorElement>('a[aria-label="Download IssueDetail.tsx"]');
expect(download?.getAttribute("href")).toBe(
"/api/issues/issue-1/file-resources/content?path=ui%2Fsrc%2Fpages%2FIssueDetail.tsx&download=1",
);
act(() => {
option!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(onOpen).toHaveBeenCalledWith({
path: "ui/src/pages/IssueDetail.tsx",
workspace: "auto",
browseFolderPath: null,
browseQuery: null,
});
});
it("groups nested paths into collapsible folder rows instead of repeating full paths", () => {
useQueryMock.mockReturnValue(
ok(availableResponse([
createItem({
title: "tweet.md",
relativePath: "videos/90-days-paperclip/tweet.md",
displayPath: "videos/90-days-paperclip/tweet.md",
}),
createItem({
title: "90-days-paperclip-1x1.mp4",
relativePath: "videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4",
displayPath: "videos/90-days-paperclip/out/90-days-paperclip-1x1.mp4",
previewKind: "video",
}),
])),
);
renderBrowser();
expect(container.querySelector('[role="tree"]')).not.toBeNull();
expect(container.textContent).toContain("videos");
expect(container.textContent).toContain("90-days-paperclip");
expect(container.textContent).toContain("tweet.md");
expect(container.textContent).toContain("90-days-paperclip-1x1.mp4");
expect(container.textContent).not.toContain("videos/90-days-paperclip/tweet.md");
const videosFolder = Array.from(container.querySelectorAll('button[role="treeitem"]')).find(
(el) => el.getAttribute("title") === "videos",
);
expect(videosFolder).not.toBeUndefined();
act(() => {
videosFolder!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(container.textContent).not.toContain("tweet.md");
});
it("can render without autofocus when embedded beside a preview", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
renderBrowser(vi.fn(), { autoFocusSearch: false });
expect(container.querySelector("input")?.hasAttribute("autofocus")).toBe(false);
expect(container.querySelector("input")?.className).toContain("max-w-full");
});
it("hides source controls, folder headings, workspace labels, and timestamps", () => {
useQueryMock.mockReturnValue(ok(availableResponse([
createItem({
relativePath: "videos/90-days-paperclip/tweet.md",
displayPath: "videos/90-days-paperclip/tweet.md",
}),
])));
renderBrowser(vi.fn(), { compact: true, autoFocusSearch: false });
expect(container.textContent).not.toContain("Source");
expect(container.textContent).not.toContain("Workspace");
expect(container.textContent).not.toContain("Recently changed");
expect(container.textContent).not.toContain("Files in folder");
expect(container.textContent).not.toContain("From Isolated workspace");
expect(container.querySelector(".tabular-nums")).toBeNull();
expect(container.textContent).toContain("videos");
expect(container.textContent).toContain("tweet.md");
});
it("marks the selected file in the tree", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
renderBrowser(vi.fn(), { selectedPath: "ui/src/pages/IssueDetail.tsx" });
const selected = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === "ui/src/pages/IssueDetail.tsx",
);
expect(selected?.getAttribute("aria-selected")).toBe("true");
});
it("lists the selected file's parent folder so deep-linked files appear selected", () => {
const commandsItem = createItem({
title: "commands.md",
relativePath: "docs/reference/cli/commands.md",
displayPath: "docs/reference/cli/commands.md",
byteSize: 8192,
});
useQueryMock.mockReturnValue(ok(availableResponse([commandsItem])));
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
selectedPath: "docs/reference/cli/commands.md",
});
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
expect(listCall?.[0].queryKey[4]).toMatchObject({
mode: "all",
path: "docs/reference/cli",
});
const selected = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === "docs/reference/cli/commands.md",
);
expect(selected?.getAttribute("aria-selected")).toBe("true");
expect(container.textContent).toContain("commands.md");
});
it("treats an explicit null initial folder as the workspace root", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem({
title: "README.md",
relativePath: "README.md",
displayPath: "README.md",
})])));
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: null,
selectedPath: "docs/reference/cli/commands.md",
});
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
expect(listCall?.[0].queryKey[4]).toMatchObject({
mode: "all",
path: null,
});
});
it("preserves an initial search instead of narrowing to the selected file's parent folder", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem({
title: "FileViewerSheet.tsx",
relativePath: "ui/src/components/FileViewerSheet.tsx",
displayPath: "ui/src/components/FileViewerSheet.tsx",
})])));
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialQuery: "FileViewerSheet",
selectedPath: "ui/src/components/FileViewerSheet.tsx",
});
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
expect(listCall?.[0].queryKey[4]).toMatchObject({
mode: "all",
q: "FileViewerSheet",
path: null,
});
});
it("does not mark an unrelated highlighted row as selected", () => {
useQueryMock.mockReturnValue(ok(availableResponse([
createItem({
title: "vite.config.ts",
relativePath: "ui/vite.config.ts",
displayPath: "ui/vite.config.ts",
}),
])));
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
selectedPath: "docs/reference/cli/commands.md",
});
const unrelated = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === "ui/vite.config.ts",
);
expect(unrelated?.getAttribute("aria-selected")).toBe("false");
});
it("does not render a Recent/All toggle", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
renderBrowser();
expect(container.textContent).not.toContain("All files");
expect(container.textContent).not.toContain("Recent changes / All");
});
it("discloses truncation in the footer", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()], true)));
renderBrowser();
expect(container.textContent).toContain("refine the search to narrow");
});
it("renders newly loaded current-folder rows after Load more", () => {
const folderPath = "ui/src/components";
const pageZero = availableAllResponse([
createItem({
title: "IssueLinkQuicklook.tsx",
relativePath: `${folderPath}/IssueLinkQuicklook.tsx`,
displayPath: `${folderPath}/IssueLinkQuicklook.tsx`,
}),
], folderPath, true, 0);
const pageOne = availableAllResponse([
createItem({
title: "SourceTrustBadge.tsx",
relativePath: `${folderPath}/SourceTrustBadge.tsx`,
displayPath: `${folderPath}/SourceTrustBadge.tsx`,
}),
], folderPath, true, LIST_LIMIT);
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
const query = options.queryKey[4] as { path?: string | null; offset?: number } | undefined;
if (query?.path === folderPath && query.offset === LIST_LIMIT) return ok(pageOne);
return ok(pageZero);
});
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: folderPath,
});
expect(container.textContent).toContain("IssueLinkQuicklook.tsx");
expect(container.textContent).not.toContain("SourceTrustBadge.tsx");
const loadMore = Array.from(container.querySelectorAll("button")).find(
(el) => el.textContent === "Load more from this folder",
);
act(() => {
loadMore!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(container.textContent).toContain("SourceTrustBadge.tsx");
});
it("opens the highlighted row when Enter is pressed in the search field", () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
const { onOpen } = renderBrowser();
const input = container.querySelector("input")!;
act(() => {
input.dispatchEvent(new KeyboardEvent("keydown", { key: "ArrowDown", bubbles: true, cancelable: true }));
});
act(() => {
input.dispatchEvent(new KeyboardEvent("keydown", { key: "Enter", bubbles: true, cancelable: true }));
});
expect(onOpen).toHaveBeenCalledWith({
path: "ui/src/pages/IssueDetail.tsx",
workspace: "auto",
browseFolderPath: null,
browseQuery: null,
});
});
it("reports live search state for URL-backed browse preservation", async () => {
useQueryMock.mockReturnValue(ok(availableResponse([createItem()])));
const onBrowseStateChange = vi.fn();
renderBrowser(vi.fn(), {
initialFolderPath: "ui/src/components",
onBrowseStateChange,
});
onBrowseStateChange.mockClear();
const input = container.querySelector("input")!;
const nativeSetter = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value")?.set;
act(() => {
nativeSetter?.call(input, "FileViewerSheet");
input.dispatchEvent(new Event("input", { bubbles: true, cancelable: true }));
});
await new Promise((resolve) => window.setTimeout(resolve, 0));
expect(onBrowseStateChange).toHaveBeenLastCalledWith({
q: "FileViewerSheet",
folderPath: "ui/src/components",
projectId: null,
workspaceId: null,
});
});
it("shows the remote-workspace state without file rows", () => {
useQueryMock.mockReturnValue(ok(unavailableResponse("remote_workspace")));
renderBrowser();
expect(container.textContent).toContain("Remote workspace preview not supported");
expect(container.querySelector('[role="tree"]')).toBeNull();
});
it("shows the no-workspace state when the issue has no workspace", () => {
useQueryMock.mockReturnValue(ok(unavailableResponse("no_workspace")));
renderBrowser();
expect(container.textContent).toContain("No workspace yet");
});
it("opens a result from a selected other project workspace", () => {
const contentItem = createItem({
relativePath: "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
displayPath: "Paperclip Content / content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
workspaceLabel: "Paperclip Content",
workspaceKind: "project_workspace",
workspaceId: "workspace-content",
projectId: "project-content",
projectName: "Paperclip Content",
});
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
if (options.queryKey[0] === "projects") return ok([createProject()]);
return ok(availableResponse([contentItem]));
});
const { onOpen } = renderBrowser(vi.fn(), {
companyId: "company-1",
initialProjectId: "project-content",
initialWorkspaceId: "workspace-content",
});
expect(container.textContent).not.toContain("Other project");
expect(container.textContent).toContain("Paperclip Content / Paperclip Content");
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
expect(listCall?.[0].queryKey[4]).toMatchObject({
workspace: "project",
projectId: "project-content",
workspaceId: "workspace-content",
});
const option = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === contentItem.displayPath,
)!;
act(() => {
option.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(onOpen).toHaveBeenCalledWith({
path: "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/README.md",
workspace: "project",
projectId: "project-content",
workspaceId: "workspace-content",
browseFolderPath: null,
browseQuery: null,
});
});
it("focuses an initial folder in a selected other project workspace", () => {
const folderPath = "content-os/cases/active/2026-06-06-pap-10199-bundled-skills/";
const contentItem = createItem({
relativePath: `${folderPath}README.md`,
displayPath: `Paperclip Content / ${folderPath}README.md`,
workspaceLabel: "Paperclip Content",
workspaceKind: "project_workspace",
workspaceId: "workspace-content",
projectId: "project-content",
projectName: "Paperclip Content",
});
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
if (options.queryKey[0] === "projects") return ok([createProject()]);
return ok(availableResponse([contentItem]));
});
const { onOpen } = renderBrowser(vi.fn(), {
companyId: "company-1",
initialProjectId: "project-content",
initialWorkspaceId: "workspace-content",
initialFolderPath: folderPath,
});
expect(container.textContent).toContain("bundled-skills");
expect(container.textContent).not.toContain(folderPath);
expect(container.textContent).not.toContain("Files in folder");
const listCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[3] === "list");
expect(listCall?.[0].queryKey[4]).toMatchObject({
workspace: "project",
projectId: "project-content",
workspaceId: "workspace-content",
path: folderPath,
});
const option = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === contentItem.displayPath,
)!;
act(() => {
option.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(onOpen).toHaveBeenCalledWith({
path: `${folderPath}README.md`,
workspace: "project",
projectId: "project-content",
workspaceId: "workspace-content",
browseFolderPath: folderPath,
browseQuery: null,
});
});
it("opens sibling files with the current folder scope", () => {
const folderPath = "docs/cli";
const controlPlane = createItem({
title: "control-plane-commands.md",
relativePath: `${folderPath}/control-plane-commands.md`,
displayPath: `${folderPath}/control-plane-commands.md`,
});
useQueryMock.mockReturnValue(ok(availableResponse([
createItem({
title: "setup-commands.md",
relativePath: `${folderPath}/setup-commands.md`,
displayPath: `${folderPath}/setup-commands.md`,
}),
controlPlane,
])));
const { onOpen } = renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: folderPath,
selectedPath: `${folderPath}/setup-commands.md`,
});
const option = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === controlPlane.displayPath,
)!;
act(() => {
option.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
expect(onOpen).toHaveBeenCalledWith({
path: `${folderPath}/control-plane-commands.md`,
workspace: "auto",
browseFolderPath: folderPath,
browseQuery: null,
});
});
it("scrolls the selected file into view after breadcrumb-scoped listings", () => {
const folderPath = "docs";
const selectedPath = "docs/cli/setup-commands.md";
const scrollIntoView = vi.fn();
Object.defineProperty(Element.prototype, "scrollIntoView", {
configurable: true,
value: scrollIntoView,
});
vi.spyOn(Element.prototype, "getBoundingClientRect").mockImplementation(function getBoundingClientRect(this: Element) {
if (this.getAttribute("aria-selected") === "true") {
return { top: 1086, bottom: 1114, left: 0, right: 100, width: 100, height: 28, x: 0, y: 1086, toJSON: () => ({}) };
}
if (typeof this.className === "string" && this.className.includes("overflow-y-auto")) {
return { top: 188, bottom: 546, left: 0, right: 320, width: 320, height: 358, x: 0, y: 188, toJSON: () => ({}) };
}
return { top: 0, bottom: 0, left: 0, right: 0, width: 0, height: 0, x: 0, y: 0, toJSON: () => ({}) };
});
useQueryMock.mockReturnValue(ok(availableResponse([createItem({
title: "setup-commands.md",
relativePath: selectedPath,
displayPath: selectedPath,
})])));
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: folderPath,
selectedPath,
});
expect(scrollIntoView).toHaveBeenCalledWith({ block: "nearest" });
});
it("loads the selected file's ancestor folders when a parent breadcrumb is open", () => {
const selectedPath = "docs/cli/setup-commands.md";
const docsResponse = availableResponse([
createDirectoryItem({
title: "cli",
relativePath: "docs/cli",
displayPath: "docs/cli/",
}),
]);
docsResponse.query = { ...docsResponse.query, mode: "all", path: "docs" };
const cliResponse = availableResponse([
createItem({
title: "setup-commands.md",
relativePath: selectedPath,
displayPath: selectedPath,
}),
]);
cliResponse.query = { ...cliResponse.query, mode: "all", path: "docs/cli" };
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
const query = options.queryKey[4] as { path?: string | null } | undefined;
if (query?.path === "docs/cli") return ok(cliResponse);
return ok(docsResponse);
});
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: "docs",
selectedPath,
});
const childListCall = useQueryMock.mock.calls.find(([options]) => options.queryKey?.[4]?.path === "docs/cli");
expect(childListCall?.[0].queryKey[4]).toMatchObject({ path: "docs/cli", offset: 0 });
const selected = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === selectedPath,
);
expect(selected?.getAttribute("aria-selected")).toBe("true");
expect(container.textContent).toContain("setup-commands.md");
});
it("auto-pages the selected file's current folder until the selected row is loaded", async () => {
const folderPath = "ui/src/components";
const selectedPath = `${folderPath}/WorkspaceFileBrowser.tsx`;
const pageZero = availableAllResponse([
createItem({
title: "ActivityCharts.tsx",
relativePath: `${folderPath}/ActivityCharts.tsx`,
displayPath: `${folderPath}/ActivityCharts.tsx`,
}),
], folderPath, true, 0);
const pageOne = availableAllResponse([
createItem({
title: "SourceTrustBadge.tsx",
relativePath: `${folderPath}/SourceTrustBadge.tsx`,
displayPath: `${folderPath}/SourceTrustBadge.tsx`,
}),
], folderPath, true, LIST_LIMIT);
const pageTwo = availableAllResponse([
createItem({
title: "WorkspaceFileBrowser.tsx",
relativePath: selectedPath,
displayPath: selectedPath,
}),
], folderPath, false, LIST_LIMIT * 2);
useQueryMock.mockImplementation((options: { queryKey: readonly unknown[] }) => {
const query = options.queryKey[4] as { path?: string | null; offset?: number } | undefined;
if (query?.path === folderPath && query.offset === LIST_LIMIT) return ok(pageOne);
if (query?.path === folderPath && query.offset === LIST_LIMIT * 2) return ok(pageTwo);
return ok(pageZero);
});
renderBrowser(vi.fn(), {
compact: true,
autoFocusSearch: false,
initialFolderPath: folderPath,
selectedPath,
});
await waitForExpectation(() => {
const pageTwoCall = useQueryMock.mock.calls.find(
([options]) => options.queryKey?.[4]?.path === folderPath && options.queryKey[4].offset === LIST_LIMIT * 2,
);
expect(pageTwoCall?.[0].queryKey[4]).toMatchObject({ path: folderPath, offset: LIST_LIMIT * 2 });
});
await waitForExpectation(() => {
const selected = Array.from(container.querySelectorAll('[role="treeitem"]')).find(
(el) => el.getAttribute("title") === selectedPath,
);
expect(selected?.getAttribute("aria-selected")).toBe("true");
expect(container.textContent).toContain("WorkspaceFileBrowser.tsx");
});
});
it("lets breadcrumb folders navigate to parent directories", () => {
const folderPath = "content-os/cases/active/";
useQueryMock.mockReturnValue(ok(availableResponse([createItem({
relativePath: `${folderPath}README.md`,
displayPath: `${folderPath}README.md`,
})])));
renderBrowser(vi.fn(), { initialFolderPath: folderPath });
const contentOsCrumb = Array.from(container.querySelectorAll("button")).find(
(el) => el.getAttribute("title") === "content-os",
);
expect(contentOsCrumb).not.toBeUndefined();
act(() => {
contentOsCrumb!.dispatchEvent(new MouseEvent("click", { bubbles: true, cancelable: true }));
});
const latestListCall = useQueryMock.mock.calls.filter(([options]) => options.queryKey?.[3] === "list").at(-1);
expect(latestListCall?.[0].queryKey[4]).toMatchObject({ path: "content-os" });
});
});
describe("describeUnavailable", () => {
it("maps reasons to copy that matches the viewer's denial voice", () => {
expect(describeUnavailable("remote_workspace").title).toBe("Remote workspace preview not supported");
expect(describeUnavailable("no_workspace").title).toBe("No workspace yet");
expect(describeUnavailable("no_local_workspace").title).toBe("No workspace yet");
expect(describeUnavailable("workspace_unavailable").title).toBe("Workspace is no longer available");
expect(describeUnavailable("archived").title).toBe("Workspace is no longer available");
});
it("never leaks the raw reason code as the body", () => {
for (const reason of ["remote_workspace", "no_workspace", "workspace_unavailable", "weird_unknown"]) {
const { body } = describeUnavailable(reason);
expect(body).not.toBe(reason);
expect(body).not.toMatch(/^[a-z_]+$/);
}
});
});