mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
* fix(runtime): retain watcher and PTY teardown ownership * fix(runtime): restore watchers after interrupted cleanup * fix(runtime): prevent stale watcher revival * test(runtime): cover watcher shutdown ownership * test(daemon): model physical PTY exit * fix(daemon): keep shutdown terminating when disposal cannot prove exit A rejecting host.dispose() (unreapable child past its exit deadline) left the shutdown RPC without its process.nextTick(shutdown) and skipped socket cleanup in shutdown(), stranding the daemon as an unreachable orphan after the stale-daemon replacement flow unlinks its socket. Log and continue: daemon exit reparents the child to init instead of blocking on it. * fix(runtime): keep local watching alive after an idle-kill deadline miss An idle child that outlived the exit deadline set shutdownRequested on the shared desktop supervisor, which has no retire-and-replace path — every later subscribe rejected supervisor_disposed and the roots were cached unwatchable, silently ending local file watching for the session. The idle path owns zero records, so there is no double-watch hazard; the zombie keeps its capacity reservation until physical exit and the next subscribe gets a fresh child. * fix(renderer): resync replayed paired-web file watches Transparent replay removed the implicit resync the old close-and-rebuild path provided: a replayed files.watch only reports changes from its own native setup, so changes during the reconnect gap were silently lost. Deliver a conservative overflow to consumers once the replayed watch is ready, matching the overflow-after-interruption contract everywhere else. * fix(runtime): address teardown review findings * fix(runtime): retry watches after teardown deadlines * Fix PTY descendant leaks on forced teardown * Fix jitter-sensitive terminal lifecycle test
473 lines
17 KiB
TypeScript
473 lines
17 KiB
TypeScript
import {
|
|
mkdtempSync,
|
|
mkdirSync,
|
|
writeFileSync,
|
|
statSync,
|
|
lstatSync,
|
|
readlinkSync,
|
|
readFileSync,
|
|
rmSync,
|
|
symlinkSync,
|
|
existsSync,
|
|
chmodSync
|
|
} from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import {
|
|
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
|
|
}): 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') {
|
|
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, worktree])
|
|
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('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()
|
|
})
|
|
})
|