Files
orca/config/scripts/reclaim-worktree-generated-files.test.mjs
T

337 lines
12 KiB
JavaScript

import {
chmod,
link,
lstat,
mkdir,
mkdtemp,
readFile,
rename,
rm,
symlink,
utimes,
writeFile
} from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join, resolve } from 'node:path'
import { afterEach, expect, it, vi } from 'vitest'
import * as processHost from '@orca/process-host'
import { runProcess } from '@orca/process-host'
import { reclaimWorktreeGeneratedFiles } from './reclaim-worktree-generated-files.mjs'
const roots = []
afterEach(async () => {
vi.restoreAllMocks()
for (const root of roots.splice(0)) {
await rm(root, { recursive: true, force: true })
}
})
async function fixture() {
const root = await mkdtemp(join(tmpdir(), 'orca-generated-reclaim-'))
roots.push(root)
const repoRoot = join(root, 'main')
const sibling = join(root, 'sibling')
await mkdir(repoRoot)
const git = async (...args) => {
const result = await runProcess({
program: 'git',
args: [
'-C',
repoRoot,
'-c',
'user.name=Test',
'-c',
'user.email=test@example.invalid',
'-c',
'commit.gpgsign=false',
...args
],
env: {
...process.env,
GIT_CONFIG_GLOBAL: join(root, 'no-global-config'),
GIT_CONFIG_NOSYSTEM: '1'
},
timeoutMs: 10_000,
terminationBarrier: true
})
expect(result.code, result.stderr).toBe(0)
}
await git('init', '--quiet')
await writeFile(join(repoRoot, '.gitignore'), '**/node_modules/\n**/out/\n**/dist/\n**/build/\n')
for (const packagePath of ['', 'mobile', 'native/addon', 'src/packages/example']) {
const path = join(repoRoot, packagePath)
await mkdir(path, { recursive: true })
await writeFile(join(path, 'package.json'), '{"private":true}')
}
await writeFile(join(repoRoot, 'source.js'), 'original source')
await git('add', '.')
await git('commit', '--quiet', '-m', 'fixture')
await git('worktree', 'add', '--quiet', '--detach', sibling)
return { root, repoRoot, sibling, git }
}
async function generated(root, path = 'node_modules', days = 40) {
const directory = join(root, path)
await mkdir(directory, { recursive: true })
const file = join(directory, 'generated.txt')
await writeFile(file, 'generated bytes')
const time = new Date(Date.now() - days * 86_400_000)
await utimes(directory, time, time)
return file
}
it('previews and removes old root/nested generated files while preserving dirty source and shared pnpm data', async () => {
const f = await fixture()
const protectedFile = await generated(f.repoRoot)
const paths = [
'node_modules',
'mobile/node_modules',
'native/addon/build',
'src/packages/example/dist',
'out'
]
const files = await Promise.all(paths.map((path) => generated(f.sibling, path)))
const store = join(f.root, 'shared-pnpm-store')
await mkdir(store)
await writeFile(join(store, 'package.js'), 'shared package')
await symlink(store, join(f.sibling, 'node_modules', 'shared-package'), 'junction')
await utimes(join(f.sibling, 'node_modules'), new Date(0), new Date(0))
await writeFile(join(f.sibling, 'source.js'), 'uncommitted source')
await mkdir(join(f.sibling, '.context'))
await writeFile(join(f.sibling, '.context', 'report.md'), 'keep this report')
const options = { repoRoot: f.repoRoot, report: () => {} }
const preview = await reclaimWorktreeGeneratedFiles(options)
expect(preview.entries).toBe(5)
for (const file of files) {
expect(await readFile(file, 'utf8')).toBe('generated bytes')
}
await reclaimWorktreeGeneratedFiles({ ...options, apply: true })
for (const file of files) {
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
}
expect(await readFile(protectedFile, 'utf8')).toBe('generated bytes')
expect(await readFile(join(store, 'package.js'), 'utf8')).toBe('shared package')
expect(await readFile(join(f.sibling, 'source.js'), 'utf8')).toBe('uncommitted source')
expect(await readFile(join(f.sibling, '.context', 'report.md'), 'utf8')).toBe('keep this report')
expect((await lstat(join(f.sibling, '.git'))).isFile()).toBe(true)
})
it('retains recent directories, tracked output, and ignored files outside recognized package roots', async () => {
const f = await fixture()
const recent = await generated(f.sibling, 'node_modules', 1)
const tracked = await generated(f.sibling, 'dist')
const unknown = await generated(f.sibling, 'personal/build')
await f.git('-C', f.sibling, 'add', '-f', 'dist/generated.txt')
await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
report: () => {}
})
for (const file of [recent, tracked, unknown]) {
expect(await readFile(file, 'utf8')).toBe('generated bytes')
}
})
it('protects tracked files under package names containing Git pathspec metacharacters', async () => {
const f = await fixture()
const root = join(f.sibling, 'native', 'addon[1]')
const file = await generated(root, 'build')
await writeFile(join(root, 'package.json'), '{}')
await f.git(
'--literal-pathspecs',
'-C',
f.sibling,
'add',
'-f',
'native/addon[1]/build/generated.txt'
)
await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
report: () => {}
})
expect(await readFile(file, 'utf8')).toBe('generated bytes')
})
it('skips symlinked targets and package ancestors, and nested repositories', async () => {
const f = await fixture()
const external = join(f.root, 'external')
const outside = await generated(external)
await writeFile(join(external, 'package.json'), '{}')
await symlink(external, join(f.sibling, 'node_modules'), 'junction')
await rm(join(f.sibling, 'native', 'addon'), { recursive: true })
await symlink(external, join(f.sibling, 'native', 'addon'), 'junction')
const nested = await generated(f.sibling, 'mobile/node_modules')
await writeFile(join(f.sibling, 'mobile', '.git'), 'gitdir: elsewhere')
const preview = await reclaimWorktreeGeneratedFiles({ repoRoot: f.repoRoot, report: () => {} })
expect(preview.entries).toBe(0)
expect(await readFile(outside, 'utf8')).toBe('generated bytes')
expect(await readFile(nested, 'utf8')).toBe('generated bytes')
})
it.each(['directory', 'file'])(
'preserves uncommitted source in a nested repository with a .git %s beneath output',
async (kind) => {
const f = await fixture()
const file = await generated(f.sibling, 'out')
const nested = join(dirname(file), 'local-checkout')
await mkdir(nested)
await (kind === 'directory'
? f.git('-C', nested, 'init', '--quiet')
: writeFile(join(nested, '.git'), 'gitdir: elsewhere'))
const source = join(nested, 'source.js')
await writeFile(source, 'uncommitted source')
await utimes(dirname(file), new Date(0), new Date(0))
for (const apply of [false, true]) {
const result = await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply,
report: () => {}
})
expect(result.entries).toBe(0)
expect(result.protected).toBe(1)
expect(await readFile(source, 'utf8')).toBe('uncommitted source')
expect(await readFile(file, 'utf8')).toBe('generated bytes')
}
}
)
it('preserves bare repositories and their refs beneath ignored output', async () => {
const f = await fixture()
const file = await generated(f.sibling, 'out')
const nested = join(dirname(file), 'saved.git')
await f.git('clone', '--quiet', '--bare', f.repoRoot, nested)
const head = await readFile(join(nested, 'HEAD'), 'utf8')
await utimes(dirname(file), new Date(0), new Date(0))
const result = await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
report: () => {}
})
expect(result.entries).toBe(0)
expect(result.protected).toBe(1)
expect(await readFile(join(nested, 'HEAD'), 'utf8')).toBe(head)
expect((await lstat(join(nested, 'objects'))).isDirectory()).toBe(true)
})
it('resolves worktree exclusions through filesystem aliases', async () => {
const f = await fixture()
const file = await generated(f.sibling)
const alias = join(f.root, 'alias')
await symlink(f.sibling, alias, 'junction')
await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
excludeWorktree: alias,
apply: true,
report: () => {}
})
expect(await readFile(file, 'utf8')).toBe('generated bytes')
})
it('retains a replacement directory introduced while the final Git check is running', async () => {
const f = await fixture()
const file = await generated(f.sibling)
const target = dirname(file)
const saved = join(f.sibling, 'saved-original')
const execute = processHost.runProcess
let checks = 0
vi.spyOn(processHost, 'runProcess').mockImplementation(async (spec) => {
const result = await execute(spec)
if (
spec.args.includes('check-ignore') &&
spec.args.at(-1) === 'node_modules/' &&
++checks === 2
) {
await rename(target, saved)
await generated(f.sibling)
}
return result
})
const result = await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
report: () => {}
})
expect(checks).toBe(2)
expect(result.protected).toBe(1)
expect(await readFile(file, 'utf8')).toBe('generated bytes')
expect(await readFile(join(saved, 'generated.txt'), 'utf8')).toBe('generated bytes')
})
it('supports a retention override and rejects invalid ages', async () => {
const f = await fixture()
const file = await generated(f.sibling, 'mobile/node_modules', 2)
await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
generatedMinAgeDays: 1,
report: () => {}
})
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
await expect(
reclaimWorktreeGeneratedFiles({ repoRoot: f.repoRoot, generatedMinAgeDays: Number.NaN })
).rejects.toThrow('--generated-min-age-days')
})
it.skipIf(process.platform === 'win32' || process.getuid?.() === 0)(
'reclaims read-only generated directories without chmodding hardlinked package files',
async () => {
const f = await fixture()
const shared = join(f.root, 'shared-package.js')
await writeFile(shared, 'shared package', { mode: 0o444 })
const originalMode = (await lstat(shared)).mode & 0o777
const file = await generated(f.sibling)
await link(shared, join(dirname(file), 'package.js'))
await utimes(dirname(file), new Date(0), new Date(0))
await chmod(dirname(file), 0o555)
await reclaimWorktreeGeneratedFiles({
repoRoot: f.repoRoot,
apply: true,
report: () => {}
})
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
expect(await readFile(shared, 'utf8')).toBe('shared package')
expect((await lstat(shared)).mode & 0o777).toBe(originalMode)
}
)
it.each(['manual', 'automatic'])(
'includes generated cleanup in %s CLI preview and apply',
async (mode) => {
const f = await fixture()
const protectedFile = await generated(f.repoRoot)
const file = await generated(f.sibling, 'mobile/node_modules')
const store = join(f.root, 'global-pnpm-store')
await mkdir(store)
const shared = join(store, 'package.js')
await writeFile(shared, 'shared package')
await link(shared, join(dirname(file), 'package.js'))
await symlink(store, join(dirname(file), 'shared-package'), 'junction')
await utimes(dirname(file), new Date(0), new Date(0))
const run = (args) =>
runProcess({
program: process.env.ORCA_TEST_NODE_EXECUTABLE ?? process.execPath,
args: [
resolve(import.meta.dirname, 'reclaim-worktree-space.mjs'),
'--repo',
f.repoRoot,
...(mode === 'automatic' ? ['--automatic'] : []),
...args
],
cwd: tmpdir(),
env: { ...process.env, ORCA_BACKGROUND_LAUNCH: '1' },
timeoutMs: 30_000,
terminationBarrier: true
})
const preview = await run([])
expect(preview.code, preview.stderr).toBe(0)
expect(preview.stdout).toContain('would remove generated directory')
expect(await readFile(file, 'utf8')).toBe('generated bytes')
const applied = await run(['--apply'])
expect(applied.code, applied.stderr).toBe(0)
await expect(lstat(file)).rejects.toMatchObject({ code: 'ENOENT' })
expect(await readFile(protectedFile, 'utf8')).toBe('generated bytes')
expect(await readFile(shared, 'utf8')).toBe('shared package')
}
)