diff --git a/src/main/ipc/filesystem-auth.test.ts b/src/main/ipc/filesystem-auth.test.ts new file mode 100644 index 00000000000..b50d82a5f82 --- /dev/null +++ b/src/main/ipc/filesystem-auth.test.ts @@ -0,0 +1,49 @@ +import { resolve } from 'node:path' +import { afterEach, describe, expect, it, vi } from 'vitest' +import type { Store } from '../persistence' +import { listRepoWorktrees } from '../repo-worktrees' +import { + invalidateAuthorizedRootsCache, + rebuildAuthorizedRootsCache, + resolveRegisteredWorktreePath +} from './filesystem-auth' + +vi.mock('../repo-worktrees', () => ({ + isRepoRoot: vi.fn(() => false), + listRepoWorktrees: vi.fn() +})) + +const listRepoWorktreesMock = vi.mocked(listRepoWorktrees) + +function createStore(repoPath: string): Store { + return { + getRepos: () => [{ id: 'repo-1', path: repoPath }], + getSettings: () => ({ workspaceDir: '' }) + } as unknown as Store +} + +afterEach(() => { + invalidateAuthorizedRootsCache() + listRepoWorktreesMock.mockReset() + vi.restoreAllMocks() +}) + +describe('filesystem auth registered worktree roots', () => { + it('registers very large worktree lists during cache rebuild', async () => { + const repoPath = '/repo/main' + const targetWorktreePath = '/linked-worktrees/worktree-129999' + const worktrees = Array.from({ length: 130_000 }, (_, index) => ({ + path: `/linked-worktrees/worktree-${index}` + })) + listRepoWorktreesMock.mockResolvedValue( + worktrees as Awaited> + ) + vi.spyOn(console, 'warn').mockImplementation(() => {}) + + await rebuildAuthorizedRootsCache(createStore(repoPath)) + + await expect( + resolveRegisteredWorktreePath(targetWorktreePath, createStore(repoPath)) + ).resolves.toBe(resolve(targetWorktreePath)) + }) +}) diff --git a/src/main/ipc/filesystem-auth.ts b/src/main/ipc/filesystem-auth.ts index 92e1f6d8943..e78baab9c4c 100644 --- a/src/main/ipc/filesystem-auth.ts +++ b/src/main/ipc/filesystem-auth.ts @@ -1,3 +1,5 @@ +/* eslint-disable max-lines -- Why: filesystem authorization shares module-local + * root caches with the path validation entrypoints that consume them. */ import { resolve, relative, dirname, basename, isAbsolute } from 'path' import { realpathSync } from 'fs' import { realpath } from 'fs/promises' @@ -102,7 +104,9 @@ export async function rebuildAuthorizedRootsCache(store: Store): Promise { const worktrees = await listRepoWorktrees(repo) const worktreeRoots = worktrees.map((wt) => resolve(wt.path)) - roots.push(...worktreeRoots) + for (const worktreeRoot of worktreeRoots) { + roots.push(worktreeRoot) + } } catch (error) { // Why: a single inaccessible repo (EACCES, EIO, etc.) must not break // the entire cache rebuild — that would disable File Explorer and