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:
Neil
2026-10-11 00:30:22 -07:00
committed by GitHub
co-authored by m4air suharvest
parent d56c0fbbb3
commit e3be586ef0
2 changed files with 62 additions and 1 deletions
+55
View File
@@ -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)
+7 -1
View File
@@ -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) {