mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 08:03:39 +00:00
fix(windows): stabilize interactive login fallback
Force service tests onto the no-console branch, preserve windowsHide for noninteractive Claude commands, and keep the change scoped to managed account sign-in.
This commit is contained in:
@@ -15,7 +15,6 @@ import {
|
||||
UnsafeWindowsBatchArgumentsError,
|
||||
WINDOWS_BATCH_UNSAFE_CHARACTERS_LABEL
|
||||
} from '../../shared/windows-batch-spawn'
|
||||
import { openWindowsInteractiveStdio } from '../../shared/windows-console-stdio'
|
||||
import { isSkillsCliAgentKeyShaped, toSkillsCliAgentKeys } from '../../shared/skills-cli-agent-keys'
|
||||
import {
|
||||
buildAgentFeatureSkillInstallArgs,
|
||||
@@ -144,14 +143,10 @@ function runNpxSkills(args: string[]): Promise<number> {
|
||||
// Why: npx is an `#!/usr/bin/env node` script, so resolving it off PATH is
|
||||
// not enough — without node alongside it the child exits 127 with no
|
||||
// 'error' event and the message below never fires.
|
||||
const consoleStdio = openWindowsInteractiveStdio()
|
||||
const child = spawn(spawnCmd, spawnArgs, {
|
||||
stdio: consoleStdio === 'inherit' ? 'inherit' : consoleStdio.stdio,
|
||||
stdio: 'inherit',
|
||||
env: { ...process.env, PATH: buildNpxPath(resolved) }
|
||||
})
|
||||
if (consoleStdio !== 'inherit') {
|
||||
consoleStdio.dispose()
|
||||
}
|
||||
// Why: a missing npx/Node on a headless host surfaces as a raw spawn ENOENT;
|
||||
// wrap it so the CLI reports an actionable message like every other failure here.
|
||||
child.once('error', (error) => {
|
||||
|
||||
@@ -517,7 +517,17 @@ describe('ClaudeAccountService credential capture', () => {
|
||||
const destroyStdin = vi.spyOn(child.stdin, 'destroy')
|
||||
const taskkill = new EventEmitter()
|
||||
const spawnMock = vi.fn((command: string) => (command === 'taskkill.exe' ? taskkill : child))
|
||||
const buildInteractiveLoginSpawn = vi.fn(() => ({
|
||||
command: getCmdExePath(),
|
||||
args: ['/d', '/c', 'start', '', '/wait', 'claude', 'auth', 'login', '--claudeai'],
|
||||
stdio: 'ignore' as const,
|
||||
windowsHide: true,
|
||||
dispose: vi.fn()
|
||||
}))
|
||||
vi.doMock('node:child_process', () => ({ spawn: spawnMock }))
|
||||
vi.doMock('../../shared/windows-interactive-login-spawn', () => ({
|
||||
buildWindowsHostInteractiveLoginSpawn: buildInteractiveLoginSpawn
|
||||
}))
|
||||
|
||||
try {
|
||||
const { ClaudeAccountService } = await import('./service')
|
||||
@@ -549,6 +559,11 @@ describe('ClaudeAccountService credential capture', () => {
|
||||
|
||||
const addPromise = service.addAccount()
|
||||
await vi.waitFor(() => {
|
||||
expect(buildInteractiveLoginSpawn).toHaveBeenCalledWith('claude', [
|
||||
'auth',
|
||||
'login',
|
||||
'--claudeai'
|
||||
])
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
getCmdExePath(),
|
||||
['/d', '/c', 'start', '', '/wait', 'claude', 'auth', 'login', '--claudeai'],
|
||||
@@ -571,6 +586,7 @@ describe('ClaudeAccountService credential capture', () => {
|
||||
expect(service.cancelPendingLogin()).toBe(false)
|
||||
} finally {
|
||||
vi.doUnmock('node:child_process')
|
||||
vi.doUnmock('../../shared/windows-interactive-login-spawn')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -35,7 +35,27 @@ describe('Claude Windows host interactive login', () => {
|
||||
queueMicrotask(() => child.emit('exit', 0))
|
||||
return child
|
||||
})
|
||||
const buildInteractiveLoginSpawn = vi.fn(() => ({
|
||||
command: getCmdExePath(),
|
||||
args: [
|
||||
'/d',
|
||||
'/c',
|
||||
'start',
|
||||
'',
|
||||
'/wait',
|
||||
'C:\\Tools\\claude.cmd',
|
||||
'auth',
|
||||
'login',
|
||||
'--claudeai'
|
||||
],
|
||||
stdio: 'ignore' as const,
|
||||
windowsHide: true,
|
||||
dispose: vi.fn()
|
||||
}))
|
||||
vi.doMock('node:child_process', () => ({ spawn: spawnMock }))
|
||||
vi.doMock('../../shared/windows-interactive-login-spawn', () => ({
|
||||
buildWindowsHostInteractiveLoginSpawn: buildInteractiveLoginSpawn
|
||||
}))
|
||||
|
||||
try {
|
||||
const { ClaudeAccountService } = await import('./service')
|
||||
@@ -58,6 +78,11 @@ describe('Claude Windows host interactive login', () => {
|
||||
1000
|
||||
)
|
||||
|
||||
expect(buildInteractiveLoginSpawn).toHaveBeenCalledWith('C:\\Tools\\claude.cmd', [
|
||||
'auth',
|
||||
'login',
|
||||
'--claudeai'
|
||||
])
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
getCmdExePath(),
|
||||
expect.arrayContaining(['start', '', '/wait']),
|
||||
@@ -69,6 +94,7 @@ describe('Claude Windows host interactive login', () => {
|
||||
expect(child.kill).not.toHaveBeenCalled()
|
||||
} finally {
|
||||
vi.doUnmock('node:child_process')
|
||||
vi.doUnmock('../../shared/windows-interactive-login-spawn')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1106,11 +1106,10 @@ export class ClaudeAccountService {
|
||||
? interactiveLogin.stdio
|
||||
: [options?.keepStdinOpen ? 'pipe' : 'ignore', 'pipe', 'pipe'],
|
||||
shell: spawnConfig.shell,
|
||||
// On Windows this is wsl.exe or a cmd invocation; without the flag it
|
||||
// opens a console and steals foreground on every managed login (#10488).
|
||||
windowsHide: true,
|
||||
// Hide noninteractive commands and the fallback wrapper; direct console
|
||||
// login handles need the owning window to remain visible.
|
||||
windowsHide: interactiveLogin?.windowsHide ?? true,
|
||||
windowsVerbatimArguments: spawnConfig.windowsVerbatimArguments,
|
||||
windowsHide: interactiveLogin?.windowsHide,
|
||||
env: spawnConfig.env,
|
||||
// Why: Claude auth can leave browser/login descendants alive after denial.
|
||||
// A process group lets cancellation terminate the whole POSIX login tree.
|
||||
|
||||
@@ -46,6 +46,13 @@ describe('Codex Windows host interactive login', () => {
|
||||
queueMicrotask(() => child.emit('close', 0))
|
||||
return child
|
||||
})
|
||||
const buildInteractiveLoginSpawn = vi.fn(() => ({
|
||||
command: getCmdExePath(),
|
||||
args: ['/d', '/c', 'start', '', '/wait', 'C:\\Tools\\codex.cmd', 'login'],
|
||||
stdio: 'ignore' as const,
|
||||
windowsHide: true,
|
||||
dispose: vi.fn()
|
||||
}))
|
||||
vi.doMock('node:child_process', () => ({
|
||||
execFileSync: vi.fn(),
|
||||
spawn: spawnMock
|
||||
@@ -53,6 +60,9 @@ describe('Codex Windows host interactive login', () => {
|
||||
vi.doMock('../codex-cli/command', () => ({
|
||||
resolveCodexCommand: () => 'C:\\Tools\\codex.cmd'
|
||||
}))
|
||||
vi.doMock('../../shared/windows-interactive-login-spawn', () => ({
|
||||
buildWindowsHostInteractiveLoginSpawn: buildInteractiveLoginSpawn
|
||||
}))
|
||||
|
||||
try {
|
||||
const { CodexAccountService } = await import('./service')
|
||||
@@ -67,6 +77,7 @@ describe('Codex Windows host interactive login', () => {
|
||||
}
|
||||
).runCodexLogin(testState.fakeHomeDir)
|
||||
|
||||
expect(buildInteractiveLoginSpawn).toHaveBeenCalledWith('C:\\Tools\\codex.cmd', ['login'])
|
||||
expect(spawnMock).toHaveBeenCalledWith(
|
||||
getCmdExePath(),
|
||||
expect.arrayContaining(['start', '', '/wait']),
|
||||
@@ -80,6 +91,7 @@ describe('Codex Windows host interactive login', () => {
|
||||
Object.defineProperty(process, 'platform', originalPlatform)
|
||||
vi.doUnmock('node:child_process')
|
||||
vi.doUnmock('../codex-cli/command')
|
||||
vi.doUnmock('../../shared/windows-interactive-login-spawn')
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user