mirror of
https://github.com/stablyai/orca.git
synced 2026-10-08 00:02:38 +00:00
Run Windows Git Bash foreground proof under bundled Bun
This commit is contained in:
@@ -1,125 +1,23 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import { afterAll, beforeAll, describe, expect, it, vi } from 'vitest'
|
||||
import { removeTreeSync } from '../../shared/windows-transient-lock-removal'
|
||||
import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell'
|
||||
import { confirmPtyShellForeground } from '../daemon/pty-subprocess/pty-shell-foreground-confirmation'
|
||||
import { createPtyShellLaunchPlan } from '../daemon/pty-subprocess/shell-launch-plan'
|
||||
import { spawnNativeDaemonPty } from '../daemon/pty-subprocess/native-pty-spawn'
|
||||
import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process'
|
||||
import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch'
|
||||
import { createDaemonPtyEnvironment } from '../daemon/pty-subprocess/spawn-environment'
|
||||
import type { PtySubprocessOptions } from '../daemon/pty-subprocess'
|
||||
import { isGitForWindowsBashLauncherPath } from '../git-bash'
|
||||
import { readWindowsPtyJobProcessIds } from './windows-pty-job-membership'
|
||||
import { describe, it } from 'vitest'
|
||||
import { runBundledBunFixture } from '../bundled-bun-test-execution'
|
||||
|
||||
const describeOnWindows = process.platform === 'win32' ? describe : describe.skip
|
||||
|
||||
describeOnWindows("Git Bash launcher shell proof with Orca's real launch", () => {
|
||||
let userData: string
|
||||
const previousUserData = process.env.ORCA_USER_DATA_PATH
|
||||
|
||||
beforeAll(() => {
|
||||
// The launch plan writes shell-ready wrappers under the user-data root.
|
||||
userData = mkdtempSync(join(tmpdir(), 'orca-git-bash-proof-'))
|
||||
process.env.ORCA_USER_DATA_PATH = userData
|
||||
})
|
||||
|
||||
afterAll(() => {
|
||||
if (previousUserData === undefined) {
|
||||
delete process.env.ORCA_USER_DATA_PATH
|
||||
} else {
|
||||
process.env.ORCA_USER_DATA_PATH = previousUserData
|
||||
}
|
||||
removeTreeSync(userData)
|
||||
})
|
||||
|
||||
it.each([
|
||||
['login shell', {}],
|
||||
// Why: with the Codex preflight set, the exec'd shell runs Orca's --rcfile wrapper.
|
||||
['rcfile wrapper', { ORCA_CODEX_LAUNCH_PREFLIGHT: 'C:\\orca-missing-preflight.exe' }]
|
||||
])(
|
||||
'confirms an idle %s prompt, refutes a running command, and confirms again',
|
||||
async (_label, extraEnv) => {
|
||||
const opts: PtySubprocessOptions = {
|
||||
sessionId: 'git-bash-shell-proof',
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
cwd: tmpdir(),
|
||||
shellOverride: WINDOWS_GIT_BASH_SHELL,
|
||||
env: extraEnv
|
||||
}
|
||||
const env = createDaemonPtyEnvironment(opts)
|
||||
const plan = await createPtyShellLaunchPlan(opts, env)
|
||||
expect(isGitForWindowsBashLauncherPath(plan.shellPath)).toBe(true)
|
||||
expect(plan.shellArgs.join(' ')).toContain('exec "$BASH"')
|
||||
const spawned = await spawnNativeDaemonPty(
|
||||
{ ...plan, env, cols: opts.cols, rows: opts.rows },
|
||||
{
|
||||
canUseBunPty,
|
||||
spawnBunPty: (args) =>
|
||||
spawnBunPty(args, {
|
||||
// Source tests use the TS worker; packaged hosts resolve their adjacent JS worker.
|
||||
createWindowsLaunch: (launch) =>
|
||||
createWindowsBunPtyLaunch(launch, {
|
||||
workerPath: join(
|
||||
__dirname,
|
||||
'../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts'
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
await runBundledBunFixture(
|
||||
join(__dirname, 'git-bash-foreground-bun-fixture.ts'),
|
||||
'verifyGitBashForeground',
|
||||
extraEnv,
|
||||
60_000
|
||||
)
|
||||
const proc = spawned.process
|
||||
let output = ''
|
||||
let dead = false
|
||||
proc.onData((chunk) => {
|
||||
output += chunk
|
||||
})
|
||||
proc.onExit(() => {
|
||||
dead = true
|
||||
})
|
||||
const confirm = (): Promise<boolean> =>
|
||||
confirmPtyShellForeground({
|
||||
process: proc,
|
||||
shellPath: spawned.shellPath,
|
||||
isDead: () => dead
|
||||
})
|
||||
try {
|
||||
await vi.waitFor(() => expect(output).toContain('$'), { timeout: 20_000 })
|
||||
// Launcher, exec stub, interactive bash: the shape that a size-1 or size-2 rule never matches.
|
||||
await vi.waitFor(() => expect(readWindowsPtyJobProcessIds(proc)?.size).toBe(3), {
|
||||
timeout: 5_000
|
||||
})
|
||||
await vi.waitFor(async () => expect(await confirm(), 'initial prompt').toBe(true), {
|
||||
timeout: 5_000
|
||||
})
|
||||
|
||||
// Interrupt only after the child is ready, not during a transient shell fork.
|
||||
proc.write(
|
||||
"node -e \"console.log(['ORCA','FOREGROUND_READY'].join('_')); setInterval(() => {}, 1000)\"\r"
|
||||
)
|
||||
await vi.waitFor(() => expect(output).toContain('ORCA_FOREGROUND_READY'), {
|
||||
timeout: 10_000
|
||||
})
|
||||
await vi.waitFor(async () => expect(await confirm()).toBe(false), { timeout: 10_000 })
|
||||
|
||||
proc.write('\x03')
|
||||
await vi.waitFor(
|
||||
async () => {
|
||||
expect(dead, 'terminal survived foreground interrupt').toBe(false)
|
||||
expect(await confirm(), 'prompt after interrupt').toBe(true)
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
|
||||
proc.write('sleep 60 &\r')
|
||||
await vi.waitFor(async () => expect(await confirm()).toBe(false), { timeout: 10_000 })
|
||||
} finally {
|
||||
proc.kill()
|
||||
}
|
||||
},
|
||||
60_000
|
||||
65_000
|
||||
)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,134 @@
|
||||
import { mkdtempSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join } from 'node:path'
|
||||
import assert from 'node:assert/strict'
|
||||
import { setTimeout as delay } from 'node:timers/promises'
|
||||
import { removeTreeSync } from '../../shared/windows-transient-lock-removal'
|
||||
import { WINDOWS_GIT_BASH_SHELL } from '../../shared/windows-terminal-shell'
|
||||
import { confirmPtyShellForeground } from '../daemon/pty-subprocess/pty-shell-foreground-confirmation'
|
||||
import { createPtyShellLaunchPlan } from '../daemon/pty-subprocess/shell-launch-plan'
|
||||
import { spawnNativeDaemonPty } from '../daemon/pty-subprocess/native-pty-spawn'
|
||||
import { canUseBunPty, spawnBunPty } from '../daemon/pty-subprocess/bun-pty-process'
|
||||
import { createWindowsBunPtyLaunch } from '../daemon/pty-subprocess/windows-bun-pty-launch'
|
||||
import { createDaemonPtyEnvironment } from '../daemon/pty-subprocess/spawn-environment'
|
||||
import type { PtySubprocessOptions } from '../daemon/pty-subprocess'
|
||||
import { isGitForWindowsBashLauncherPath } from '../git-bash'
|
||||
import { readWindowsPtyJobProcessIds } from './windows-pty-job-membership'
|
||||
|
||||
async function waitForAssertion(check: () => unknown, options: { timeout: number }): Promise<void> {
|
||||
const deadline = Date.now() + options.timeout
|
||||
for (;;) {
|
||||
try {
|
||||
await check()
|
||||
return
|
||||
} catch (error) {
|
||||
if (Date.now() >= deadline) {
|
||||
throw error
|
||||
}
|
||||
await delay(50)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyGitBashForeground(extraEnv: Record<string, string>): Promise<void> {
|
||||
assert.equal(process.platform, 'win32')
|
||||
assert(process.versions.bun)
|
||||
const previousUserData = process.env.ORCA_USER_DATA_PATH
|
||||
const userData = mkdtempSync(join(tmpdir(), 'orca-git-bash-proof-'))
|
||||
process.env.ORCA_USER_DATA_PATH = userData
|
||||
try {
|
||||
const opts: PtySubprocessOptions = {
|
||||
sessionId: 'git-bash-shell-proof',
|
||||
cols: 120,
|
||||
rows: 30,
|
||||
cwd: tmpdir(),
|
||||
shellOverride: WINDOWS_GIT_BASH_SHELL,
|
||||
env: extraEnv
|
||||
}
|
||||
const env = createDaemonPtyEnvironment(opts)
|
||||
const plan = await createPtyShellLaunchPlan(opts, env)
|
||||
assert(isGitForWindowsBashLauncherPath(plan.shellPath))
|
||||
assert(plan.shellArgs.join(' ').includes('exec "$BASH"'))
|
||||
const spawned = await spawnNativeDaemonPty(
|
||||
{ ...plan, env, cols: opts.cols, rows: opts.rows },
|
||||
{
|
||||
canUseBunPty,
|
||||
spawnBunPty: (args) =>
|
||||
spawnBunPty(args, {
|
||||
// Source tests use the TS worker; packaged hosts resolve their adjacent JS worker.
|
||||
createWindowsLaunch: (launch) =>
|
||||
createWindowsBunPtyLaunch(launch, {
|
||||
workerPath: join(
|
||||
__dirname,
|
||||
'../daemon/pty-subprocess/windows-bun-pty-gate-entry.ts'
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
)
|
||||
const proc = spawned.process
|
||||
let output = ''
|
||||
let dead = false
|
||||
proc.onData((chunk) => {
|
||||
output += chunk
|
||||
})
|
||||
const exited = new Promise<void>((resolve) => {
|
||||
proc.onExit(() => {
|
||||
dead = true
|
||||
resolve()
|
||||
})
|
||||
})
|
||||
const confirm = (): Promise<boolean> =>
|
||||
confirmPtyShellForeground({
|
||||
process: proc,
|
||||
shellPath: spawned.shellPath,
|
||||
isDead: () => dead
|
||||
})
|
||||
try {
|
||||
await waitForAssertion(() => assert(output.includes('$')), { timeout: 20_000 })
|
||||
// Launcher, exec stub, interactive bash: the shape that a size-1 or size-2 rule never matches.
|
||||
await waitForAssertion(() => assert.equal(readWindowsPtyJobProcessIds(proc)?.size, 3), {
|
||||
timeout: 5_000
|
||||
})
|
||||
await waitForAssertion(async () => assert.equal(await confirm(), true, 'initial prompt'), {
|
||||
timeout: 5_000
|
||||
})
|
||||
|
||||
// Interrupt only after the child is ready, not during a transient shell fork.
|
||||
proc.write(
|
||||
"node -e \"console.log(['ORCA','FOREGROUND_READY'].join('_')); setInterval(() => {}, 1000)\"\r"
|
||||
)
|
||||
await waitForAssertion(() => assert(output.includes('ORCA_FOREGROUND_READY')), {
|
||||
timeout: 10_000
|
||||
})
|
||||
await waitForAssertion(async () => assert.equal(await confirm(), false), { timeout: 10_000 })
|
||||
|
||||
proc.write('\x03')
|
||||
await waitForAssertion(
|
||||
async () => {
|
||||
assert.equal(dead, false, 'terminal survived foreground interrupt')
|
||||
assert.equal(await confirm(), true, 'prompt after interrupt')
|
||||
},
|
||||
{ timeout: 10_000 }
|
||||
)
|
||||
|
||||
proc.write('sleep 60 &\r')
|
||||
await waitForAssertion(async () => assert.equal(await confirm(), false), { timeout: 10_000 })
|
||||
} finally {
|
||||
proc.kill()
|
||||
await Promise.race([
|
||||
exited,
|
||||
delay(5_000, undefined, { ref: false }).then(() => {
|
||||
throw new Error('Git Bash fixture did not exit')
|
||||
})
|
||||
])
|
||||
}
|
||||
} finally {
|
||||
if (previousUserData === undefined) {
|
||||
delete process.env.ORCA_USER_DATA_PATH
|
||||
} else {
|
||||
process.env.ORCA_USER_DATA_PATH = previousUserData
|
||||
}
|
||||
removeTreeSync(userData)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user