Files
orca/src/shared/git-fetch-head-lock.test.ts
T
JahyunBaekandClaude Opus 5 aa95bdb11a test(shared): stop two suites asserting POSIX separators on Windows (#16511)
Both files describe paths with POSIX literals while their subjects compose
paths through `node:path`, so the assertions only hold where the separator
happens to be `/`.

`node-markdown-document-discovery` keys its fake tree at `/repo/docs` and
`/repo/one`, but `discoverMarkdownRelativePaths` descends with
`join(absoluteDirectoryPath, entry.name)` — `\repo\docs` on win32. The child
lookup misses, `readDirectory` yields nothing, and the walk stops at the root:
`docs/guide.mdx` disappears and the depth-limit case never reaches its limit,
so it resolves `[]` instead of rejecting. Keying the children with `join` walks
the tree the subject actually walks.

`git-fetch-head-lock` expects `cwd: '/tmp/repo'` from a subject that returns
`path.resolve(cwd, 'repo')`, which is `C:\tmp\repo` on win32. Asserting through
`path.resolve` pins the behaviour — that `-C` and `--git-dir` are resolved
against the cwd — rather than the separator of whichever machine runs the suite.

Verified on Windows 11: the two files go from 3 failed / 12 passed to
14 passed / 1 skipped, and the wider `src/shared` run shows no regression.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-29 14:23:22 -07:00

200 lines
7.2 KiB
TypeScript

import { describe, expect, it, vi } from 'vitest'
import { mkdtemp, mkdir, rm, symlink, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import * as path from 'node:path'
import { resolveGitFetchHeadCommand, runWithGitFetchHeadLock } from './git-fetch-head-lock'
describe('runWithGitFetchHeadLock', () => {
it.each([
{ args: ['fetch', 'origin'], expected: true },
{ args: ['pull', '--rebase'], expected: true },
{ args: ['-c', 'maintenance.auto=false', 'fetch', 'origin'], expected: true },
{ args: ['fetch', '--no-write-fetch-head', 'origin'], expected: false },
{
args: [
'fetch',
'--no-write-fetch-head',
'origin',
'+refs/heads/main:refs/orca/rebase/one',
'+refs/heads/main:refs/remotes/origin/main'
],
expected: true
},
{ args: ['fetch', '--no-write-fetch-head', '--write-fetch-head'], expected: true },
{ args: ['rev-parse', 'fetch'], expected: false }
])('classifies FETCH_HEAD operations: $args', ({ args, expected }) => {
expect(resolveGitFetchHeadCommand(args, '/repo').needsLock).toBe(expected)
})
// Why resolve() rather than a literal: the subject resolves these through path.resolve, so a
// POSIX literal asserts the separator of the machine running the suite instead of the behaviour.
it('resolves -C and --git-dir before deriving the lock identity', () => {
expect(resolveGitFetchHeadCommand(['-C', 'repo', 'fetch'], '/tmp')).toMatchObject({
cwd: path.resolve('/tmp', 'repo'),
needsLock: true
})
expect(resolveGitFetchHeadCommand(['--git-dir=/repo/.git', 'fetch'], '/tmp')).toMatchObject({
gitDir: path.resolve('/tmp', '/repo/.git'),
needsLock: true
})
})
it('serializes FETCH_HEAD users in one worktree without blocking another', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'fetch-head-lock-isolation-'))
const repo = path.join(root, 'repo')
const otherRepo = path.join(root, 'other')
await Promise.all([
mkdir(path.join(repo, '.git'), { recursive: true }),
mkdir(path.join(otherRepo, '.git'), { recursive: true })
])
let releaseFirst!: () => void
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve
})
let markFirstStarted!: () => void
const firstStarted = new Promise<void>((resolve) => {
markFirstStarted = resolve
})
const order: string[] = []
const first = runWithGitFetchHeadLock(repo, undefined, async () => {
order.push('first:start')
markFirstStarted()
await firstGate
order.push('first:end')
})
await firstStarted
const second = runWithGitFetchHeadLock(repo, undefined, async () => {
order.push('second')
})
const other = runWithGitFetchHeadLock(otherRepo, undefined, async () => {
order.push('other')
})
try {
await other
expect(order).toEqual(['first:start', 'other'])
releaseFirst()
await Promise.all([first, second])
expect(order).toEqual(['first:start', 'other', 'first:end', 'second'])
} finally {
releaseFirst()
await Promise.allSettled([first, second, other])
await rm(root, { recursive: true, force: true })
}
})
it('does not run a queued operation after cancellation', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'fetch-head-lock-cancel-'))
const repo = path.join(root, 'repo')
await mkdir(path.join(repo, '.git'), { recursive: true })
let releaseFirst!: () => void
const firstGate = new Promise<void>((resolve) => {
releaseFirst = resolve
})
let markFirstStarted!: () => void
const firstStarted = new Promise<void>((resolve) => {
markFirstStarted = resolve
})
const first = runWithGitFetchHeadLock(repo, undefined, async () => {
markFirstStarted()
await firstGate
})
await firstStarted
const controller = new AbortController()
const queued = runWithGitFetchHeadLock(repo, controller.signal, async () => 'ran')
try {
controller.abort()
await expect(queued).rejects.toMatchObject({ name: 'AbortError' })
} finally {
releaseFirst()
await Promise.allSettled([first, queued])
await rm(root, { recursive: true, force: true })
}
})
// Windows CI agents usually lack the elevation `symlink` needs.
it.skipIf(process.platform === 'win32')(
'serializes root, nested, and symlink aliases of one worktree',
async () => {
const root = await mkdtemp(path.join(tmpdir(), 'fetch-head-lock-'))
const repo = path.join(root, 'repo')
const nested = path.join(repo, 'nested')
const alias = path.join(root, 'alias')
await mkdir(path.join(repo, '.git'), { recursive: true })
await mkdir(nested)
await symlink(repo, alias)
let release!: () => void
const gate = new Promise<void>((resolve) => {
release = resolve
})
let markFirstStarted!: () => void
const firstStarted = new Promise<void>((resolve) => {
markFirstStarted = resolve
})
const order: string[] = []
try {
const first = runWithGitFetchHeadLock(repo, undefined, async () => {
order.push('root')
markFirstStarted()
await gate
})
await firstStarted
const nestedRun = runWithGitFetchHeadLock(nested, undefined, async () => {
order.push('nested')
})
const aliasRun = runWithGitFetchHeadLock(alias, undefined, async () => {
order.push('alias')
})
await new Promise((resolve) => setTimeout(resolve, 20))
expect(order).toEqual(['root'])
release()
await Promise.all([first, nestedRun, aliasRun])
expect(order[0]).toBe('root')
expect(new Set(order.slice(1))).toEqual(new Set(['nested', 'alias']))
} finally {
await rm(root, { recursive: true, force: true })
}
}
)
it('serializes linked worktrees through their shared Git directory', async () => {
const root = await mkdtemp(path.join(tmpdir(), 'fetch-head-linked-lock-'))
const main = path.join(root, 'main')
const linked = path.join(root, 'linked')
const commonGitDir = path.join(main, '.git')
const linkedGitDir = path.join(commonGitDir, 'worktrees', 'linked')
await Promise.all([mkdir(linkedGitDir, { recursive: true }), mkdir(linked)])
await Promise.all([
writeFile(path.join(linked, '.git'), `gitdir: ${linkedGitDir}\n`),
writeFile(path.join(linkedGitDir, 'commondir'), '../..\n')
])
let release!: () => void
const gate = new Promise<void>((resolve) => {
release = resolve
})
const order: string[] = []
const first = runWithGitFetchHeadLock(main, undefined, async () => {
order.push('main')
await gate
})
await vi.waitFor(() => expect(order).toEqual(['main']))
const second = runWithGitFetchHeadLock(linked, undefined, async () => {
order.push('linked')
})
try {
await new Promise((resolve) => setTimeout(resolve, 20))
expect(order).toEqual(['main'])
release()
await Promise.all([first, second])
expect(order).toEqual(['main', 'linked'])
} finally {
release()
await Promise.allSettled([first, second])
await rm(root, { recursive: true, force: true })
}
})
})