mirror of
https://github.com/stablyai/orca.git
synced 2026-09-21 16:02:20 +00:00
909 lines
32 KiB
TypeScript
909 lines
32 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { execFileMock, execFileSyncMock, spawnMock } = vi.hoisted(() => ({
|
|
execFileMock: vi.fn(),
|
|
execFileSyncMock: vi.fn(),
|
|
spawnMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execFile: execFileMock,
|
|
execFileSync: execFileSyncMock,
|
|
spawn: spawnMock
|
|
}))
|
|
vi.mock('../observability/instrumentation', () => ({
|
|
withGitSpan: (_attributes: unknown, run: () => unknown) => run()
|
|
}))
|
|
vi.mock('../diagnostics/main-thread-churn-probe', () => ({ recordSubprocessSpawn: vi.fn() }))
|
|
|
|
import { getBranchConflictKind } from './repo-branch-conflict'
|
|
import { pendingWslDirectGitReadEnvironment } from './command-runner/git-command-resolution'
|
|
import { gitExecFileAsync, gitSpawn, gitStreamStdout } from './runner'
|
|
import {
|
|
GitAdmissionScheduler,
|
|
_resetGitAdmissionForTests,
|
|
type GitAdmissionEvent
|
|
} from './command-runner/git-subprocess-admission'
|
|
import {
|
|
disableWslGitReadEnvironment,
|
|
getWslGitReadEnvironment,
|
|
peekWslGitReadEnvironment,
|
|
resetWslGitReadEnvironmentForTests,
|
|
seedWslGitReadEnvironmentForTests,
|
|
WSL_GIT_READ_ENVIRONMENT_WAIT_MS
|
|
} from './wsl-git-read-environment'
|
|
import {
|
|
prepareWslLinkedWorktreeGitRouting,
|
|
resetWslLinkedWorktreeGitRoutingForTests,
|
|
WSL_LINKED_WORKTREE_ROUTE_TTL_MS,
|
|
type WslLinkedWorktreeRoutingFileSystem
|
|
} from './wsl-linked-worktree-git-routing'
|
|
|
|
afterEach(() => _resetGitAdmissionForTests())
|
|
|
|
const DISTRO = 'Ubuntu'
|
|
const LOGIN_ENVIRONMENT = {
|
|
gitPath: '/home/user/bin/git',
|
|
home: '/home/user',
|
|
path: '/home/user/bin:/usr/bin:/bin'
|
|
}
|
|
|
|
/** Stand in for the guest shell: rc chatter first, then the payload inside the command's own fence. */
|
|
function fencedProbeStdout(command: unknown, payload: string): string {
|
|
const nonce = /__ORCA_WSL_CAPTURE_BEGIN_([^_]+)__/.exec(String(command))?.[1] ?? ''
|
|
return `profile banner\n__ORCA_WSL_CAPTURE_BEGIN_${nonce}__${payload}__ORCA_WSL_CAPTURE_END_${nonce}__`
|
|
}
|
|
|
|
const LOGIN_ENVIRONMENT_FIELDS = `${LOGIN_ENVIRONMENT.path}\0${LOGIN_ENVIRONMENT.gitPath}\0${LOGIN_ENVIRONMENT.home}`
|
|
|
|
type MockChild = EventEmitter & {
|
|
stdout: EventEmitter
|
|
stderr: EventEmitter
|
|
pid: number
|
|
kill: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
function createMockChild(): MockChild {
|
|
const child = new EventEmitter() as MockChild
|
|
child.stdout = new EventEmitter()
|
|
child.stderr = new EventEmitter()
|
|
child.pid = 1234
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
async function withPlatform<T>(platform: NodeJS.Platform, run: () => Promise<T>): Promise<T> {
|
|
const original = process.platform
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: platform })
|
|
try {
|
|
return await run()
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: original })
|
|
}
|
|
}
|
|
|
|
function succeedExecFile(stdout = 'ok'): void {
|
|
execFileMock.mockImplementation((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => callback?.(null, stdout, ''))
|
|
return child
|
|
})
|
|
}
|
|
|
|
describe('WSL direct Git reads', () => {
|
|
beforeEach(() => {
|
|
execFileMock.mockReset()
|
|
execFileSyncMock.mockReset()
|
|
spawnMock.mockReset()
|
|
resetWslGitReadEnvironmentForTests()
|
|
resetWslLinkedWorktreeGitRoutingForTests()
|
|
})
|
|
|
|
afterEach(() => {
|
|
resetWslGitReadEnvironmentForTests()
|
|
resetWslLinkedWorktreeGitRoutingForTests()
|
|
})
|
|
|
|
it('coalesces the login-shell environment probe per distro', async () => {
|
|
let completeProbe: ((error: Error | null, stdout: string, stderr: string) => void) | undefined
|
|
execFileMock.mockImplementation((_command, _args, _options, callback) => {
|
|
completeProbe = callback
|
|
return createMockChild()
|
|
})
|
|
|
|
const first = getWslGitReadEnvironment(DISTRO)
|
|
const second = getWslGitReadEnvironment(DISTRO)
|
|
expect(execFileMock).toHaveBeenCalledTimes(1)
|
|
completeProbe?.(
|
|
null,
|
|
fencedProbeStdout(
|
|
execFileMock.mock.calls[0]?.[1]?.[5],
|
|
'/home/user/bin:/usr/bin\0/home/user/bin/git\0/home/user'
|
|
),
|
|
''
|
|
)
|
|
|
|
await expect(Promise.all([first, second])).resolves.toEqual([
|
|
{ gitPath: '/home/user/bin/git', home: '/home/user', path: '/home/user/bin:/usr/bin' },
|
|
{ gitPath: '/home/user/bin/git', home: '/home/user', path: '/home/user/bin:/usr/bin' }
|
|
])
|
|
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
expect(execFileMock.mock.calls[0]?.[1]?.[5]).toContain('^GIT_')
|
|
})
|
|
|
|
it('retries a transient environment probe after a bounded delay', async () => {
|
|
let now = 1_000
|
|
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => now)
|
|
try {
|
|
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() =>
|
|
callback?.(Object.assign(new Error('timed out'), { code: 'ETIMEDOUT' }), '', '')
|
|
)
|
|
return child
|
|
})
|
|
|
|
await expect(getWslGitReadEnvironment(DISTRO)).resolves.toBeNull()
|
|
await expect(getWslGitReadEnvironment(DISTRO)).resolves.toBeNull()
|
|
expect(execFileMock).toHaveBeenCalledTimes(1)
|
|
|
|
now += 30_000
|
|
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() =>
|
|
callback?.(
|
|
null,
|
|
fencedProbeStdout(execFileMock.mock.calls.at(-1)?.[1]?.[5], LOGIN_ENVIRONMENT_FIELDS),
|
|
''
|
|
)
|
|
)
|
|
return child
|
|
})
|
|
|
|
await expect(getWslGitReadEnvironment(DISTRO)).resolves.toEqual(LOGIN_ENVIRONMENT)
|
|
expect(execFileMock).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('bounds settled environment entries during distro churn', () => {
|
|
for (let index = 0; index < 132; index += 1) {
|
|
seedWslGitReadEnvironmentForTests(`distro-${index}`, LOGIN_ENVIRONMENT)
|
|
}
|
|
expect(peekWslGitReadEnvironment('distro-0')).toBeUndefined()
|
|
expect(peekWslGitReadEnvironment('distro-131')).toEqual(LOGIN_ENVIRONMENT)
|
|
})
|
|
|
|
it('runs an opted-in read directly with translated cwd and arguments', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['status', '--short', String.raw`C:\repo\file.txt`], {
|
|
cwd: String.raw`C:\repo`,
|
|
env: { GIT_OPTIONAL_LOCKS: '0' },
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[0]).toBe('wsl.exe')
|
|
expect(execFileMock.mock.calls[0]?.[1]).toEqual([
|
|
'-d',
|
|
DISTRO,
|
|
'--exec',
|
|
'/usr/bin/env',
|
|
`PATH=${LOGIN_ENVIRONMENT.path}`,
|
|
`HOME=${LOGIN_ENVIRONMENT.home}`,
|
|
'LANGUAGE=en',
|
|
'LC_ALL=en_US.UTF-8',
|
|
'LANG=en_US.UTF-8',
|
|
'GIT_OPTIONAL_LOCKS=0',
|
|
LOGIN_ENVIRONMENT.gitPath,
|
|
'-C',
|
|
'/mnt/c/repo',
|
|
'status',
|
|
'--short',
|
|
'/mnt/c/repo/file.txt'
|
|
])
|
|
})
|
|
})
|
|
|
|
it('keeps the first read on the login shell while priming the fast path', async () => {
|
|
await withPlatform('win32', async () => {
|
|
let completeProbe: ((error: Error | null, stdout: string, stderr: string) => void) | undefined
|
|
execFileMock.mockImplementation((_command, args, _options, callback) => {
|
|
const child = createMockChild()
|
|
if ((args as string[])[5]?.includes('^GIT_')) {
|
|
completeProbe = callback
|
|
} else {
|
|
queueMicrotask(() => callback?.(null, 'ok', ''))
|
|
}
|
|
return child
|
|
})
|
|
|
|
const options = {
|
|
cwd: String.raw`C:\repo`,
|
|
preferWslDirectGit: true as const,
|
|
wslDistro: DISTRO
|
|
}
|
|
await gitExecFileAsync(['status', '--short'], options)
|
|
|
|
expect(execFileMock).toHaveBeenCalledTimes(2)
|
|
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
completeProbe?.(
|
|
null,
|
|
fencedProbeStdout(execFileMock.mock.calls[1]?.[1]?.[5], LOGIN_ENVIRONMENT_FIELDS),
|
|
''
|
|
)
|
|
await Promise.resolve()
|
|
await gitExecFileAsync(['status', '--short'], options)
|
|
expect(execFileMock.mock.calls[2]?.[1]).toContain('--exec')
|
|
})
|
|
})
|
|
|
|
it('keeps unopted commands on the WSL login shell', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['add', '--', 'file.txt'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
})
|
|
})
|
|
|
|
it('keeps reads with custom Git environment policy on the login shell', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
env: { GIT_CONFIG_GLOBAL: '/home/user/custom.gitconfig' },
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
})
|
|
})
|
|
|
|
it('fences parsed output from a barrier Git login-shell fallback', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
spawnMock.mockImplementation((_command, args) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
const capturedCommand = args?.find((arg) =>
|
|
String(arg).includes('__ORCA_WSL_CAPTURE_BEGIN_')
|
|
)
|
|
const fenced = fencedProbeStdout(capturedCommand, 'fork-point\n')
|
|
const echoedMarker = fenced.match(/__ORCA_WSL_CAPTURE_BEGIN_[^_]+__/)?.[0] ?? ''
|
|
child.stdout.emit('data', Buffer.from(`${echoedMarker}shell trace\n${fenced}`))
|
|
child.emit('close', 0, null)
|
|
})
|
|
return child
|
|
})
|
|
|
|
await expect(
|
|
gitExecFileAsync(['merge-base', '--fork-point', 'upstream/main', 'HEAD'], {
|
|
cwd: String.raw`C:\repo`,
|
|
env: { GIT_CONFIG_GLOBAL: '/home/user/custom.gitconfig' },
|
|
wslDistro: DISTRO,
|
|
captureWslLoginShellOutput: true,
|
|
terminationBarrier: true
|
|
})
|
|
).resolves.toEqual({ stdout: 'fork-point\n', stderr: '' })
|
|
|
|
expect(spawnMock.mock.calls[0]?.[1]?.join(' ')).toContain('setsid --wait')
|
|
expect(spawnMock.mock.calls[0]?.[1]?.join(' ')).toContain('__ORCA_WSL_CAPTURE_BEGIN_')
|
|
})
|
|
})
|
|
|
|
it('verifies the WSL guest process group before settling an abort', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
const command = createMockChild()
|
|
spawnMock.mockImplementation((_program, args) => {
|
|
if (spawnMock.mock.calls.length === 1) {
|
|
queueMicrotask(() => {
|
|
const marker = String(args?.join(' ')).match(
|
|
/(__ORCA_WSL_PROCESS_GROUP_[0-9a-f-]+__=)/
|
|
)?.[1]
|
|
command.stderr.emit('data', Buffer.from(`${marker}4321\n`))
|
|
})
|
|
return command
|
|
}
|
|
const terminator = createMockChild()
|
|
queueMicrotask(() => terminator.emit('close', 0, null))
|
|
return terminator
|
|
})
|
|
const controller = new AbortController()
|
|
const pending = gitExecFileAsync(['status'], {
|
|
cwd: String.raw`C:\repo`,
|
|
env: { GIT_CONFIG_GLOBAL: '/home/user/custom.gitconfig' },
|
|
wslDistro: DISTRO,
|
|
signal: controller.signal,
|
|
terminationBarrier: true
|
|
})
|
|
await vi.waitFor(() => expect(spawnMock).toHaveBeenCalledTimes(1))
|
|
await Promise.resolve()
|
|
|
|
controller.abort()
|
|
|
|
await expect(pending).rejects.toMatchObject({ name: 'AbortError' })
|
|
expect(command.kill).not.toHaveBeenCalled()
|
|
expect(spawnMock.mock.calls[1]?.[1]?.join(' ')).toContain('kill -TERM')
|
|
expect(spawnMock.mock.calls[1]?.[1]).toContain('4321')
|
|
})
|
|
})
|
|
|
|
it('ignores unchanged ambient host Git variables when selecting the direct path', async () => {
|
|
await withPlatform('win32', async () => {
|
|
const originalAskpass = process.env.GIT_ASKPASS
|
|
process.env.GIT_ASKPASS = String.raw`C:\host\askpass.exe`
|
|
try {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
env: { ...process.env, GIT_OPTIONAL_LOCKS: '0' },
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
} finally {
|
|
if (originalAskpass === undefined) {
|
|
delete process.env.GIT_ASKPASS
|
|
} else {
|
|
process.env.GIT_ASKPASS = originalAskpass
|
|
}
|
|
}
|
|
})
|
|
})
|
|
|
|
// A UNC worktree names its distro in the path, so a read there needs no resolved WSL project
|
|
// runtime to skip the shell -- requiring one is what kept every diff read on the login shell.
|
|
it('takes the direct read route for an override-less WSL UNC cwd', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`,
|
|
preferWslDirectGit: true
|
|
})
|
|
|
|
const resolved = execFileMock.mock.calls[0]?.[1] ?? []
|
|
expect(resolved).toContain('--exec')
|
|
expect(resolved).toContain(`PATH=${LOGIN_ENVIRONMENT.path}`)
|
|
expect(resolved).not.toContain('bash')
|
|
})
|
|
})
|
|
|
|
// The classifier already recognizes `show`, so the blob reads behind every diff take the
|
|
// direct route from the cwd alone -- no caller-supplied distro, no explicit opt-in.
|
|
it('takes the direct read route for an unclassified-caller blob read on a UNC cwd', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
})
|
|
})
|
|
|
|
// Kicking the probe off and resolving without it left the reads issued before it
|
|
// answered on the login shell -- the slow route, chosen by nothing but timing.
|
|
it('waits for a cold probe so the first read already skips the shell', async () => {
|
|
await withPlatform('win32', async () => {
|
|
execFileMock.mockImplementation((_command, args, _options, callback) => {
|
|
const child = createMockChild()
|
|
if (String(args).includes('_orca_git_path')) {
|
|
setTimeout(
|
|
() => callback?.(null, fencedProbeStdout(args, LOGIN_ENVIRONMENT_FIELDS), ''),
|
|
0
|
|
)
|
|
} else {
|
|
queueMicrotask(() => callback?.(null, 'ok', ''))
|
|
}
|
|
return child
|
|
})
|
|
|
|
await gitExecFileAsync(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`
|
|
})
|
|
|
|
expect(execFileMock.mock.calls.at(-1)?.[1]).toContain('--exec')
|
|
expect(execFileMock.mock.calls.at(-1)?.[1]).toContain(`PATH=${LOGIN_ENVIRONMENT.path}`)
|
|
})
|
|
})
|
|
|
|
it('stops waiting for a wedged probe and reads through the shell route', async () => {
|
|
await withPlatform('win32', async () => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
execFileMock.mockImplementation((_command, args, _options, callback) => {
|
|
const child = createMockChild()
|
|
// The probe never answers; only the git command itself does.
|
|
if (!String(args).includes('_orca_git_path')) {
|
|
queueMicrotask(() => callback?.(null, 'ok', ''))
|
|
}
|
|
return child
|
|
})
|
|
|
|
const pending = gitExecFileAsync(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`
|
|
})
|
|
await vi.advanceTimersByTimeAsync(WSL_GIT_READ_ENVIRONMENT_WAIT_MS)
|
|
await pending
|
|
|
|
// Exactly the routing this read had before the wait existed: an unanswered
|
|
// probe must cost the bound and nothing else.
|
|
const resolved = execFileMock.mock.calls.at(-1)?.[1] ?? []
|
|
expect(resolved.slice(3, 5)).toEqual(['bash', '-c'])
|
|
expect(resolved).not.toContain('/usr/bin/env')
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
})
|
|
|
|
// Once the route is disabled the answer is already known, so paying for a timer and two
|
|
// extra microtask hops on every later read is pure overhead on the host this PR targets.
|
|
it('stops deferring reads once the direct route is disabled for the distro', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
disableWslGitReadEnvironment(DISTRO)
|
|
|
|
expect(
|
|
pendingWslDirectGitReadEnvironment(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`
|
|
})
|
|
).toBeNull()
|
|
})
|
|
})
|
|
|
|
it('never waits on the probe for an already-aborted read', async () => {
|
|
await withPlatform('win32', async () => {
|
|
const controller = new AbortController()
|
|
controller.abort()
|
|
|
|
expect(
|
|
pendingWslDirectGitReadEnvironment(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`,
|
|
signal: controller.signal
|
|
})
|
|
).toBeNull()
|
|
expect(execFileMock).not.toHaveBeenCalled()
|
|
})
|
|
})
|
|
|
|
it('stops waiting for a cold probe as soon as the read aborts', async () => {
|
|
await withPlatform('win32', async () => {
|
|
vi.useFakeTimers()
|
|
try {
|
|
// The probe never answers, so only the abort can end the wait.
|
|
execFileMock.mockImplementation(() => createMockChild())
|
|
const controller = new AbortController()
|
|
|
|
const pending = pendingWslDirectGitReadEnvironment(['show', ':src/file.ts'], {
|
|
cwd: String.raw`\\wsl.localhost\Ubuntu\repo`,
|
|
signal: controller.signal
|
|
})
|
|
controller.abort()
|
|
|
|
await expect(pending).resolves.toBeNull()
|
|
} finally {
|
|
vi.useRealTimers()
|
|
}
|
|
})
|
|
})
|
|
|
|
it('waits for direct Git to close before retrying through the login shell', async () => {
|
|
await withPlatform('win32', async () => {
|
|
const admissionEvents: GitAdmissionEvent[] = []
|
|
_resetGitAdmissionForTests(
|
|
new GitAdmissionScheduler({ onAdmissionEvent: (event) => admissionEvents.push(event) })
|
|
)
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
let directChild!: ReturnType<typeof createMockChild>
|
|
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
directChild = child
|
|
queueMicrotask(() => {
|
|
callback?.(
|
|
Object.assign(new Error('exit 127'), { code: 127 }),
|
|
'',
|
|
'/usr/bin/env: No such file or directory'
|
|
)
|
|
})
|
|
return child
|
|
})
|
|
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
child.emit('close', 0, null)
|
|
callback?.(null, 'ok', '')
|
|
})
|
|
return child
|
|
})
|
|
|
|
const result = gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
await vi.waitFor(() => expect(directChild).toBeDefined())
|
|
await Promise.resolve()
|
|
expect(execFileMock).toHaveBeenCalledTimes(1)
|
|
directChild.emit('close', 127, null)
|
|
await expect(result).resolves.toEqual({ stdout: 'ok', stderr: '' })
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
expect(admissionEvents.map(({ phase, waiterId }) => [phase, waiterId])).toEqual([
|
|
['grant', 0],
|
|
['release', 0]
|
|
])
|
|
})
|
|
})
|
|
|
|
it('falls back and disables the fast path after another direct Git failure', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
execFileMock.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
callback?.(Object.assign(new Error('exit 128'), { code: 128 }), '', 'helper failed')
|
|
child.emit('close', 128, null)
|
|
})
|
|
return child
|
|
})
|
|
execFileMock.mockImplementation((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
callback?.(null, 'ok', '')
|
|
child.emit('close', 0, null)
|
|
})
|
|
return child
|
|
})
|
|
const options = {
|
|
cwd: String.raw`C:\repo`,
|
|
preferWslDirectGit: true as const,
|
|
wslDistro: DISTRO
|
|
}
|
|
|
|
await gitExecFileAsync(['status', '--short'], options)
|
|
await gitExecFileAsync(['status', '--short'], options)
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
expect(execFileMock.mock.calls[2]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
})
|
|
})
|
|
|
|
it('checks a missing branch conflict without retrying through a login shell', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
execFileMock.mockImplementation((_command, args: string[], _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
const missingRef = args.join(' ').includes('rev-parse')
|
|
const quiet = args.includes('--quiet')
|
|
const code = missingRef ? (quiet ? 1 : 128) : 0
|
|
callback?.(
|
|
code ? Object.assign(new Error('missing ref'), { code }) : null,
|
|
'',
|
|
missingRef && !quiet ? 'fatal: Needed a single revision' : ''
|
|
)
|
|
child.emit('close', code, null)
|
|
})
|
|
return child
|
|
})
|
|
|
|
await expect(
|
|
getBranchConflictKind(String.raw`\\wsl.localhost\Ubuntu\repo`, 'new-feature')
|
|
).resolves.toBeNull()
|
|
expect(execFileMock).toHaveBeenCalledTimes(2)
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--quiet')
|
|
})
|
|
})
|
|
|
|
it('keeps the fast path when direct and login Git both report an expected failure', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
execFileMock
|
|
.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
callback?.(Object.assign(new Error('exit 128'), { code: 128 }), '', 'missing ref')
|
|
child.emit('close', 128, null)
|
|
})
|
|
return child
|
|
})
|
|
.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
callback?.(Object.assign(new Error('exit 128'), { code: 128 }), '', 'missing ref')
|
|
child.emit('close', 128, null)
|
|
})
|
|
return child
|
|
})
|
|
.mockImplementationOnce((_command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
callback?.(null, 'ok', '')
|
|
child.emit('close', 0, null)
|
|
})
|
|
return child
|
|
})
|
|
const options = {
|
|
cwd: String.raw`C:\repo`,
|
|
preferWslDirectGit: true as const,
|
|
wslDistro: DISTRO
|
|
}
|
|
|
|
await expect(gitExecFileAsync(['rev-parse', '--verify', 'missing'], options)).rejects.toThrow(
|
|
'exit 128'
|
|
)
|
|
await gitExecFileAsync(['status', '--short'], options)
|
|
|
|
expect(execFileMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
expect(execFileMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
expect(execFileMock.mock.calls[2]?.[1]).toContain('--exec')
|
|
})
|
|
})
|
|
|
|
it('streams opted-in reads directly', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
spawnMock.mockImplementation(() => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
child.stdout.emit('data', Buffer.from('# branch.head main\n'))
|
|
child.emit('close', 0)
|
|
})
|
|
return child
|
|
})
|
|
const chunks: string[] = []
|
|
|
|
await gitStreamStdout(['status', '--porcelain=v2'], {
|
|
cwd: String.raw`C:\repo`,
|
|
onStdout: (chunk) => {
|
|
chunks.push(chunk)
|
|
},
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(chunks).toEqual(['# branch.head main\n'])
|
|
expect(spawnMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
})
|
|
})
|
|
|
|
it('retries a direct stream only when no stdout was delivered', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
spawnMock
|
|
.mockImplementationOnce(() => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
child.stderr.emit('data', Buffer.from('/usr/bin/env: No such file or directory'))
|
|
child.emit('close', 127)
|
|
})
|
|
return child
|
|
})
|
|
.mockImplementationOnce(() => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
child.stdout.emit('data', Buffer.from('# branch.head main\n'))
|
|
child.emit('close', 0)
|
|
})
|
|
return child
|
|
})
|
|
|
|
await gitStreamStdout(['status', '--porcelain=v2'], {
|
|
cwd: String.raw`C:\repo`,
|
|
onStdout: () => {},
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(spawnMock).toHaveBeenCalledTimes(2)
|
|
expect(spawnMock.mock.calls[0]?.[1]).toContain('--exec')
|
|
expect(spawnMock.mock.calls[1]?.[1]?.slice(3, 5)).toEqual(['sh', '-lc'])
|
|
})
|
|
})
|
|
|
|
it('does not retry a failed direct stream after delivering output', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
spawnMock.mockImplementation(() => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
child.stdout.emit('data', Buffer.from('partial'))
|
|
child.stderr.emit('data', Buffer.from('/usr/bin/env: No such file or directory'))
|
|
child.emit('close', 127)
|
|
})
|
|
return child
|
|
})
|
|
|
|
await expect(
|
|
gitStreamStdout(['status', '--porcelain=v2'], {
|
|
cwd: String.raw`C:\repo`,
|
|
onStdout: () => {},
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
).rejects.toThrow('git exited with 127')
|
|
expect(spawnMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
it('keeps network reads on the login shell even if opted in', async () => {
|
|
await withPlatform('win32', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['fetch', '--dry-run'], {
|
|
cwd: String.raw`C:\repo`,
|
|
preferWslDirectGit: true,
|
|
useConfiguredSshCommandForNetwork: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(
|
|
execFileMock.mock.calls.every((call) => call[1]?.slice(3, 5).join(' ') === 'sh -lc')
|
|
).toBe(true)
|
|
})
|
|
})
|
|
|
|
it('leaves non-Windows execution unchanged', async () => {
|
|
await withPlatform('linux', async () => {
|
|
seedWslGitReadEnvironmentForTests(DISTRO, LOGIN_ENVIRONMENT)
|
|
succeedExecFile()
|
|
|
|
await gitExecFileAsync(['status', '--short'], {
|
|
cwd: '/repo',
|
|
preferWslDirectGit: true,
|
|
wslDistro: DISTRO
|
|
})
|
|
|
|
expect(execFileMock.mock.calls[0]?.[0]).toBe('git')
|
|
})
|
|
})
|
|
|
|
it('keeps gitSpawn cache-only while async linked-worktree discovery is pending', async () => {
|
|
await withPlatform('win32', async () => {
|
|
let releaseStat: (() => void) | undefined
|
|
const delayedStat = new Promise<void>((resolve) => {
|
|
releaseStat = resolve
|
|
})
|
|
const fileSystem: WslLinkedWorktreeRoutingFileSystem = {
|
|
stat: vi.fn(async () => {
|
|
await delayedStat
|
|
return { isDirectory: () => false, isFile: () => true }
|
|
}),
|
|
readFile: vi.fn(async () => 'gitdir: C:/main/.git/worktrees/linked\n')
|
|
}
|
|
const pending = prepareWslLinkedWorktreeGitRouting(String.raw`C:\repo`, DISTRO, {
|
|
platform: 'win32',
|
|
fileSystem
|
|
})
|
|
spawnMock.mockReturnValue(createMockChild())
|
|
|
|
gitSpawn(['ls-files'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
|
|
expect(fileSystem.stat).toHaveBeenCalledTimes(1)
|
|
expect(spawnMock.mock.calls[0]?.[0]).toBe('wsl.exe')
|
|
releaseStat?.()
|
|
await expect(pending).resolves.toBe(true)
|
|
|
|
gitSpawn(['ls-files'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
expect(fileSystem.stat).toHaveBeenCalledTimes(1)
|
|
expect(spawnMock.mock.calls[1]?.[0]).toBe('git')
|
|
})
|
|
})
|
|
|
|
it('aborts an async Git call while linked-worktree discovery remains pending', async () => {
|
|
await withPlatform('win32', async () => {
|
|
let releaseStat: (() => void) | undefined
|
|
const delayedStat = new Promise<void>((resolve) => {
|
|
releaseStat = resolve
|
|
})
|
|
const fileSystem: WslLinkedWorktreeRoutingFileSystem = {
|
|
stat: vi.fn(async () => {
|
|
await delayedStat
|
|
return { isDirectory: () => false, isFile: () => true }
|
|
}),
|
|
readFile: vi.fn(async () => 'gitdir: C:/main/.git/worktrees/linked\n')
|
|
}
|
|
const discovery = prepareWslLinkedWorktreeGitRouting(String.raw`C:\repo`, DISTRO, {
|
|
platform: 'win32',
|
|
fileSystem
|
|
})
|
|
const controller = new AbortController()
|
|
|
|
const command = gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
signal: controller.signal
|
|
})
|
|
controller.abort()
|
|
|
|
await expect(command).rejects.toMatchObject({ name: 'AbortError' })
|
|
expect(execFileMock).not.toHaveBeenCalled()
|
|
releaseStat?.()
|
|
await expect(discovery).resolves.toBe(true)
|
|
})
|
|
})
|
|
|
|
it('keeps gitSpawn cache-only when a linked-worktree route expires', async () => {
|
|
await withPlatform('win32', async () => {
|
|
let currentTime = 1_000
|
|
const nowSpy = vi.spyOn(Date, 'now').mockImplementation(() => currentTime)
|
|
try {
|
|
const fileSystem: WslLinkedWorktreeRoutingFileSystem = {
|
|
stat: vi.fn(async () => ({ isDirectory: () => false, isFile: () => true })),
|
|
readFile: vi.fn(async () => 'gitdir: C:/main/.git/worktrees/linked\n')
|
|
}
|
|
await prepareWslLinkedWorktreeGitRouting(String.raw`C:\repo`, DISTRO, {
|
|
platform: 'win32',
|
|
fileSystem
|
|
})
|
|
spawnMock.mockReturnValue(createMockChild())
|
|
|
|
gitSpawn(['ls-files'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
currentTime += WSL_LINKED_WORKTREE_ROUTE_TTL_MS
|
|
gitSpawn(['ls-files'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
|
|
expect(spawnMock.mock.calls[0]?.[0]).toBe('git')
|
|
expect(spawnMock.mock.calls[1]?.[0]).toBe('wsl.exe')
|
|
expect(fileSystem.stat).toHaveBeenCalledTimes(1)
|
|
|
|
await prepareWslLinkedWorktreeGitRouting(String.raw`C:\repo`, DISTRO, {
|
|
platform: 'win32',
|
|
fileSystem
|
|
})
|
|
gitSpawn(['ls-files'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: DISTRO,
|
|
stdio: ['ignore', 'pipe', 'pipe']
|
|
})
|
|
expect(fileSystem.stat).toHaveBeenCalledTimes(2)
|
|
expect(spawnMock.mock.calls[2]?.[0]).toBe('git')
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
})
|
|
})
|