mirror of
https://github.com/stablyai/orca.git
synced 2026-11-02 18:17:36 +00:00
fix(ssh): two overlapping bridged-terminal attaches hand back the viewer they replace (#30856)
* fix(ssh): two overlapping bridged attaches hand back the viewer they replace Co-authored-by: Claude <noreply@anthropic.com> * fix(ssh): the newest bridged attach keeps its viewer even when an older one answers last Co-authored-by: Claude <noreply@anthropic.com> * fix(ssh): an older bridged connect still paints its screen when a newer attach keeps the viewer Co-authored-by: Claude <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -387,6 +387,48 @@ describe('a runtime pane bridged to an older relay’s terminal', () => {
|
||||
).toEqual(['ssh:ssh-box@@pty-3'])
|
||||
})
|
||||
|
||||
it('hands back every viewer when two attaches for the pane overlap', async () => {
|
||||
let next = 0
|
||||
bridge.attach.mockImplementation(async () => {
|
||||
next += 1
|
||||
return { status: 'attached', replay: '', acquisitionId: next }
|
||||
})
|
||||
const transport = createLegacyRelayBridgeTransport(PTY, innerTransport())
|
||||
// A connect and an attach for the same pane, both answered before either settles.
|
||||
const connecting = transport.connect({ url: '', sessionId: PTY, callbacks: callbacks() })
|
||||
transport.attach({ existingPtyId: PTY, callbacks: callbacks() })
|
||||
await connecting
|
||||
await vi.waitFor(() => expect(bridge.attach).toHaveBeenCalledTimes(2))
|
||||
await Promise.resolve()
|
||||
transport.detach?.()
|
||||
expect(bridge.detach.mock.calls.map(([args]) => args.acquisitionId).sort()).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('keeps the newest attach’s viewer when an older attach answers last', async () => {
|
||||
const answers: ((acquisitionId: number) => void)[] = []
|
||||
bridge.attach.mockImplementation(
|
||||
() =>
|
||||
new Promise((resolve) =>
|
||||
answers.push((acquisitionId) =>
|
||||
resolve({ status: 'attached', replay: `SCREEN-${acquisitionId}`, acquisitionId })
|
||||
)
|
||||
)
|
||||
)
|
||||
const transport = createLegacyRelayBridgeTransport(PTY, innerTransport())
|
||||
const older = transport.connect({ url: '', sessionId: PTY, callbacks: callbacks() })
|
||||
transport.attach({ existingPtyId: PTY, callbacks: callbacks() })
|
||||
await vi.waitFor(() => expect(answers).toHaveLength(2))
|
||||
// The newer attach answers first, then the older one.
|
||||
answers[1](2)
|
||||
await vi.waitFor(() => expect(transport.isConnected()).toBe(true))
|
||||
answers[0](1)
|
||||
// The connect still paints its screen: the attach that won delivers none of its own.
|
||||
await expect(older).resolves.toMatchObject({ replay: 'SCREEN-1' })
|
||||
expect(bridge.detach.mock.calls.map(([args]) => args.acquisitionId)).toEqual([1])
|
||||
transport.detach?.()
|
||||
expect(bridge.detach.mock.calls.map(([args]) => args.acquisitionId)).toEqual([1, 2])
|
||||
})
|
||||
|
||||
it('hands any other connect straight to the managed server’s transport', async () => {
|
||||
const inner = innerTransport()
|
||||
const transport = createLegacyRelayBridgeTransport(PTY, inner)
|
||||
|
||||
@@ -42,6 +42,23 @@ export function createLegacyRelayBridgeTransport(ptyId: string, inner: PtyTransp
|
||||
viewer = null
|
||||
}
|
||||
|
||||
/** Issue order of this pane's attaches, and which one the held viewer came from. */
|
||||
const seqs = { issued: 0, viewer: 0 }
|
||||
|
||||
// Overlapping attaches (connect, attach, reattach) each get a viewer; the newest issued keeps
|
||||
// its own, whichever answers last, and every other one is handed back.
|
||||
const adoptViewer = (seq: number, acquisitionId: number): void => {
|
||||
if (seq < seqs.viewer) {
|
||||
api?.detach({ ptyId, acquisitionId })
|
||||
return
|
||||
}
|
||||
if (viewer !== null && viewer !== acquisitionId) {
|
||||
api?.detach({ ptyId, acquisitionId: viewer })
|
||||
}
|
||||
viewer = acquisitionId
|
||||
seqs.viewer = seq
|
||||
}
|
||||
|
||||
const release = (): void => {
|
||||
epoch += 1
|
||||
unsubscribe.forEach((dispose) => dispose())
|
||||
@@ -85,6 +102,7 @@ export function createLegacyRelayBridgeTransport(ptyId: string, inner: PtyTransp
|
||||
}
|
||||
reattaching = true
|
||||
const at = epoch
|
||||
const seq = ++seqs.issued
|
||||
try {
|
||||
// The screen comes through onReplay in output order: applied from this reply instead, its
|
||||
// clear would erase output the relay printed after it.
|
||||
@@ -93,7 +111,7 @@ export function createLegacyRelayBridgeTransport(ptyId: string, inner: PtyTransp
|
||||
return
|
||||
}
|
||||
if (result.status === 'attached') {
|
||||
viewer = result.acquisitionId
|
||||
adoptViewer(seq, result.acquisitionId)
|
||||
clearUnreachable()
|
||||
} else if (result.status === 'exited' || result.status === 'not-bridged') {
|
||||
clearUnreachable()
|
||||
@@ -168,6 +186,7 @@ export function createLegacyRelayBridgeTransport(ptyId: string, inner: PtyTransp
|
||||
mode = 'bridge'
|
||||
subscribe()
|
||||
const at = epoch
|
||||
const seq = ++seqs.issued
|
||||
const result = await api.attach({ ptyId })
|
||||
if (!stillWanted(at, result)) {
|
||||
return undefined
|
||||
@@ -184,7 +203,7 @@ export function createLegacyRelayBridgeTransport(ptyId: string, inner: PtyTransp
|
||||
showUnreachable()
|
||||
return { id: ptyId, isReattach: true, replay: '' }
|
||||
}
|
||||
viewer = result.acquisitionId
|
||||
adoptViewer(seq, result.acquisitionId)
|
||||
return { id: ptyId, isReattach: true, replay: result.replay }
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user