mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 16:03:53 +00:00
* fix(git): run WSL git reads without a shell WSL-routed git ran through the distro user's interactive login shell for one reason: to inherit their PATH. That shell also runs the distro's rc/motd and writes it to the stdout callers parse, which is why #10917 reports a shell banner breaking GitHub source detection. A shell-free route already existed (`--exec /usr/bin/env PATH=... git`, added for status reads in #13207) but it was opt-in, and only gitStatusReadOptionsForWorktree opted in. Every other read -- remote get-url, config --get, log, show, rev-parse -- took the login shell on every call, so the reported parse never benefited. Classify reads at the resolver instead of at each caller. A read needs nothing the login shell provides, so it takes the direct route without the caller asking. Writes and network operations stay on the login shell: they can depend on credential helpers and ssh-agent that the user's profile sets up. Subcommands that both read and write (config, remote, branch, submodule) require an explicit read flag before they qualify, so `config --get` goes direct while `config user.email x` does not. `git show` blob reads stop forcing the login shell and go direct too. The fence stays on that path: the login shell is still the fallback when the environment probe is cold or rejected, and a banner there would become file content. Behavior note: the first WSL read per distro now also warms the environment probe in the background, so a cold read spawns one extra wsl.exe. Subsequent reads start no shell at all. * fix(git): keep queried WSL remote reads on login shell * style: format WSL runner test * fix(git): match WSL read markers positionally The conditional read markers were matched anywhere after the subcommand, so a positional argument that happened to share a marker's name routed a write shell-free: `worktree remove list` read as a listing, and `submodule foreach status` as a status query -- the latter can run arbitrary commands, including network ones. Split the two kinds of marker apart. `config`/`branch` are flag-marked and still match anywhere; `remote`/`worktree`/`submodule` are action-marked and must match the first non-flag argument. The queried `remote show` rule and the `symbolic-ref` arity rule are unchanged. Neither case is reachable today -- Orca issues no `submodule foreach`, and the worktree paths are its own CLI, not git argv -- but the loose match was the shape of the defect, not those two instances. * test(git): pin read routing behind global options Writes hidden behind -c/-C/--git-dir must not reach the shell-free route, and an unparsed global form must fall back to the login shell rather than guess at the subcommand. Both directions fail safe. --------- Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com> Co-authored-by: Neil <4138956+nwparker@users.noreply.github.com>
104 lines
3.3 KiB
TypeScript
104 lines
3.3 KiB
TypeScript
import { EventEmitter } from 'node:events'
|
|
import type * as FsPromises from 'node:fs/promises'
|
|
import { afterEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { execFileMock, statMock, readFileMock } = vi.hoisted(() => ({
|
|
execFileMock: vi.fn(),
|
|
statMock: vi.fn(),
|
|
readFileMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('node:child_process', () => ({
|
|
execFile: execFileMock,
|
|
execFileSync: vi.fn(),
|
|
spawn: vi.fn()
|
|
}))
|
|
|
|
vi.mock('node:fs/promises', async (importOriginal) => ({
|
|
...(await importOriginal<typeof FsPromises>()),
|
|
stat: statMock,
|
|
readFile: readFileMock
|
|
}))
|
|
|
|
import { gitExecFileAsync } from './runner'
|
|
import {
|
|
resetWslLinkedWorktreeGitRoutingForTests,
|
|
WSL_LINKED_WORKTREE_ROUTE_PROBE_TIMEOUT_MS
|
|
} from './wsl-linked-worktree-git-routing'
|
|
|
|
function createMockChild(): EventEmitter & { pid: number; kill: ReturnType<typeof vi.fn> } {
|
|
const child = new EventEmitter() as EventEmitter & {
|
|
pid: number
|
|
kill: ReturnType<typeof vi.fn>
|
|
}
|
|
child.pid = 1234
|
|
child.kill = vi.fn()
|
|
return child
|
|
}
|
|
|
|
async function withWindowsPlatform(run: () => Promise<void>): Promise<void> {
|
|
const original = process.platform
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' })
|
|
try {
|
|
await run()
|
|
} finally {
|
|
Object.defineProperty(process, 'platform', { configurable: true, value: original })
|
|
}
|
|
}
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
vi.restoreAllMocks()
|
|
resetWslLinkedWorktreeGitRoutingForTests()
|
|
})
|
|
|
|
describe('WSL linked-worktree routing probe timeout', () => {
|
|
it('retries discovery on the next production Git call', async () => {
|
|
vi.useFakeTimers()
|
|
await withWindowsPlatform(async () => {
|
|
statMock
|
|
.mockImplementationOnce(() => new Promise(() => {}))
|
|
.mockResolvedValue({ isDirectory: () => false, isFile: () => true })
|
|
readFileMock.mockResolvedValue('gitdir: C:/main/.git/worktrees/linked\n')
|
|
execFileMock.mockImplementation((command, _args, _options, callback) => {
|
|
const child = createMockChild()
|
|
queueMicrotask(() => {
|
|
if (command === 'wsl.exe') {
|
|
callback?.(
|
|
Object.assign(new Error('not a git repository'), { code: 128 }),
|
|
'',
|
|
'fatal: not a git repository'
|
|
)
|
|
} else {
|
|
callback?.(null, 'host git recovered\n', '')
|
|
}
|
|
})
|
|
return child
|
|
})
|
|
|
|
const first = gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
const firstFailure = expect(first).rejects.toThrow('not a git repository')
|
|
await vi.advanceTimersByTimeAsync(WSL_LINKED_WORKTREE_ROUTE_PROBE_TIMEOUT_MS)
|
|
await firstFailure
|
|
|
|
await expect(
|
|
gitExecFileAsync(['status', '--short'], {
|
|
cwd: String.raw`C:\repo`,
|
|
wslDistro: 'Ubuntu'
|
|
})
|
|
).resolves.toEqual({ stdout: 'host git recovered\n', stderr: '' })
|
|
|
|
expect(statMock).toHaveBeenCalledTimes(2)
|
|
// A WSL read also warms the direct-git environment probe in the background;
|
|
// it is not part of the routing sequence under test.
|
|
const routedCommands = execFileMock.mock.calls
|
|
.filter(([, args]) => !String((args as string[])?.at(-1)).includes('^GIT_'))
|
|
.map(([command]) => command)
|
|
expect(routedCommands).toEqual(['wsl.exe', 'git'])
|
|
})
|
|
})
|
|
})
|