mirror of
https://github.com/stablyai/orca.git
synced 2026-10-22 08:03:37 +00:00
fix(terminal): make pane recovery strictly best-effort in timer contexts
Recovery fires from stall-watch timers, replay-guard releases, and onData — contexts where a throw becomes an unhandled error (CI verify caught this: pty-connection.test.ts mocks a partial store, other tests advance fake timers past the stall window, and the certification path hit a missing remountTerminalTabForRecovery). Guard the store action call and the ptyIdsByTabId reads so a partial surface yields a false return, never a throw, and pin it with a regression test.
This commit is contained in:
@@ -3337,7 +3337,7 @@ export function connectPanePty(
|
||||
if (transport.isConnected() && transport.getPtyId() !== null) {
|
||||
return
|
||||
}
|
||||
const storePtyId = useAppStore.getState().ptyIdsByTabId[deps.tabId]?.[0] ?? null
|
||||
const storePtyId = useAppStore.getState().ptyIdsByTabId?.[deps.tabId]?.[0] ?? null
|
||||
void requestTerminalPaneRecovery({
|
||||
tabId: deps.tabId,
|
||||
ptyId: transport.getPtyId() ?? storePtyId,
|
||||
@@ -3351,7 +3351,7 @@ export function connectPanePty(
|
||||
const unregisterUndeliverableWriteHandler = registerUndeliverableWriteHandler(
|
||||
pane.terminal,
|
||||
(reason) => {
|
||||
const storePtyId = useAppStore.getState().ptyIdsByTabId[deps.tabId]?.[0] ?? null
|
||||
const storePtyId = useAppStore.getState().ptyIdsByTabId?.[deps.tabId]?.[0] ?? null
|
||||
void requestTerminalPaneRecovery({
|
||||
tabId: deps.tabId,
|
||||
ptyId: transport.getPtyId() ?? storePtyId,
|
||||
|
||||
@@ -149,6 +149,19 @@ describe('requestTerminalPaneRecovery', () => {
|
||||
expect(mocks.remountTerminalTabForRecovery).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('never throws when the store surface is partial (timer/callback contexts)', async () => {
|
||||
// Regression: recovery fires from stall-watch timers and write callbacks;
|
||||
// an environment with a partial store (mocked suites, teardown races) must
|
||||
// get a false return, not an unhandled TypeError.
|
||||
mocks.remountTerminalTabForRecovery.mockImplementation(() => {
|
||||
throw new TypeError('remountTerminalTabForRecovery is not a function')
|
||||
})
|
||||
|
||||
await expect(
|
||||
requestTerminalPaneRecovery({ tabId: 'tab-1', ptyId: 'pty-1', reason: 'write-stalled' })
|
||||
).resolves.toBe(false)
|
||||
})
|
||||
|
||||
it('does not consume budget when the tab no longer exists', async () => {
|
||||
mocks.remountTerminalTabForRecovery.mockReturnValue(false)
|
||||
|
||||
|
||||
@@ -78,7 +78,15 @@ export async function requestTerminalPaneRecovery(request: RecoveryRequest): Pro
|
||||
return false
|
||||
}
|
||||
}
|
||||
const remounted = useAppStore.getState().remountTerminalTabForRecovery(request.tabId)
|
||||
let remounted = false
|
||||
try {
|
||||
remounted = useAppStore.getState().remountTerminalTabForRecovery(request.tabId)
|
||||
} catch {
|
||||
// Why: recovery fires from timer and write-callback contexts (stall watch,
|
||||
// replay guard, onData) — it is best-effort by contract and must never
|
||||
// surface a throw there (partial store surfaces in tests, teardown races).
|
||||
return false
|
||||
}
|
||||
if (!remounted) {
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user