mirror of
https://github.com/stablyai/orca.git
synced 2026-11-06 03:57:35 +00:00
fix(ssh): a fast remote directory listing no longer reports a false failure (#26940)
ssh2 can deliver the exec ack and the exit status in one chunk, so 'exit' fires before the browse handler subscribes. Fall back to the status that 'close' repeats when no exit was observed. Co-authored-by: m4air <m4air@Mac.localdomain> Co-authored-by: suharvest <suharvest@users.noreply.github.com>
This commit is contained in:
@@ -80,6 +80,61 @@ describe('registerSshBrowseHandler', () => {
|
||||
expect(channel.stderr.listenerCount('error')).toBe(0)
|
||||
})
|
||||
|
||||
it('reads the status from close when exit fired before the channel was returned', async () => {
|
||||
const channel = createMockChannel()
|
||||
// ssh2 can parse exit-status in the same chunk as the exec ack, before listeners attach.
|
||||
const exec = vi
|
||||
.fn()
|
||||
.mockImplementationOnce(async () => {
|
||||
channel.emit('exit', 0)
|
||||
return channel
|
||||
})
|
||||
.mockRejectedValue(new Error('unexpected PowerShell fallback'))
|
||||
const getConnectionManager = () => ({
|
||||
getConnection: () => ({ exec })
|
||||
})
|
||||
registerSshBrowseHandler(getConnectionManager as never)
|
||||
|
||||
const resultPromise = handler(null, { targetId: 'ssh-1', dirPath: '~' })
|
||||
await Promise.resolve()
|
||||
channel.emit('data', Buffer.from('/home/user\nsrc/\n'))
|
||||
channel.emit('close', 0)
|
||||
|
||||
await expect(resultPromise).resolves.toEqual({
|
||||
resolvedPath: '/home/user',
|
||||
pathFlavor: 'posix',
|
||||
entries: [{ name: 'src', isDirectory: true }]
|
||||
})
|
||||
expect(exec).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps an observed exit status over the close status', async () => {
|
||||
const channel = createMockChannel()
|
||||
const fallback = createMockChannel()
|
||||
const exec = vi
|
||||
.fn()
|
||||
.mockResolvedValueOnce(channel)
|
||||
.mockImplementationOnce(async () => {
|
||||
fallback.emit('exit', 127)
|
||||
return fallback
|
||||
})
|
||||
const getConnectionManager = () => ({
|
||||
getConnection: () => ({ exec })
|
||||
})
|
||||
registerSshBrowseHandler(getConnectionManager as never)
|
||||
|
||||
const resultPromise = handler(null, { targetId: 'ssh-1', dirPath: '~' })
|
||||
const rejected = expect(resultPromise).rejects.toThrow('Remote listing failed (exit 7)')
|
||||
await Promise.resolve()
|
||||
channel.emit('data', Buffer.from('/home/user\n'))
|
||||
channel.emit('exit', 7)
|
||||
channel.emit('close', 0)
|
||||
await vi.waitFor(() => expect(fallback.listenerCount('close')).toBe(1))
|
||||
fallback.emit('close', 127)
|
||||
|
||||
await rejected
|
||||
})
|
||||
|
||||
it('escapes remote browse paths and reads the listing past rc-file stdout', async () => {
|
||||
const channel = createMockChannel()
|
||||
const exec = vi.fn().mockResolvedValue(channel)
|
||||
|
||||
@@ -118,6 +118,7 @@ async function runBrowseCommand(
|
||||
let stdout = ''
|
||||
let stderr = ''
|
||||
let exitCode: number | null = null
|
||||
let exitObserved = false
|
||||
let settled = false
|
||||
let timeout: ReturnType<typeof setTimeout> | null = null
|
||||
|
||||
@@ -175,12 +176,17 @@ async function runBrowseCommand(
|
||||
}
|
||||
// `exit` fires before `close`; capture the code to tell a failed `ls` (that still printed `pwd`) from an empty listing.
|
||||
const onExit = (code: number | null): void => {
|
||||
exitObserved = true
|
||||
exitCode = code
|
||||
}
|
||||
const onError = (error: Error): void => {
|
||||
rejectOnce(error)
|
||||
}
|
||||
const onClose = (): void => {
|
||||
const onClose = (code?: number | null): void => {
|
||||
// Why: ssh2 can emit `exit` before `exec` resolves; `close` repeats the status.
|
||||
if (!exitObserved && typeof code === 'number') {
|
||||
exitCode = code
|
||||
}
|
||||
stdout = fence?.readStdout(stdout) ?? stdout
|
||||
// Why: a null exitCode (channel closed without exit status) isn't success; don't treat empty stdout as an empty dir.
|
||||
if (exitCode !== 0) {
|
||||
|
||||
Reference in New Issue
Block a user