Files
orca/src/main/ipc/worktree-symlinks.test.ts
T
Brennan Benson b600e25fa1 feat(worktrees): support project-level .worktreeinclude (literal paths) for copying gitignored files into worktrees (#9791)
* feat(worktrees): copy project-level .worktreeinclude paths into new worktrees

Read .worktreeinclude at the repo root (gitignore syntax) and copy matching
gitignored paths from the primary checkout into each newly created local
worktree, so .env and other local config carry over with zero per-user setup.

- Literal patterns resolve by direct stat; globs match against
  ls-files --others --ignored --exclude-standard --directory (collapsed
  dirs keep huge repos fast); every candidate is re-verified with
  check-ignore so tracked or unignored files are never copied.
- Copy semantics, never symlink: APFS clone-copy on macOS, real copy
  elsewhere, so each worktree owns its files (unlike repo.symlinkPaths,
  which it merges with rather than replaces).
- Failures never block worktree creation.
- Remote (SSH) creation skips it, same as symlinkPaths.
- Split APFS clone helpers into worktree-apfs-clone.ts (max-lines).

Closes #7549

* fix(worktrees): harden worktree include copying

* fix(worktrees): support nested includes on Git 2.25

* fix(worktrees): bound include copy costs

* fix(worktrees): close include correctness and perf gaps

* fix(worktrees): preserve included copy semantics

* fix(worktrees): harden include resolution

* fix(worktrees): preserve bounded include resolution

* fix(worktrees): bound include filesystem resolution

* fix(worktrees): harden include matching

* fix(worktrees): tighten include matching and scan bounds

* fix(types): use concrete filesystem stat types

* fix(worktrees): harden included path materialization

* perf(worktrees): stop include parsing at resolver budgets

* chore(skills): refresh bundled skill manifests

* refactor(worktrees): reduce .worktreeinclude to focused literal-only scope

The reviewed implementation grew well past the ticket (#7549), which asks for a
size-M feature that reuses existing worktree machinery. Trim back to the minimal
change that solves the reported problem safely:

- Resolver now supports literal files and directories only. Glob/negation lines
  are skipped with a warning (documented follow-up), which removes the entire
  user-controlled-regex ReDoS surface, the CPU/byte budgets, the git enumeration
  scan, and the case-sensitivity engine. The filesystem + git check-ignore
  handle existence and case for free.
- Copy layer folded back into worktree-symlinks.ts (link/copy modes share one
  loop); dropped worktree-path-copy.ts, worktree-target-safety.ts, the
  descendant-dedup/realpath/target-parent machinery, and the per-materialization
  APFS filesystem cache. Kept the df/diskutil probe timeout.
- Reverted unrelated changes: check-ignored-paths timeout param and the
  git-binary-compatibility enumeration tests.

Net: -1903/+172 across the include+copy code. Behavior for the ticket's cases
(.env, .env.local, .vscode/, node_modules, config/secrets.json) is unchanged;
gitignored-only + copy-not-symlink semantics preserved.

Closes #7549

* fix(worktrees): dereference symlinked .worktreeinclude entries + cache APFS volume probe

Two issues found by review + perf audit of the copy path:

- Correctness (HIGH): a listed entry that is itself a gitignored symlink was
  copied AS a symlink (fs.cp dereference:false), and the darwin APFS branch was
  skipped for all symlink sources. Editing the worktree's copy then wrote through
  to the shared/primary target — inverting copy-mode's 'each worktree owns its
  files' guarantee, and escaping the worktree entirely if the link pointed
  outside it. Now resolve realpath for a top-level symlink in copy mode so we
  copy content; nested symlinks inside a copied dir stay as-is (cp -R semantics).

- Perf: assertSameApfsVolume ran df+diskutil per copied path (4 subprocesses
  each), so an N-entry include spawned ~4N short-lived processes on the macOS
  create hot path, all re-probing one volume. Add a per-materialization
  device-keyed cache: one probe per distinct volume (4N -> ~4).

Tests: symlinked-file and symlinked-dir dereference regressions (no leak to
primary); APFS volume probed once regardless of copied-path count.
2026-07-24 15:31:23 -07:00

641 lines
24 KiB
TypeScript

import {
mkdtempSync,
mkdirSync,
writeFileSync,
statSync,
lstatSync,
readlinkSync,
readFileSync,
rmSync,
symlinkSync,
existsSync,
chmodSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join, sep } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
createWorktreeCopiedPaths,
createWorktreeLinkedPaths,
createWorktreeSymlinks,
findExistingWorktreeSymlinkPaths,
removeWorktreeLinkedPaths,
removeWorktreeSymlinks
} from './worktree-symlinks'
type WorktreeLinkedPathOptionsForTest = NonNullable<Parameters<typeof createWorktreeLinkedPaths>[3]>
type ApfsCloneDepsForTest = NonNullable<WorktreeLinkedPathOptionsForTest['apfsCloneDeps']>
const posixIt = process.platform === 'win32' ? it.skip : it
function createApfsCloneDeps(options: {
uuid?: string
onCp?: (args: readonly string[]) => void
onDiskutil?: () => void
diskutilError?: Error
}): ApfsCloneDepsForTest {
const execFileAsync = vi.fn<ApfsCloneDepsForTest['execFileAsync']>(async (file, args) => {
if (file === '/bin/df') {
return {
stdout: `Filesystem 512-blocks Used Available Capacity Mounted on
/dev/disk3s1 100 50 50 50% /
`,
stderr: ''
}
}
if (file === '/usr/sbin/diskutil') {
if (options.diskutilError) {
throw options.diskutilError
}
options.onDiskutil?.()
return {
stdout: `<plist><dict><key>FilesystemName</key><string>APFS</string></dict></plist>`,
stderr: ''
}
}
if (file === '/bin/cp') {
options.onCp?.(args)
return { stdout: '', stderr: '' }
}
throw new Error(`Unexpected execFile command: ${file}`)
})
return {
execFileAsync,
randomUUID: () => options.uuid ?? 'test'
}
}
describe('createWorktreeSymlinks', () => {
let root: string
let primary: string
let worktree: string
let warn: ReturnType<typeof vi.spyOn>
let error: ReturnType<typeof vi.spyOn>
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-symlinks-'))
primary = join(root, 'primary')
worktree = join(root, 'worktree')
mkdirSync(primary, { recursive: true })
mkdirSync(worktree, { recursive: true })
warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
error = vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
warn.mockRestore()
error.mockRestore()
rmSync(root, { recursive: true, force: true })
})
it('symlinks a file from primary into the worktree at the same relative path', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
await createWorktreeSymlinks(primary, worktree, ['.env'])
const linkStat = lstatSync(join(worktree, '.env'))
expect(linkStat.isSymbolicLink()).toBe(true)
expect(readlinkSync(join(worktree, '.env'))).toBe(join(primary, '.env'))
// Following the link yields the primary's contents.
expect(statSync(join(worktree, '.env')).isFile()).toBe(true)
})
it('symlinks a directory from primary into the worktree', async () => {
mkdirSync(join(primary, 'node_modules'))
writeFileSync(join(primary, 'node_modules', 'marker'), 'installed')
await createWorktreeSymlinks(primary, worktree, ['node_modules'])
expect(lstatSync(join(worktree, 'node_modules')).isSymbolicLink()).toBe(true)
expect(statSync(join(worktree, 'node_modules', 'marker')).isFile()).toBe(true)
})
it('creates parent directories lazily for nested paths', async () => {
mkdirSync(join(primary, 'apps', 'web'), { recursive: true })
writeFileSync(join(primary, 'apps', 'web', '.env'), 'X=1\n')
await createWorktreeSymlinks(primary, worktree, ['apps/web/.env'])
expect(lstatSync(join(worktree, 'apps', 'web', '.env')).isSymbolicLink()).toBe(true)
})
it('skips entries whose source is missing in the primary checkout', async () => {
await createWorktreeSymlinks(primary, worktree, ['node_modules'])
// No link created, no throw.
expect(() => lstatSync(join(worktree, 'node_modules'))).toThrow()
expect(error).not.toHaveBeenCalled()
})
it('preserves a pre-existing target in the worktree (no clobber)', async () => {
writeFileSync(join(primary, '.env'), 'FROM_PRIMARY=1\n')
writeFileSync(join(worktree, '.env'), 'FROM_WORKTREE=1\n')
await createWorktreeSymlinks(primary, worktree, ['.env'])
// The pre-existing regular file stays; no symlink was created.
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
expect(statSync(join(worktree, '.env')).isFile()).toBe(true)
})
it('does not escape the primary checkout via a leading-slash path', async () => {
// Why: the helper strips leading slashes (so `/etc/passwd` becomes the
// relative `etc/passwd`). No file is created outside the worktree, and the
// resolved source — which falls inside `primary/etc/passwd` — is missing,
// so the entry is silently skipped rather than linking to `/etc/passwd`.
await createWorktreeSymlinks(primary, worktree, ['/etc/passwd'])
expect(() => lstatSync(join(worktree, 'etc', 'passwd'))).toThrow()
expect(error).not.toHaveBeenCalled()
})
it('rejects parent-directory traversal', async () => {
await createWorktreeSymlinks(primary, worktree, ['../secrets'])
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Skipping unsafe path "../secrets"')
)
})
it('rejects nested traversal via ..', async () => {
await createWorktreeSymlinks(primary, worktree, ['safe/../../escape'])
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Skipping unsafe path "safe/../../escape"')
)
})
it('rejects traversal using backslash separators (Windows form)', async () => {
// Why: users configuring paths on Windows (or pasting a mixed-separator
// value) could bypass a POSIX-only split. The guard normalizes across
// `/` and `\` so `..\escape` and `foo\..\..\escape` both get rejected.
await createWorktreeSymlinks(primary, worktree, ['..\\escape'])
await createWorktreeSymlinks(primary, worktree, ['foo\\..\\..\\escape'])
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Skipping unsafe path "..\\escape"')
)
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Skipping unsafe path "foo\\..\\..\\escape"')
)
})
it('strips a leading slash then treats the remainder as a relative path', async () => {
writeFileSync(join(primary, '.env'), 'X=1\n')
await createWorktreeSymlinks(primary, worktree, ['/.env'])
// Leading slash is stripped in the helper; the remaining `.env` is a valid relative path.
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(true)
expect(warn).not.toHaveBeenCalled()
})
it('skips empty and whitespace-only entries', async () => {
// The helper logs an "unsafe path" warn for these; nothing gets linked.
await createWorktreeSymlinks(primary, worktree, ['', ' '])
expect(error).not.toHaveBeenCalled()
})
it('continues processing later entries after one fails', async () => {
writeFileSync(join(primary, '.env'), 'X=1\n')
writeFileSync(join(primary, 'config.json'), '{}')
await createWorktreeSymlinks(primary, worktree, [
'../escape', // rejected
'missing-source', // no source, skipped
'.env', // succeeds
'config.json' // succeeds
])
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(true)
expect(lstatSync(join(worktree, 'config.json')).isSymbolicLink()).toBe(true)
})
it('is a no-op for an empty paths list', async () => {
await createWorktreeSymlinks(primary, worktree, [])
expect(warn).not.toHaveBeenCalled()
expect(error).not.toHaveBeenCalled()
})
it('uses APFS clone-copy for configured paths on macOS', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const cloneWorktreePath = vi.fn(async (_source: string, target: string) => {
writeFileSync(target, 'SECRET=1\n')
})
await createWorktreeLinkedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(cloneWorktreePath).toHaveBeenCalledWith(
join(primary, '.env'),
join(worktree, '.env'),
false
)
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
expect(statSync(join(worktree, '.env')).isFile()).toBe(true)
})
it('does not overwrite a file target that appears before APFS clone-copy is published', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const target = join(worktree, '.env')
const deps = createApfsCloneDeps({
uuid: 'file-race',
onCp: (args) => {
const tempTarget = args.at(-1)
if (!tempTarget) {
throw new Error('Missing APFS clone temp target')
}
writeFileSync(tempTarget, 'SECRET=1\n')
writeFileSync(target, 'RACE=1\n')
}
})
await createWorktreeLinkedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
apfsCloneDeps: deps
})
expect(readFileSync(target, 'utf8')).toBe('RACE=1\n')
expect(existsSync(join(worktree, '.orca-apfs-clone-file-race'))).toBe(false)
expect(warn).not.toHaveBeenCalled()
expect(error).not.toHaveBeenCalled()
})
it('does not replace a directory target that appears before APFS clone-copy reserves it', async () => {
mkdirSync(join(primary, 'node_modules'))
writeFileSync(join(primary, 'node_modules', 'primary-marker'), 'PRIMARY\n')
const target = join(worktree, 'node_modules')
let createdRacedTarget = false
const deps = createApfsCloneDeps({
onDiskutil: () => {
if (!createdRacedTarget) {
createdRacedTarget = true
mkdirSync(target)
writeFileSync(join(target, 'user-marker'), 'USER\n')
}
},
onCp: () => {
throw new Error('APFS clone-copy should not run after the target appears')
}
})
await createWorktreeLinkedPaths(primary, worktree, ['node_modules'], {
platform: 'darwin',
apfsCloneDeps: deps
})
expect(readFileSync(join(target, 'user-marker'), 'utf8')).toBe('USER\n')
expect(existsSync(join(target, 'primary-marker'))).toBe(false)
expect(deps.execFileAsync).not.toHaveBeenCalledWith('/bin/cp', expect.anything())
expect(warn).not.toHaveBeenCalled()
expect(error).not.toHaveBeenCalled()
})
it('does not overwrite or remove a nested target after APFS clone-copy hits a conflict', async () => {
const source = join(primary, 'node_modules')
mkdirSync(source)
writeFileSync(join(primary, 'node_modules', 'primary-marker'), 'PRIMARY\n')
const target = join(worktree, 'node_modules')
let cpArgs: readonly string[] | undefined
const deps = createApfsCloneDeps({
onCp: (args) => {
cpArgs = args
writeFileSync(join(target, 'primary-marker'), 'USER\n')
throw new Error('clone-copy skipped an existing nested target')
}
})
await createWorktreeLinkedPaths(primary, worktree, ['node_modules'], {
platform: 'darwin',
apfsCloneDeps: deps
})
expect(cpArgs).toEqual(['-n', '-c', '-R', `${source}${sep}.`, target])
expect(readFileSync(join(target, 'primary-marker'), 'utf8')).toBe('USER\n')
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] APFS clone-copy unavailable'),
expect.any(Error)
)
expect(error).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Failed to link "node_modules"'),
expect.any(Error)
)
})
posixIt('preserves the source directory mode after APFS clone-copy reserves it', async () => {
const source = join(primary, 'node_modules')
mkdirSync(source)
chmodSync(source, 0o700)
const target = join(worktree, 'node_modules')
const deps = createApfsCloneDeps({
onCp: () => {
writeFileSync(join(target, 'marker'), 'CLONED\n')
}
})
await createWorktreeLinkedPaths(primary, worktree, ['node_modules'], {
platform: 'darwin',
apfsCloneDeps: deps
})
expect(statSync(target).mode & 0o777).toBe(0o700)
expect(readFileSync(join(target, 'marker'), 'utf8')).toBe('CLONED\n')
})
it('falls back to symlink when macOS clone-copy is unavailable', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const cloneWorktreePath = vi.fn(async () => {
throw new Error('clonefile unsupported')
})
await createWorktreeLinkedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(true)
expect(readlinkSync(join(worktree, '.env'))).toBe(join(primary, '.env'))
expect(warn).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] APFS clone-copy unavailable'),
expect.any(Error)
)
})
it('does not delete a target that appears while APFS clone-copy is failing', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const cloneWorktreePath = vi.fn(async (_source: string, target: string) => {
writeFileSync(target, 'RACE=1\n')
throw new Error('clonefile failed after target appeared')
})
await createWorktreeLinkedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(readFileSync(join(worktree, '.env'), 'utf8')).toBe('RACE=1\n')
expect(error).toHaveBeenCalledWith(
expect.stringContaining('[worktree-symlinks] Failed to link ".env"'),
expect.any(Error)
)
})
it('keeps symlink sources as symlinks instead of APFS clone-copying their targets', async () => {
writeFileSync(join(primary, '.env.real'), 'SECRET=1\n')
symlinkSync(join(primary, '.env.real'), join(primary, '.env'), 'file')
const cloneWorktreePath = vi.fn(async () => {
throw new Error('clone should not be called for symlink sources')
})
await createWorktreeLinkedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(cloneWorktreePath).not.toHaveBeenCalled()
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(true)
expect(readlinkSync(join(worktree, '.env'))).toBe(join(primary, '.env'))
})
})
describe('createWorktreeCopiedPaths', () => {
let root: string
let primary: string
let worktree: string
let warn: ReturnType<typeof vi.spyOn>
let error: ReturnType<typeof vi.spyOn>
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-copiedpaths-'))
primary = join(root, 'primary')
worktree = join(root, 'worktree')
mkdirSync(primary, { recursive: true })
mkdirSync(worktree, { recursive: true })
warn = vi.spyOn(console, 'warn').mockImplementation(() => {})
error = vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
warn.mockRestore()
error.mockRestore()
rmSync(root, { recursive: true, force: true })
})
it('copies a file so worktree edits never leak back to the primary checkout', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
await createWorktreeCopiedPaths(primary, worktree, ['.env'], { platform: 'linux' })
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
expect(readFileSync(join(worktree, '.env'), 'utf8')).toBe('SECRET=1\n')
writeFileSync(join(worktree, '.env'), 'SECRET=2\n')
expect(readFileSync(join(primary, '.env'), 'utf8')).toBe('SECRET=1\n')
})
it('copies a directory recursively without symlinking', async () => {
mkdirSync(join(primary, '.vscode'))
writeFileSync(join(primary, '.vscode', 'settings.json'), '{}')
await createWorktreeCopiedPaths(primary, worktree, ['.vscode'], { platform: 'linux' })
expect(lstatSync(join(worktree, '.vscode')).isSymbolicLink()).toBe(false)
expect(readFileSync(join(worktree, '.vscode', 'settings.json'), 'utf8')).toBe('{}')
})
it('creates parent directories lazily for nested paths', async () => {
mkdirSync(join(primary, 'apps', 'web'), { recursive: true })
writeFileSync(join(primary, 'apps', 'web', '.env'), 'A=1')
await createWorktreeCopiedPaths(primary, worktree, ['apps/web/.env'], { platform: 'linux' })
expect(readFileSync(join(worktree, 'apps', 'web', '.env'), 'utf8')).toBe('A=1')
})
// Finding 1 regression: a symlinked include entry must become an independent
// copy, not a symlink, or worktree edits would leak back into the shared target.
posixIt('dereferences a symlinked file entry so edits do not leak to the primary', async () => {
writeFileSync(join(primary, '.env.shared'), 'SECRET=1\n')
symlinkSync(join(primary, '.env.shared'), join(primary, '.env'))
await createWorktreeCopiedPaths(primary, worktree, ['.env'], { platform: 'linux' })
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
writeFileSync(join(worktree, '.env'), 'SECRET=2\n')
expect(readFileSync(join(primary, '.env.shared'), 'utf8')).toBe('SECRET=1\n')
})
posixIt('dereferences a symlinked directory entry into an independent copy', async () => {
mkdirSync(join(primary, '.cache-real'))
writeFileSync(join(primary, '.cache-real', 'f'), 'ORIG\n')
symlinkSync(join(primary, '.cache-real'), join(primary, '.cache'), 'dir')
await createWorktreeCopiedPaths(primary, worktree, ['.cache'], { platform: 'linux' })
expect(lstatSync(join(worktree, '.cache')).isSymbolicLink()).toBe(false)
writeFileSync(join(worktree, '.cache', 'f'), 'CHANGED\n')
expect(readFileSync(join(primary, '.cache-real', 'f'), 'utf8')).toBe('ORIG\n')
})
it('preserves a pre-existing target in the worktree (no clobber)', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
writeFileSync(join(worktree, '.env'), 'MINE=1\n')
await createWorktreeCopiedPaths(primary, worktree, ['.env'], { platform: 'linux' })
expect(readFileSync(join(worktree, '.env'), 'utf8')).toBe('MINE=1\n')
})
it('rejects traversal and treats absolute paths as repo-relative', async () => {
writeFileSync(join(root, 'outside.txt'), 'OUT=1')
await createWorktreeCopiedPaths(primary, worktree, ['../outside.txt', '/etc/passwd'], {
platform: 'linux'
})
expect(existsSync(join(worktree, 'outside.txt'))).toBe(false)
// `/etc/passwd` → `etc/passwd`, absent from primary → silently skipped.
expect(existsSync(join(worktree, 'etc'))).toBe(false)
expect(warn).toHaveBeenCalledTimes(1)
})
it('falls back to a real copy, not a symlink, when macOS clone-copy is unavailable', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const cloneWorktreePath = vi.fn(async () => {
throw new Error('clonefile unsupported')
})
await createWorktreeCopiedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
expect(readFileSync(join(worktree, '.env'), 'utf8')).toBe('SECRET=1\n')
})
it('uses APFS clone-copy for configured paths on macOS', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
const cloneWorktreePath = vi.fn(async (_source: string, target: string) => {
writeFileSync(target, 'CLONED=1\n')
})
await createWorktreeCopiedPaths(primary, worktree, ['.env'], {
platform: 'darwin',
cloneWorktreePath
})
expect(cloneWorktreePath).toHaveBeenCalledWith(
join(primary, '.env'),
join(worktree, '.env'),
false
)
expect(readFileSync(join(worktree, '.env'), 'utf8')).toBe('CLONED=1\n')
})
// Perf: the df+diskutil volume probe must not scale with the number of copied
// paths — one probe per distinct volume, cached across the materialization.
it('probes each APFS volume once regardless of how many paths are copied', async () => {
for (const name of ['.env', '.env.local', 'config.json', 'secrets.json']) {
writeFileSync(join(primary, name), `${name}\n`)
}
const deps = createApfsCloneDeps({ onCp: () => {} })
await createWorktreeCopiedPaths(
primary,
worktree,
['.env', '.env.local', 'config.json', 'secrets.json'],
{ platform: 'darwin', apfsCloneDeps: deps }
)
const execFileAsyncMock = vi.mocked(deps.execFileAsync)
const dfCalls = execFileAsyncMock.mock.calls.filter(([file]) => file === '/bin/df').length
const diskutilCalls = execFileAsyncMock.mock.calls.filter(
([file]) => file === '/usr/sbin/diskutil'
).length
// 4 paths would be 8 df + 8 diskutil un-cached; source+worktree share one
// tmp volume, so caching collapses this to a single probe pair.
expect(dfCalls).toBeLessThanOrEqual(2)
expect(diskutilCalls).toBeLessThanOrEqual(2)
// The copies themselves still happen per path.
expect(execFileAsyncMock.mock.calls.filter(([file]) => file === '/bin/cp')).toHaveLength(4)
})
})
describe('removeWorktreeSymlinks', () => {
let root: string
let primary: string
let worktree: string
let error: ReturnType<typeof vi.spyOn>
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), 'orca-unlink-'))
primary = join(root, 'primary')
worktree = join(root, 'worktree')
mkdirSync(primary, { recursive: true })
mkdirSync(worktree, { recursive: true })
error = vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
error.mockRestore()
rmSync(root, { recursive: true, force: true })
})
it('unlinks configured symlinks from the worktree', async () => {
writeFileSync(join(primary, '.env'), 'SECRET=1\n')
mkdirSync(join(primary, 'node_modules'))
symlinkSync(join(primary, '.env'), join(worktree, '.env'), 'file')
symlinkSync(join(primary, 'node_modules'), join(worktree, 'node_modules'), 'dir')
await removeWorktreeSymlinks(worktree, ['.env', 'node_modules'])
expect(existsSync(join(worktree, '.env'))).toBe(false)
expect(existsSync(join(worktree, 'node_modules'))).toBe(false)
// Source is untouched.
expect(statSync(join(primary, '.env')).isFile()).toBe(true)
expect(statSync(join(primary, 'node_modules')).isDirectory()).toBe(true)
})
it('leaves a regular file at the configured path alone', async () => {
// Why: a user who created a real file at `.env` (instead of symlinking)
// must not lose it just because `.env` is in the configured list.
writeFileSync(join(worktree, '.env'), 'USER_WROTE_THIS=1\n')
await removeWorktreeSymlinks(worktree, ['.env'])
expect(lstatSync(join(worktree, '.env')).isSymbolicLink()).toBe(false)
expect(statSync(join(worktree, '.env')).isFile()).toBe(true)
})
it('identifies only actual symlinks for removal preflight', async () => {
writeFileSync(join(primary, '.env'), 'PRIMARY=1\n')
symlinkSync(join(primary, '.env'), join(worktree, '.env'))
writeFileSync(join(worktree, 'config.json'), '{}\n')
await expect(
findExistingWorktreeSymlinkPaths(worktree, ['.env', 'config.json', 'missing'])
).resolves.toEqual(['.env'])
})
it('leaves APFS clone-copied regular files for git removal to judge', async () => {
writeFileSync(join(worktree, '.env'), 'CLONED=1\n')
await removeWorktreeLinkedPaths(worktree, ['.env'])
expect(existsSync(join(worktree, '.env'))).toBe(true)
})
it('ignores missing entries', async () => {
await removeWorktreeSymlinks(worktree, ['.env', 'node_modules'])
expect(error).not.toHaveBeenCalled()
})
it('rejects unsafe paths without touching the filesystem', async () => {
// Parent-dir traversal is silently skipped; no unlink attempted.
writeFileSync(join(root, 'outside-file'), 'DO_NOT_DELETE')
await removeWorktreeSymlinks(worktree, ['../outside-file'])
expect(existsSync(join(root, 'outside-file'))).toBe(true)
})
it('is a no-op for an empty paths list', async () => {
await removeWorktreeSymlinks(worktree, [])
expect(error).not.toHaveBeenCalled()
})
})