mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
* fix(ssh): recover instead of wedging when the relay channel dies mid-connect Three coupled defects made a dropped SSH relay look like a permanent bug: 1. SshRelaySession.establish()/reconnect() ran their last liveness gate before configureRelayGraceTime(), whose mux.notify() can dispose the mux synchronously (writer control-lane admission cap, or a throwing transport). The session then latched _state='ready' + _onReady (status bar "connected") while watchMuxForRelayLoss() silently no-op'd on the dead mux, so the bounded relay backoff in ipc/ssh.ts never ran and the fs/pty/git providers stayed registered against a dead multiplexer. Both sites now re-check mux.isDisposed() after the notify and take the existing failure path. 2. SshChannelMultiplexer.request()/notifyWithSettlement() always reported the permanent-shutdown string 'Multiplexer disposed' with no code, even when the recorded dispose reason was connection_lost. The reason is now recorded and a shared disposedError() factory serves dispose(), request(), notifyWithSettlement(), so a transient drop reports 'SSH connection lost, reconnecting...' / CONNECTION_LOST. onDispose() on an already-disposed mux now fires the handler synchronously with that reason instead of returning a silent no-op (without retaining it). 3. TerminalErrorToast no longer renders a transient relay drop in the red "please file an issue" style. The marker is matched with includes() because the message reaches the toast IPC-wrapped. ssh-git-response-stream-reader registers its onDispose subscriber after the abort wiring, since an already-dead mux now fails synchronously there and the cleanup must be able to drop the caller's abort listener. Closes #11953 * fix(ssh): treat a mux killed during PTY reattach as relay loss reconnect()'s post-reattach gate bare-returned when ownsAttempt() went false, and reattachKnownPtys swallows every per-PTY error, so a control-lane failure during a large reattach burst disposed the mux without ever reaching the catch: providers stayed bound to the dead mux, no relay-loss watcher was installed, and the session wedged in 'reconnecting' until restart. Take the failure path when our own mux is the one that died so ssh.ts's bounded backoff retries. Co-authored-by: Orca <help@stably.ai> * fix(ssh): recover when relay dies during setup instead of wedging Introduce verifyRelayAttempt() to detect mux disposal at each setup phase (consumer session, home resolution, provider registration, PTY reattach). Routes mid-setup connection loss into relay-loss recovery instead of hanging in reconnecting state. * Extract SSH disposal error factory Multiple sites were duplicating the disposal error creation logic with specific message and code values. The renderer uses these to distinguish temporary disconnects (show reconnection overlay) from permanent shutdown (show error toast), so all producers must use the same factory to avoid silent UI degradation. --------- Co-authored-by: Orca <help@stably.ai> Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
31 lines
1.3 KiB
TypeScript
31 lines
1.3 KiB
TypeScript
import { describe, expect, it, vi } from 'vitest'
|
|
import { requestGitStreamable } from './ssh-git-response-stream-reader'
|
|
import { SshChannelMultiplexer, type MultiplexerTransport } from './ssh-channel-multiplexer'
|
|
|
|
function createMockTransport(): MultiplexerTransport {
|
|
return {
|
|
write: () => {},
|
|
onData: () => {},
|
|
onClose: () => {}
|
|
}
|
|
}
|
|
|
|
describe('requestGitStreamable on an already-dead multiplexer', () => {
|
|
it('rejects as a transient relay loss and leaves no listener on the caller signal', async () => {
|
|
const mux = new SshChannelMultiplexer(createMockTransport())
|
|
mux.dispose('connection_lost')
|
|
const controller = new AbortController()
|
|
const addListener = vi.spyOn(controller.signal, 'addEventListener')
|
|
const removeListener = vi.spyOn(controller.signal, 'removeEventListener')
|
|
|
|
await expect(
|
|
requestGitStreamable(mux, 'git.status', { cwd: '/repo' }, { signal: controller.signal })
|
|
).rejects.toThrow('SSH connection lost, reconnecting...')
|
|
|
|
// #11953: a disposed mux fails synchronously inside onDispose, so the abort
|
|
// listener must already be registered when that cleanup runs — otherwise it
|
|
// outlives the request for the lifetime of the caller's signal.
|
|
expect(removeListener).toHaveBeenCalledTimes(addListener.mock.calls.length)
|
|
})
|
|
})
|