mirror of
https://github.com/stablyai/orca.git
synced 2026-10-06 16:02:25 +00:00
fix(cursor): resume exact conversation after startup status replay (#24670)
* fix(cursor): restore exact conversation after startup snapshot * fix(terminal): preserve ready snapshot reattach and isolate bridge fixtures * test(cursor): seed the real startup bridge after module resets * test(terminal): settle startup snapshots in remote restore fixtures Signed-off-by: Neil <neil@stably.ai> --------- Signed-off-by: Neil <neil@stably.ai>
This commit is contained in:
@@ -27,6 +27,19 @@ afterEach(() => {
|
||||
})
|
||||
|
||||
describe('Cursor hook normalization', () => {
|
||||
it('publishes the conversation identity through the owning hook store', () => {
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
buildBody({
|
||||
hook_event_name: 'beforeSubmitPrompt',
|
||||
prompt: 'remember the codeword',
|
||||
conversation_id: 'conversation-742'
|
||||
}),
|
||||
'production'
|
||||
)
|
||||
expect(result?.providerSession).toEqual({ key: 'conversation_id', id: 'conversation-742' })
|
||||
})
|
||||
|
||||
it('beforeSubmitPrompt maps to working and captures the prompt', () => {
|
||||
const result = _internals.normalizeHookPayload(
|
||||
'cursor',
|
||||
|
||||
+2
-2
@@ -225,14 +225,14 @@ function observeFinalPaneState(drive: SshPaneDrive): {
|
||||
}
|
||||
|
||||
describe('direct-SSH hidden-output restore abandonment', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
// The reporter's client is WSL2, but the banner path is platform-independent;
|
||||
// pin darwin so the Windows synchronized-output scan stays out of the writes.
|
||||
;(window.api.platform as unknown as Record<string, unknown>).get = vi.fn(() => ({
|
||||
|
||||
+2
-2
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -141,14 +141,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
// A process-lifetime producer (OpenCode 2 `opencode run`) posts the run's Done after the
|
||||
// command-finished fact; the exited-agent drop must keep it, as it keeps a late hook Done.
|
||||
describe('command-finished drop and a Done that lands after it', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -138,14 +138,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('deliberate sleep keeps mounted panes cold', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -136,14 +136,14 @@ function createDirectSshSplitRetryCommit() {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -138,14 +138,14 @@ function createDirectSshSplitRetryCommit() {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -140,14 +140,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty process-exit retirement', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -138,14 +138,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -135,14 +135,14 @@ function expectNoGlobalAtlasRecovery(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -146,14 +146,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -137,14 +137,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -141,14 +141,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -149,14 +149,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -134,14 +134,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
// Why: activeRuntimeEnvironmentId exercises the remote-runtime path where the renderer still owns OSC 9999 status.
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -135,14 +135,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
|
||||
// Why: xterm and the pane mirror must end every restore on the same kitty flags.
|
||||
describe('connectPanePty kitty keyboard restore', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -145,14 +145,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -128,14 +128,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -137,14 +137,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -70,13 +70,13 @@ vi.mock('./pty-transport', () => ({
|
||||
}))
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -164,14 +164,14 @@ function installSleepingCodexResumeState(restoredPtyId?: string) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -106,14 +106,14 @@ function spawnOnConnect(transport: MockTransport, ptyId: string): void {
|
||||
// force-park); spending it on a later respawn drops a command queued after the first launch
|
||||
// (STA-4876).
|
||||
describe('connectPanePty queued startup consume', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -157,14 +157,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -140,14 +140,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -183,14 +183,14 @@ async function connectRemotePane(): Promise<{
|
||||
}
|
||||
|
||||
describe('pushed remote snapshot replay grid', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -135,14 +135,14 @@ function expectNoGlobalAtlasRecovery(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -155,14 +155,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -117,14 +117,14 @@ vi.mock('./pty-dispatcher', async (importOriginal) => {
|
||||
const BLANK_MODEL_IMAGE = '\x1b[0m\x1b[?25h\x1b[?7h'
|
||||
|
||||
describe('restored snapshot baseline shortfall (STA-5179)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -131,14 +131,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -158,14 +158,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -120,14 +120,14 @@ vi.mock('./pty-dispatcher', async (importOriginal) => {
|
||||
})
|
||||
|
||||
describe('fresh spawn leaves a local pane unbound', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -79,13 +79,13 @@ vi.mock('./pty-transport', () => ({
|
||||
}))
|
||||
|
||||
describe('connectPanePty split cwd resolution', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -112,10 +112,10 @@ function connectRestoredSshPane(): {
|
||||
}
|
||||
|
||||
describe('restored SSH pane input typed while its reattach is in flight', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks()
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
seedRestoredSshTab()
|
||||
})
|
||||
|
||||
|
||||
+2
-2
@@ -133,14 +133,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -136,14 +136,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -132,14 +132,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -147,14 +147,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -139,14 +139,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import { vi } from 'vitest'
|
||||
import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery'
|
||||
import * as agentStatusStartupSnapshot from '@/hooks/ipc-events/agent-status-startup-snapshot'
|
||||
import { drainFakeTimerWork, flushAsyncTicks } from './pty-connection-test-async'
|
||||
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
|
||||
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
|
||||
const originalDocument = globalThis.document
|
||||
type StatusSnapshot = ReturnType<
|
||||
typeof agentStatusStartupSnapshot.registerAgentStatusStartupSnapshot
|
||||
>
|
||||
let statusSnapshot: StatusSnapshot | undefined
|
||||
let resetStatusSnapshot: StatusSnapshot | undefined
|
||||
|
||||
export function buildAgentStatusModuleMock(
|
||||
actual: Record<string, unknown>
|
||||
@@ -29,7 +35,16 @@ export function buildAgentStatusModuleMock(
|
||||
}
|
||||
}
|
||||
|
||||
export function installTerminalTestGlobals(): void {
|
||||
export async function installTerminalTestGlobals(): Promise<void> {
|
||||
statusSnapshot?.dispose()
|
||||
resetStatusSnapshot?.dispose()
|
||||
statusSnapshot = agentStatusStartupSnapshot.registerAgentStatusStartupSnapshot()
|
||||
statusSnapshot.settle()
|
||||
resetStatusSnapshot = undefined
|
||||
// Import after resetModules so the seeded store and connection share one bridge.
|
||||
const actual = await import('@/hooks/ipc-events/agent-status-startup-snapshot')
|
||||
resetStatusSnapshot = actual.registerAgentStatusStartupSnapshot()
|
||||
resetStatusSnapshot.settle()
|
||||
;(globalThis as unknown as { window: unknown }).window = {
|
||||
api: {
|
||||
ssh: {
|
||||
@@ -107,6 +122,10 @@ export async function restoreTerminalTestGlobals(): Promise<void> {
|
||||
// continuation throws `ReferenceError: window is not defined` and fails the
|
||||
// whole file (orca#14728, CI-only because it needs a slow enough tick).
|
||||
await flushAsyncTicks(20)
|
||||
statusSnapshot?.dispose()
|
||||
resetStatusSnapshot?.dispose()
|
||||
statusSnapshot = undefined
|
||||
resetStatusSnapshot = undefined
|
||||
vi.restoreAllMocks()
|
||||
if (originalRequestAnimationFrame) {
|
||||
globalThis.requestAnimationFrame = originalRequestAnimationFrame
|
||||
|
||||
+2
-2
@@ -139,14 +139,14 @@ function enableActiveRuntimeEnvironment(environmentId = 'env-1'): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -130,14 +130,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('command-finished cleanup when the shell check cannot answer', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -129,14 +129,14 @@ function createDeps(overrides: Record<string, unknown> = {}) {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -155,13 +155,13 @@ async function connectVisibleRemotePane(): Promise<{
|
||||
}
|
||||
|
||||
describe('remote pane output pause vs visibility', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -138,14 +138,14 @@ function expectNoGlobalAtlasRecovery(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+2
-2
@@ -142,14 +142,14 @@ function notifyStoreSubscribers(): void {
|
||||
}
|
||||
|
||||
describe('connectPanePty', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
+43
@@ -5,11 +5,54 @@ import { toProcessExitStartup } from './process-exit-startup'
|
||||
import { recoverUnverifiableDirectSshReattach } from './direct-ssh-reattach-recovery'
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
import {
|
||||
awaitAgentStatusStartupSnapshot,
|
||||
isAgentStatusStartupSnapshotReady
|
||||
} from '@/hooks/ipc-events/agent-status-startup-snapshot'
|
||||
|
||||
const PANE_OWNER_UNVERIFIED_ERROR = 'terminal_pane_owner_unverified'
|
||||
|
||||
export function startDeferredSessionReattach(
|
||||
session: ConnectPanePtySession,
|
||||
deferredReattachSessionId: string
|
||||
): void {
|
||||
void prepareDeferredSessionReattach(session, deferredReattachSessionId).catch(
|
||||
(error: unknown) => {
|
||||
if (!session.disposed) {
|
||||
session.reportError(error instanceof Error ? error.message : String(error))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
async function prepareDeferredSessionReattach(
|
||||
session: ConnectPanePtySession,
|
||||
deferredReattachSessionId: string
|
||||
): Promise<void> {
|
||||
const generation = session.transportStreamGeneration
|
||||
let waitedForSnapshot = false
|
||||
if (
|
||||
!session.runtimeEnvironmentId &&
|
||||
!session.buildColdRestoreAgentResumeStartup() &&
|
||||
!isAgentStatusStartupSnapshotReady()
|
||||
) {
|
||||
waitedForSnapshot = true
|
||||
await awaitAgentStatusStartupSnapshot()
|
||||
}
|
||||
if (
|
||||
session.disposed ||
|
||||
generation !== session.transportStreamGeneration ||
|
||||
(waitedForSnapshot &&
|
||||
session.deps.paneTransportsRef.current.get(session.pane.id) !== session.transport)
|
||||
) {
|
||||
return
|
||||
}
|
||||
connectDeferredSessionReattach(session, deferredReattachSessionId)
|
||||
}
|
||||
|
||||
function connectDeferredSessionReattach(
|
||||
session: ConnectPanePtySession,
|
||||
deferredReattachSessionId: string
|
||||
): void {
|
||||
session.allowInitialIdleCacheSeed = true
|
||||
recordPtyConnectDiagnostic(`pane=${session.pane.id} -> REATTACH ${deferredReattachSessionId}`)
|
||||
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { registerAgentStatusStartupSnapshot } from '@/hooks/ipc-events/agent-status-startup-snapshot'
|
||||
import { startDeferredSessionReattach } from './deferred-session-reattach-connect'
|
||||
import type { ConnectPanePtySession } from './connect-pane-pty-session'
|
||||
|
||||
vi.mock('../terminal-lifecycle-diagnostics', () => ({ warnTerminalLifecycleAnomaly: vi.fn() }))
|
||||
vi.mock('./direct-ssh-reattach-recovery', () => ({ recoverUnverifiableDirectSshReattach: vi.fn() }))
|
||||
vi.mock('./paired-parked-terminal-restore', () => ({ isRemoteRuntimePtyId: () => false }))
|
||||
vi.mock('./pty-connect-limits', () => ({
|
||||
recordPtyConnectDiagnostic: vi.fn(),
|
||||
isSshSessionGoneError: () => false
|
||||
}))
|
||||
|
||||
function createSession() {
|
||||
let startup: {
|
||||
command: string
|
||||
agent: string
|
||||
resumeProviderSession: { key: string; id: string }
|
||||
} | null = null
|
||||
const transport = { connect: vi.fn().mockResolvedValue(null), getPtyId: () => 'restored-pty' }
|
||||
const fixture = {
|
||||
pane: { id: 1 },
|
||||
transport,
|
||||
disposed: false,
|
||||
transportStreamGeneration: 1,
|
||||
deps: { paneTransportsRef: { current: new Map([[1, transport]]) } },
|
||||
buildColdRestoreAgentResumeStartup: () => startup,
|
||||
prepaintParkedSshSnapshot: vi.fn(),
|
||||
captureTransportOutputCallbacks: () => ({ generation: 1, callbacks: {} }),
|
||||
beginReattachLiveDataDeferral: vi.fn(),
|
||||
finishReattachLiveDataDeferral: vi.fn(),
|
||||
shouldDeclareHiddenAtSpawn: () => true,
|
||||
handleReattachResult: vi.fn().mockResolvedValue(true),
|
||||
armDirectSshPaneRetryTimeout: vi.fn(),
|
||||
reportError: vi.fn(),
|
||||
isCapturedDirectSshReattachCurrent: () => true,
|
||||
cols: 80,
|
||||
rows: 24,
|
||||
cacheKey: 'tab:leaf'
|
||||
}
|
||||
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: This fixture provides every session field read by deferred reattach; unrelated installers are not exercised.
|
||||
const session = fixture as unknown as ConnectPanePtySession
|
||||
return {
|
||||
fixture,
|
||||
session,
|
||||
transport,
|
||||
replay: () => {
|
||||
startup = {
|
||||
command: "cursor-agent '--resume' 'conversation-742'",
|
||||
agent: 'cursor',
|
||||
resumeProviderSession: { key: 'conversation_id', id: 'conversation-742' }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
describe('cold reattach waits for host identity replay', () => {
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers()
|
||||
vi.stubGlobal('window', {
|
||||
api: { pty: { declarePendingPaneSerializer: vi.fn().mockResolvedValue(null) } }
|
||||
})
|
||||
})
|
||||
afterEach(() => {
|
||||
vi.unstubAllGlobals()
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('connects synchronously when host replay already applied before pane registration', () => {
|
||||
const bridge = registerAgentStatusStartupSnapshot()
|
||||
bridge.settle()
|
||||
const { session, fixture, transport } = createSession()
|
||||
fixture.deps.paneTransportsRef.current.clear()
|
||||
startDeferredSessionReattach(session, 'live-pty')
|
||||
expect(transport.connect).toHaveBeenCalledOnce()
|
||||
fixture.deps.paneTransportsRef.current.set(1, transport)
|
||||
bridge.dispose()
|
||||
})
|
||||
|
||||
it('connects once with the exact conversation after the initial snapshot applies', async () => {
|
||||
const bridge = registerAgentStatusStartupSnapshot()
|
||||
const { session, transport, replay } = createSession()
|
||||
startDeferredSessionReattach(session, 'lost-pty')
|
||||
expect(transport.connect).not.toHaveBeenCalled()
|
||||
replay()
|
||||
bridge.settle()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(transport.connect).toHaveBeenCalledOnce()
|
||||
expect(transport.connect).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
command: "cursor-agent '--resume' 'conversation-742'",
|
||||
resumeProviderSession: { key: 'conversation_id', id: 'conversation-742' },
|
||||
initiallyHidden: true
|
||||
})
|
||||
)
|
||||
bridge.dispose()
|
||||
})
|
||||
|
||||
it.each(['disposed', 'replaced', 'generation'] as const)(
|
||||
'never connects an obsolete %s pane after waiting',
|
||||
async (kind) => {
|
||||
const bridge = registerAgentStatusStartupSnapshot()
|
||||
const { session, fixture, transport } = createSession()
|
||||
startDeferredSessionReattach(session, 'lost-pty')
|
||||
if (kind === 'disposed') {
|
||||
fixture.disposed = true
|
||||
}
|
||||
if (kind === 'replaced') {
|
||||
fixture.deps.paneTransportsRef.current.delete(1)
|
||||
}
|
||||
if (kind === 'generation') {
|
||||
fixture.transportStreamGeneration += 1
|
||||
}
|
||||
bridge.settle()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(transport.connect).not.toHaveBeenCalled()
|
||||
bridge.dispose()
|
||||
}
|
||||
)
|
||||
|
||||
it('retains the bounded fallback when the host never answers', async () => {
|
||||
const bridge = registerAgentStatusStartupSnapshot()
|
||||
const { session, transport } = createSession()
|
||||
startDeferredSessionReattach(session, 'lost-pty')
|
||||
await vi.advanceTimersByTimeAsync(4_999)
|
||||
expect(transport.connect).not.toHaveBeenCalled()
|
||||
await vi.advanceTimersByTimeAsync(1)
|
||||
expect(transport.connect).toHaveBeenCalledOnce()
|
||||
bridge.dispose()
|
||||
})
|
||||
|
||||
it('ignores an old bridge completion after replacement', async () => {
|
||||
const old = registerAgentStatusStartupSnapshot()
|
||||
const { session, transport, replay } = createSession()
|
||||
startDeferredSessionReattach(session, 'lost-pty')
|
||||
const replacement = registerAgentStatusStartupSnapshot()
|
||||
old.settle()
|
||||
old.dispose()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(transport.connect).not.toHaveBeenCalled()
|
||||
replay()
|
||||
replacement.settle()
|
||||
await vi.advanceTimersByTimeAsync(0)
|
||||
expect(transport.connect).toHaveBeenCalledOnce()
|
||||
replacement.dispose()
|
||||
})
|
||||
})
|
||||
+3
-1
@@ -47,7 +47,9 @@ function createContext(replay: string, attemptGeneration: number): ReattachPaylo
|
||||
}
|
||||
|
||||
describe('reattach payload context', () => {
|
||||
beforeEach(() => installTerminalTestGlobals())
|
||||
beforeEach(async () => {
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => restoreTerminalTestGlobals())
|
||||
|
||||
|
||||
+3
-1
@@ -89,7 +89,9 @@ function paintedBytes(fireLog: FireLog): string {
|
||||
// test over sshReconnectPaintsFromModel stayed green, so the coverage has to run
|
||||
// through createReattachPayloadHandlers rather than the gate itself.
|
||||
describe('reattach payload SSH reconnect model paint', () => {
|
||||
beforeEach(() => installTerminalTestGlobals())
|
||||
beforeEach(async () => {
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => restoreTerminalTestGlobals())
|
||||
|
||||
|
||||
+7
-44
@@ -1,6 +1,9 @@
|
||||
import type * as React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery'
|
||||
import {
|
||||
installTerminalTestGlobals,
|
||||
restoreTerminalTestGlobals
|
||||
} from './pty-connection-test-environment'
|
||||
|
||||
async function flushAsyncTicks(count = 6): Promise<void> {
|
||||
for (let i = 0; i < count; i++) {
|
||||
@@ -8,20 +11,6 @@ async function flushAsyncTicks(count = 6): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function drainFakeTimerWork(limit = 20): Promise<void> {
|
||||
await flushAsyncTicks(20)
|
||||
if (!vi.isFakeTimers()) {
|
||||
return
|
||||
}
|
||||
for (let iteration = 0; iteration < limit && vi.getTimerCount() > 0; iteration += 1) {
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await flushAsyncTicks(20)
|
||||
}
|
||||
vi.clearAllTimers()
|
||||
await flushAsyncTicks(20)
|
||||
vi.clearAllTimers()
|
||||
}
|
||||
|
||||
const LEAF_1 = '11111111-1111-4111-8111-111111111111' as const
|
||||
const LEAF_2 = '22222222-2222-4222-8222-222222222222' as const
|
||||
|
||||
@@ -453,13 +442,10 @@ async function advanceModernRetryProbe(): Promise<void> {
|
||||
}
|
||||
|
||||
describe('remote hidden-output restore outcomes', () => {
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
|
||||
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
|
||||
const originalDocument = globalThis.document
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
await installTerminalTestGlobals()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
@@ -597,30 +583,7 @@ describe('remote hidden-output restore outcomes', () => {
|
||||
globalThis.cancelAnimationFrame = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await drainFakeTimerWork()
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
if (originalRequestAnimationFrame) {
|
||||
globalThis.requestAnimationFrame = originalRequestAnimationFrame
|
||||
} else {
|
||||
delete (globalThis as { requestAnimationFrame?: typeof requestAnimationFrame })
|
||||
.requestAnimationFrame
|
||||
}
|
||||
if (originalCancelAnimationFrame) {
|
||||
globalThis.cancelAnimationFrame = originalCancelAnimationFrame
|
||||
} else {
|
||||
delete (globalThis as { cancelAnimationFrame?: typeof cancelAnimationFrame })
|
||||
.cancelAnimationFrame
|
||||
}
|
||||
if (originalDocument) {
|
||||
globalThis.document = originalDocument
|
||||
} else {
|
||||
delete (globalThis as { document?: Document }).document
|
||||
}
|
||||
delete (globalThis as unknown as { window?: unknown }).window
|
||||
resetAgentStartupDelayedDeliveryForTests()
|
||||
})
|
||||
afterEach(restoreTerminalTestGlobals)
|
||||
|
||||
it('[modern] repaints a recovered visible pane from the retained host buffer', async () => {
|
||||
const serializeBuffer = vi.fn()
|
||||
|
||||
+7
-44
@@ -1,6 +1,9 @@
|
||||
import type * as React from 'react'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { resetAgentStartupDelayedDeliveryForTests } from '@/lib/agent-startup-delayed-delivery'
|
||||
import {
|
||||
installTerminalTestGlobals,
|
||||
restoreTerminalTestGlobals
|
||||
} from './pty-connection-test-environment'
|
||||
|
||||
// Deterministic reproduction for issue2-hidden-output-skip:
|
||||
// "[Orca skipped hidden terminal output because main recovery was unavailable.]"
|
||||
@@ -31,20 +34,6 @@ async function flushAsyncTicks(count = 6): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
async function drainFakeTimerWork(limit = 20): Promise<void> {
|
||||
await flushAsyncTicks(20)
|
||||
if (!vi.isFakeTimers()) {
|
||||
return
|
||||
}
|
||||
for (let iteration = 0; iteration < limit && vi.getTimerCount() > 0; iteration += 1) {
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await flushAsyncTicks(20)
|
||||
}
|
||||
vi.clearAllTimers()
|
||||
await flushAsyncTicks(20)
|
||||
vi.clearAllTimers()
|
||||
}
|
||||
|
||||
const LEAF_1 = '11111111-1111-4111-8111-111111111111' as const
|
||||
const LEAF_2 = '22222222-2222-4222-8222-222222222222' as const
|
||||
|
||||
@@ -482,13 +471,10 @@ function observeFinalPaneState(drive: RemotePaneDrive): {
|
||||
}
|
||||
|
||||
describe('remote hidden-output restore abandonment (issue2-hidden-output-skip)', () => {
|
||||
const originalRequestAnimationFrame = globalThis.requestAnimationFrame
|
||||
const originalCancelAnimationFrame = globalThis.cancelAnimationFrame
|
||||
const originalDocument = globalThis.document
|
||||
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
await installTerminalTestGlobals()
|
||||
transportFactoryQueue = []
|
||||
createdTransportOptions = []
|
||||
storeSubscribers = []
|
||||
@@ -626,30 +612,7 @@ describe('remote hidden-output restore abandonment (issue2-hidden-output-skip)',
|
||||
globalThis.cancelAnimationFrame = vi.fn()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await drainFakeTimerWork()
|
||||
vi.useRealTimers()
|
||||
vi.restoreAllMocks()
|
||||
if (originalRequestAnimationFrame) {
|
||||
globalThis.requestAnimationFrame = originalRequestAnimationFrame
|
||||
} else {
|
||||
delete (globalThis as { requestAnimationFrame?: typeof requestAnimationFrame })
|
||||
.requestAnimationFrame
|
||||
}
|
||||
if (originalCancelAnimationFrame) {
|
||||
globalThis.cancelAnimationFrame = originalCancelAnimationFrame
|
||||
} else {
|
||||
delete (globalThis as { cancelAnimationFrame?: typeof cancelAnimationFrame })
|
||||
.cancelAnimationFrame
|
||||
}
|
||||
if (originalDocument) {
|
||||
globalThis.document = originalDocument
|
||||
} else {
|
||||
delete (globalThis as { document?: Document }).document
|
||||
}
|
||||
delete (globalThis as unknown as { window?: unknown }).window
|
||||
resetAgentStartupDelayedDeliveryForTests()
|
||||
})
|
||||
afterEach(restoreTerminalTestGlobals)
|
||||
|
||||
// ── Mechanism (GREEN on main): pins the defective transition exactly ──────
|
||||
|
||||
|
||||
+2
-2
@@ -203,13 +203,13 @@ function bannerCount(writes: string[]): number {
|
||||
}
|
||||
|
||||
describe('parked remote pane reveal with an unverifiable host snapshot', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach(async () => {
|
||||
vi.resetModules()
|
||||
vi.clearAllMocks()
|
||||
transportFactoryQueue = []
|
||||
storeSubscribers = []
|
||||
mockStoreState = createInitialStoreState(() => mockStoreState)
|
||||
installTerminalTestGlobals()
|
||||
await installTerminalTestGlobals()
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
|
||||
@@ -15,6 +15,8 @@ import type {
|
||||
} from './agent-status-bridge-types'
|
||||
import { shouldRetryPendingAgentStatusesAfterStoreUpdate } from './agent-status-pending-retry-gate'
|
||||
|
||||
import { registerAgentStatusStartupSnapshot } from './agent-status-startup-snapshot'
|
||||
|
||||
const PENDING_AGENT_STATUS_RETRY_MS = 100
|
||||
const PENDING_AGENT_STATUS_TTL_MS = 15_000
|
||||
const MAX_PENDING_AGENT_STATUS_EVENTS = 100
|
||||
@@ -104,11 +106,15 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu
|
||||
transientClearWatermarkByConnectionId,
|
||||
enqueuePendingAgentStatus
|
||||
})
|
||||
const startupSnapshot = registerAgentStatusStartupSnapshot()
|
||||
let snapshotRequestedForReadyWindow = false
|
||||
let snapshotRequestId = 0
|
||||
const requestAgentStatusSnapshotIfReady = (): void => {
|
||||
const store = useAppStore.getState()
|
||||
if (!store.workspaceSessionReady) {
|
||||
if (snapshotRequestedForReadyWindow) {
|
||||
startupSnapshot.reset()
|
||||
}
|
||||
snapshotRequestedForReadyWindow = false
|
||||
return
|
||||
}
|
||||
@@ -117,6 +123,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu
|
||||
}
|
||||
const getSnapshot = window.api.agentStatus.getSnapshot
|
||||
if (typeof getSnapshot !== 'function') {
|
||||
startupSnapshot.settle()
|
||||
return
|
||||
}
|
||||
snapshotRequestedForReadyWindow = true
|
||||
@@ -131,6 +138,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu
|
||||
return
|
||||
}
|
||||
applyAgentStatusBatch(entries.map((data) => ({ data, replay: true })))
|
||||
startupSnapshot.settle()
|
||||
const getMigrationUnsupportedSnapshot =
|
||||
window.api.agentStatus.getMigrationUnsupportedSnapshot
|
||||
if (typeof getMigrationUnsupportedSnapshot !== 'function') {
|
||||
@@ -157,6 +165,9 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu
|
||||
})
|
||||
.catch((err) => {
|
||||
// Why: stay latched on failure; the store subscriber fires on every update, so resetting here would turn a persistent IPC failure into a retry storm (flag clears on workspaceSessionReady toggle).
|
||||
if (!disposed && requestId === snapshotRequestId) {
|
||||
startupSnapshot.settle()
|
||||
}
|
||||
console.warn('[agent-status] failed to load startup snapshot:', err)
|
||||
})
|
||||
}
|
||||
@@ -275,6 +286,7 @@ export function registerAgentStatusIpcBridge(unsubs: (() => void)[]): AgentStatu
|
||||
return {
|
||||
disposeAsyncState: () => {
|
||||
disposed = true
|
||||
startupSnapshot.dispose()
|
||||
snapshotRequestId += 1
|
||||
if (pendingAgentStatusRetryTimer !== null) {
|
||||
globalThis.clearTimeout(pendingAgentStatusRetryTimer)
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
type SnapshotSignal = {
|
||||
ready: boolean
|
||||
claimed: boolean
|
||||
promise: Promise<void>
|
||||
resolve: () => void
|
||||
}
|
||||
|
||||
function createSignal(): SnapshotSignal {
|
||||
let resolve = (): void => undefined
|
||||
const promise = new Promise<void>((done) => {
|
||||
resolve = done
|
||||
})
|
||||
return { ready: false, claimed: false, promise, resolve }
|
||||
}
|
||||
|
||||
let current = createSignal()
|
||||
|
||||
export function isAgentStatusStartupSnapshotReady(): boolean {
|
||||
return current.ready
|
||||
}
|
||||
|
||||
/** Coordinates the bridge's existing replay; it retains no agent rows. */
|
||||
export function registerAgentStatusStartupSnapshot(): {
|
||||
reset: () => void
|
||||
settle: () => void
|
||||
dispose: () => void
|
||||
} {
|
||||
if (current.claimed) {
|
||||
current.resolve()
|
||||
current = createSignal()
|
||||
}
|
||||
let owned = current
|
||||
owned.claimed = true
|
||||
return {
|
||||
reset: () => {
|
||||
if (current !== owned) {
|
||||
return
|
||||
}
|
||||
owned.resolve()
|
||||
owned = createSignal()
|
||||
owned.claimed = true
|
||||
current = owned
|
||||
},
|
||||
settle: () => {
|
||||
if (current !== owned) {
|
||||
return
|
||||
}
|
||||
owned.ready = true
|
||||
owned.resolve()
|
||||
},
|
||||
dispose: () => {
|
||||
if (current !== owned) {
|
||||
return
|
||||
}
|
||||
owned.resolve()
|
||||
current = createSignal()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function awaitAgentStatusStartupSnapshot(timeoutMs = 5_000): Promise<void> {
|
||||
const deadline = Date.now() + timeoutMs
|
||||
while (!current.ready) {
|
||||
const remaining = deadline - Date.now()
|
||||
if (remaining <= 0) {
|
||||
return
|
||||
}
|
||||
let timer: ReturnType<typeof setTimeout> | undefined
|
||||
await Promise.race([
|
||||
current.promise,
|
||||
new Promise<void>((resolve) => {
|
||||
timer = setTimeout(resolve, remaining)
|
||||
})
|
||||
])
|
||||
clearTimeout(timer)
|
||||
}
|
||||
}
|
||||
@@ -151,9 +151,7 @@ describe('ai vault resume command runtime', () => {
|
||||
).toBe("claude '--resume' 'session one'")
|
||||
})
|
||||
|
||||
it('follows the live Windows shell for non-resumable agents in the fallback path', () => {
|
||||
// Why: agents without a TUI startup plan (e.g. cursor) queue through the
|
||||
// shared-builder fallback, which must quote for the live shell too (#6152).
|
||||
it('follows the live Windows shell for Cursor resume', () => {
|
||||
const state = makeState({ worktreePath: 'C:\\Users\\alice\\repo' })
|
||||
|
||||
expect(
|
||||
@@ -167,7 +165,7 @@ describe('ai vault resume command runtime', () => {
|
||||
codexHome: null
|
||||
}
|
||||
})
|
||||
).toBe("cursor-agent --resume 'session one'")
|
||||
).toBe("cursor-agent '--yolo' '--resume' 'session one'")
|
||||
})
|
||||
|
||||
it('queues a PowerShell-valid local OMP resume by absolute transcript path', () => {
|
||||
|
||||
@@ -256,7 +256,7 @@ export function getAiVaultAgentProviderSession(
|
||||
if (!isResumableTuiAgent(session.agent)) {
|
||||
return null
|
||||
}
|
||||
if (session.agent === 'antigravity') {
|
||||
if (session.agent === 'antigravity' || session.agent === 'cursor') {
|
||||
return { key: 'conversation_id', id: session.sessionId }
|
||||
}
|
||||
if (session.agent === 'pi' || session.agent === 'prime-agent') {
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OMP_RESUME_PATH_RUNTIME_CAPABILITY,
|
||||
@@ -13,6 +14,12 @@ import {
|
||||
import { agentResumeHostAuthorityCapability } from './agent-resume-host-authority-capability'
|
||||
|
||||
describe('agentResumeHostAuthorityCapability', () => {
|
||||
it('requires Cursor support from the owning host', () => {
|
||||
expect(agentResumeHostAuthorityCapability('cursor')).toBe(
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY
|
||||
)
|
||||
expect(RUNTIME_CAPABILITIES).toContain(AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY)
|
||||
})
|
||||
it('gates Muse resume behind its own advertised capability', () => {
|
||||
expect(agentResumeHostAuthorityCapability('muse')).toBe(
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY
|
||||
@@ -44,6 +51,7 @@ describe('agentResumeHostAuthorityCapability', () => {
|
||||
claude: undefined,
|
||||
codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
codex: undefined,
|
||||
cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
gemini: undefined,
|
||||
antigravity: undefined,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ResumableTuiAgent } from '../../../shared/agent-session-resume'
|
||||
import type { TuiAgent } from '../../../shared/tui-agent'
|
||||
import {
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
@@ -23,6 +24,7 @@ const RESUME_HOST_AUTHORITY_CAPABILITY_BY_AGENT = {
|
||||
claude: undefined,
|
||||
codebuddy: AGENT_SESSION_CODEBUDDY_RESUME_RUNTIME_CAPABILITY,
|
||||
codex: undefined,
|
||||
cursor: AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
qoder: AGENT_SESSION_QODER_RESUME_RUNTIME_CAPABILITY,
|
||||
gemini: undefined,
|
||||
antigravity: undefined,
|
||||
|
||||
@@ -45,7 +45,7 @@ describe('remote agent-session launch routing', () => {
|
||||
expect(legacy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it.each(['kimi', 'muse', 'codebuddy'] as const)(
|
||||
it.each(['kimi', 'muse', 'codebuddy', 'cursor'] as const)(
|
||||
'falls back to legacy when an older host lacks the %s resume capability',
|
||||
async (agent) => {
|
||||
const hostAuthority = vi.fn().mockResolvedValue('structured')
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { createTestStore, makeTab } from './store-test-helpers'
|
||||
|
||||
describe('Cursor quit checkpoint', () => {
|
||||
it.each(['working', 'done'] as const)(
|
||||
'retains the same %s conversation in a folder workspace',
|
||||
(state) => {
|
||||
const store = createTestStore()
|
||||
store.setState({
|
||||
tabsByWorktree: { 'folder-1': [makeTab({ id: 'tab-1', worktreeId: 'folder-1' })] }
|
||||
})
|
||||
store
|
||||
.getState()
|
||||
.setAgentStatus(
|
||||
'tab-1:leaf-1',
|
||||
{ state: 'working', prompt: 'remember the codeword', agentType: 'cursor' },
|
||||
'Cursor',
|
||||
{ updatedAt: 10, stateStartedAt: 10 },
|
||||
{ tabId: 'tab-1', worktreeId: 'folder-1' },
|
||||
{ providerSession: { key: 'conversation_id', id: 'conversation-742' } }
|
||||
)
|
||||
if (state === 'done') {
|
||||
store.getState().setAgentStatus('tab-1:leaf-1', {
|
||||
state,
|
||||
prompt: 'remember the codeword',
|
||||
agentType: 'cursor'
|
||||
})
|
||||
}
|
||||
store.getState().captureAllSleepingAgentSessions('quit')
|
||||
expect(store.getState().sleepingAgentSessionsByPaneKey['tab-1:leaf-1']).toMatchObject({
|
||||
agent: 'cursor',
|
||||
state,
|
||||
worktreeId: 'folder-1',
|
||||
providerSession: { key: 'conversation_id', id: 'conversation-742' },
|
||||
origin: state === 'done' ? 'live' : 'quit'
|
||||
})
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
extractAgentProviderSession,
|
||||
getAgentResumeArgv,
|
||||
isResumableTuiAgent
|
||||
} from './agent-session-resume'
|
||||
import { buildAgentResumeStartupPlan } from './tui-agent-startup'
|
||||
|
||||
describe('Cursor conversation continuity', () => {
|
||||
it('captures only the hook conversation identity', () => {
|
||||
expect(isResumableTuiAgent('cursor')).toBe(true)
|
||||
expect(
|
||||
extractAgentProviderSession('cursor', {
|
||||
conversation_id: 'conversation-742',
|
||||
session_id: 'tool-session'
|
||||
})
|
||||
).toEqual({ key: 'conversation_id', id: 'conversation-742' })
|
||||
expect(extractAgentProviderSession('cursor', { session_id: 'tool-session' })).toBeNull()
|
||||
expect(extractAgentProviderSession('cursor', { conversation_id: '--continue' })).toBeNull()
|
||||
expect(
|
||||
extractAgentProviderSession('cursor', { conversation_id: 'session\ncommand' })
|
||||
).toBeNull()
|
||||
})
|
||||
|
||||
it('resumes the exact conversation instead of the latest one', () => {
|
||||
expect(
|
||||
getAgentResumeArgv('cursor', { key: 'conversation_id', id: 'conversation-742' })
|
||||
).toEqual(['cursor-agent', '--resume', 'conversation-742'])
|
||||
expect(getAgentResumeArgv('cursor', { key: 'session_id', id: 'tool-session' })).toBeNull()
|
||||
})
|
||||
|
||||
it.each(['linux', 'darwin', 'win32'] as const)(
|
||||
'preserves the configured executable and launch recipe on %s',
|
||||
(platform) => {
|
||||
const plan = buildAgentResumeStartupPlan({
|
||||
agent: 'cursor',
|
||||
providerSession: { key: 'conversation_id', id: 'conversation-742' },
|
||||
cmdOverrides: { cursor: 'custom-cursor' },
|
||||
platform,
|
||||
agentArgs: '--mode ask'
|
||||
})
|
||||
expect(plan?.launchCommand).toContain('custom-cursor')
|
||||
expect(plan?.launchCommand).toContain('conversation-742')
|
||||
expect(plan?.launchCommand).toContain('--resume')
|
||||
expect(plan?.launchCommand).toContain('--mode')
|
||||
expect(plan?.launchCommand).not.toContain('--continue')
|
||||
}
|
||||
)
|
||||
})
|
||||
@@ -20,6 +20,7 @@ export const RESUMABLE_TUI_AGENTS = [
|
||||
'omp',
|
||||
'prime-agent',
|
||||
'copilot',
|
||||
'cursor',
|
||||
'kimi',
|
||||
'muse',
|
||||
'zcode',
|
||||
@@ -264,8 +265,11 @@ export function extractAgentProviderSession(
|
||||
const id = readSessionId(payload, ['session_id', 'sessionId'])
|
||||
return id ? { key: 'session_id', id } : null
|
||||
}
|
||||
case 'cursor': {
|
||||
const id = readSessionId(payload, ['conversation_id'])
|
||||
return id ? { key: 'conversation_id', id } : null
|
||||
}
|
||||
case 'amp':
|
||||
case 'cursor':
|
||||
case 'command-code':
|
||||
case 'hermes':
|
||||
return null
|
||||
@@ -283,6 +287,8 @@ export function getAgentResumeArgv(
|
||||
return providerSession.key === 'session_id' ? ['codebuddy', '--resume', id] : null
|
||||
case 'claude':
|
||||
return providerSession.key === 'session_id' ? ['claude', '--resume', id] : null
|
||||
case 'cursor':
|
||||
return providerSession.key === 'conversation_id' ? ['cursor-agent', '--resume', id] : null
|
||||
case 'codex':
|
||||
return providerSession.key === 'session_id' ? ['codex', 'resume', id] : null
|
||||
case 'qoder':
|
||||
|
||||
@@ -249,6 +249,8 @@ export const AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY =
|
||||
// Why: adding kimi to RESUMABLE_TUI_AGENTS grows terminal.ensureAgentSession's enum, and an
|
||||
// older host answers the unknown member with invalid_argument — a code the launch fallback does
|
||||
// not retry on — so clients must probe before taking the host-authority path.
|
||||
export const AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.cursor-resume.v1' as const
|
||||
export const AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY = 'agent-session.kimi-resume.v1' as const
|
||||
export const AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY =
|
||||
'agent-session.opencode2-resume.v1' as const
|
||||
@@ -413,6 +415,7 @@ export const RUNTIME_CAPABILITIES = [
|
||||
AGENT_SESSION_BACKGROUND_TASK_ROW_STOP_CAPABILITY,
|
||||
AGENT_SESSION_BACKGROUND_TASK_CHILD_VIEWS_CAPABILITY,
|
||||
AGENT_SESSION_KIMI_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_CURSOR_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_OPENCODE2_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_MUSE_RESUME_RUNTIME_CAPABILITY,
|
||||
AGENT_SESSION_DSH_RESUME_RUNTIME_CAPABILITY,
|
||||
|
||||
@@ -3,10 +3,10 @@ import {
|
||||
mutateStoppedProfileState
|
||||
} from './helpers/persisted-profile-state'
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import path from 'node:path'
|
||||
import type { ChildProcess } from 'node:child_process'
|
||||
import type { ElectronApplication } from '@stablyai/playwright-test'
|
||||
import type { ElectronApplication, Page } from '@stablyai/playwright-test'
|
||||
import { test, expect } from './helpers/orca-app'
|
||||
import { TEST_REPO_PATH_FILE } from './global-setup'
|
||||
import {
|
||||
@@ -144,135 +144,231 @@ function persistedLiveRecordExists(userDataDir: string): boolean {
|
||||
)
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
test('resumes a live agent record after force-exit restart when pane PTY ownership is gone', async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set.
|
||||
{}, testInfo) => {
|
||||
const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim()
|
||||
if (!repoPath || !existsSync(repoPath)) {
|
||||
test.skip(true, 'Global setup did not produce a seeded test repo')
|
||||
return
|
||||
}
|
||||
|
||||
const session = createRestartSession(testInfo)
|
||||
let firstApp: ElectronApplication | null = null
|
||||
let secondApp: ElectronApplication | null = null
|
||||
|
||||
try {
|
||||
const firstLaunch = await session.launch()
|
||||
firstApp = firstLaunch.app
|
||||
const page = firstLaunch.page
|
||||
const worktreeId = await attachRepoAndOpenTerminal(page, repoPath)
|
||||
await waitForSessionReady(page)
|
||||
// Why: the session writer persists only once hydrationSucceeded flips (not
|
||||
// just workspaceSessionReady) — see shouldPersistWorkspaceSession — so the
|
||||
// record write below is a silent no-op until hydration completes.
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__store?.getState().hydrationSucceeded === true), {
|
||||
timeout: 30_000,
|
||||
message: 'hydrationSucceeded did not become true before persisting the live record'
|
||||
})
|
||||
.toBe(true)
|
||||
await waitForActiveWorktree(page)
|
||||
await ensureTerminalVisible(page)
|
||||
await waitForActiveTerminalManager(page, 30_000)
|
||||
await waitForPaneCount(page, 1, 30_000)
|
||||
|
||||
const descriptor = await waitForActivePaneHookDescriptor(page)
|
||||
const ptyId = await waitForActivePanePtyId(page)
|
||||
const transcriptPath = session.seedCodexResumeRollout(PROVIDER_SESSION_ID, repoPath)
|
||||
const marker = `AGENT_LIVE_FORCE_EXIT_${Date.now()}`
|
||||
await execInTerminal(page, ptyId, `echo ${marker}`)
|
||||
await waitForTerminalOutput(page, marker)
|
||||
|
||||
await page.evaluate(
|
||||
({ paneKey, worktreeId: wtId, providerSessionId, transcriptPath }) => {
|
||||
window.__store?.getState().setAgentStatus(
|
||||
paneKey,
|
||||
{ state: 'working', prompt: 'finish the task', agentType: 'codex' },
|
||||
'Codex',
|
||||
undefined,
|
||||
{ worktreeId: wtId },
|
||||
{
|
||||
providerSession: {
|
||||
key: 'session_id',
|
||||
id: providerSessionId,
|
||||
transcriptPath
|
||||
async function captureOutputRow(page: Page, expected: string, proofPath: string): Promise<void> {
|
||||
let clip: { x: number; y: number; width: number; height: number } | null = null
|
||||
await expect
|
||||
.poll(
|
||||
async () => {
|
||||
clip = await page.evaluate((expected) => {
|
||||
const state = window.__store?.getState()
|
||||
const tabId = state?.activeTabId
|
||||
const manager = tabId ? window.__paneManagers?.get(tabId) : null
|
||||
const pane = manager?.getActivePane?.() ?? manager?.getPanes?.()[0]
|
||||
const screen = pane?.container.querySelector('.xterm-screen')
|
||||
if (!pane || !screen) {
|
||||
return null
|
||||
}
|
||||
const buffer = pane.terminal.buffer.active
|
||||
const bounds = screen.getBoundingClientRect()
|
||||
const rowHeight = bounds.height / pane.terminal.rows
|
||||
for (let row = 0; row < pane.terminal.rows; row += 1) {
|
||||
const text = buffer
|
||||
.getLine(buffer.viewportY + row)
|
||||
?.translateToString(true)
|
||||
.trim()
|
||||
if (text === expected) {
|
||||
return {
|
||||
x: bounds.x,
|
||||
y: bounds.y + row * rowHeight,
|
||||
width: bounds.width,
|
||||
height: rowHeight
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
return null
|
||||
}, expected)
|
||||
return clip !== null
|
||||
},
|
||||
{
|
||||
paneKey: descriptor.paneKey,
|
||||
worktreeId: descriptor.worktreeId,
|
||||
providerSessionId: PROVIDER_SESSION_ID,
|
||||
transcriptPath
|
||||
}
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
|
||||
// Exercise quit capture: origin:'quit' changes the live record, triggering the
|
||||
// hydration-gated writer before polling persisted state.
|
||||
await page.evaluate(() => window.__store?.getState().captureAllSleepingAgentSessions('quit'))
|
||||
|
||||
// Why: the record reaches disk via the debounced session writer (150ms) plus
|
||||
// the main-process scheduleSave (up to 5s). Under CI event-loop starvation —
|
||||
// the same shard drifts renderer timers ~1s — both stages need headroom, so
|
||||
// poll to 30s (this suite's other readiness budget). On a miss, surface store
|
||||
// vs disk state to separate a lost write from a merely slow flush.
|
||||
const persistDeadline = Date.now() + 30_000
|
||||
let persisted = false
|
||||
while (Date.now() < persistDeadline) {
|
||||
if (persistedLiveRecordExists(session.userDataDir)) {
|
||||
persisted = true
|
||||
break
|
||||
}
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
if (!persisted) {
|
||||
const storeRecords = await page.evaluate(
|
||||
() => window.__store?.getState().sleepingAgentSessionsByPaneKey
|
||||
)
|
||||
throw new Error(
|
||||
`Live sleeping-agent record was not persisted before force exit. store=${JSON.stringify(
|
||||
storeRecords
|
||||
)} disk=${JSON.stringify(
|
||||
readPersistedData(session.userDataDir).workspaceSession?.sleepingAgentSessionsByPaneKey
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
const daemonPid = readDaemonPid(session.userDataDir)
|
||||
await forceKillElectronApp(firstApp)
|
||||
firstApp = null
|
||||
killPid(daemonPid)
|
||||
stripPersistedPtyOwnership(session.userDataDir)
|
||||
|
||||
const secondLaunch = await session.launch()
|
||||
secondApp = secondLaunch.app
|
||||
await waitForSessionReady(secondLaunch.page)
|
||||
await expect
|
||||
.poll(
|
||||
async () => secondLaunch.page.evaluate(() => window.__store?.getState().activeWorktreeId),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(worktreeId)
|
||||
await ensureTerminalVisible(secondLaunch.page)
|
||||
await waitForActiveTerminalManager(secondLaunch.page, 30_000)
|
||||
|
||||
await waitForTerminalOutput(secondLaunch.page, PROVIDER_SESSION_ID, 30_000)
|
||||
|
||||
const terminalTabCount = await secondLaunch.page.evaluate(
|
||||
(wtId) => (window.__store?.getState().tabsByWorktree[wtId] ?? []).length,
|
||||
worktreeId
|
||||
)
|
||||
expect(terminalTabCount).toBe(2)
|
||||
} finally {
|
||||
if (secondApp) {
|
||||
await session.close(secondApp)
|
||||
}
|
||||
if (firstApp) {
|
||||
await forceKillElectronApp(firstApp)
|
||||
}
|
||||
await session.dispose()
|
||||
.toBe(true)
|
||||
if (!clip) {
|
||||
throw new Error('Expected output row was not rendered')
|
||||
}
|
||||
})
|
||||
await page.screenshot({ path: proofPath, clip })
|
||||
}
|
||||
|
||||
test.describe.configure({ mode: 'serial' })
|
||||
|
||||
for (const agent of ['codex', 'cursor'] as const) {
|
||||
const providerSessionKey = agent === 'cursor' ? 'conversation_id' : 'session_id'
|
||||
test(`resumes a live ${agent} record after force-exit restart when pane PTY ownership is gone`, async (// oxlint-disable-next-line no-empty-pattern -- Playwright's second fixture arg is testInfo; the first must be an object destructure to opt out of the default fixture set.
|
||||
{}, testInfo) => {
|
||||
const repoPath = readFileSync(TEST_REPO_PATH_FILE, 'utf-8').trim()
|
||||
if (!repoPath || !existsSync(repoPath)) {
|
||||
test.skip(true, 'Global setup did not produce a seeded test repo')
|
||||
return
|
||||
}
|
||||
|
||||
const session = createRestartSession(testInfo, {
|
||||
ORCA_BACKGROUND_LAUNCH: '1',
|
||||
ORCA_DISABLE_CODEX_TRUST_RPC: '1'
|
||||
})
|
||||
const seedPath = path.join(session.userDataDir, 'orca-data.json')
|
||||
const seed = JSON.parse(readFileSync(seedPath, 'utf8'))
|
||||
seed.settings.agentStatusHooksEnabled = false
|
||||
writeFileSync(seedPath, JSON.stringify(seed))
|
||||
let firstApp: ElectronApplication | null = null
|
||||
let secondApp: ElectronApplication | null = null
|
||||
|
||||
try {
|
||||
const firstLaunch = await session.launch()
|
||||
firstApp = firstLaunch.app
|
||||
const page = firstLaunch.page
|
||||
expect(
|
||||
await firstApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().every((window) => !window.isVisible())
|
||||
)
|
||||
).toBe(true)
|
||||
expect(
|
||||
await page.evaluate(() => window.__store?.getState().settings?.agentStatusHooksEnabled)
|
||||
).toBe(false)
|
||||
expect(await firstApp.evaluate(() => process.env.ORCA_DISABLE_CODEX_TRUST_RPC)).toBe('1')
|
||||
const worktreeId = await attachRepoAndOpenTerminal(page, repoPath)
|
||||
await waitForSessionReady(page)
|
||||
// Why: the session writer persists only once hydrationSucceeded flips (not
|
||||
// just workspaceSessionReady) — see shouldPersistWorkspaceSession — so the
|
||||
// record write below is a silent no-op until hydration completes.
|
||||
await expect
|
||||
.poll(() => page.evaluate(() => window.__store?.getState().hydrationSucceeded === true), {
|
||||
timeout: 30_000,
|
||||
message: 'hydrationSucceeded did not become true before persisting the live record'
|
||||
})
|
||||
.toBe(true)
|
||||
await waitForActiveWorktree(page)
|
||||
await ensureTerminalVisible(page)
|
||||
await waitForActiveTerminalManager(page, 30_000)
|
||||
await waitForPaneCount(page, 1, 30_000)
|
||||
|
||||
const descriptor = await waitForActivePaneHookDescriptor(page)
|
||||
const ptyId = await waitForActivePanePtyId(page)
|
||||
const transcriptPath =
|
||||
agent === 'codex'
|
||||
? session.seedCodexResumeRollout(PROVIDER_SESSION_ID, repoPath)
|
||||
: undefined
|
||||
const marker = `AGENT_LIVE_FORCE_EXIT_${Date.now()}`
|
||||
await execInTerminal(page, ptyId, `echo ${marker}`)
|
||||
await waitForTerminalOutput(page, marker)
|
||||
if (agent === 'cursor') {
|
||||
const before = testInfo.outputPath('cursor-resume-before.png')
|
||||
await captureOutputRow(page, marker, before)
|
||||
await testInfo.attach('Cursor fixture before checkpoint', {
|
||||
path: before,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
}
|
||||
|
||||
await page.evaluate(
|
||||
({
|
||||
paneKey,
|
||||
worktreeId: wtId,
|
||||
providerSessionId,
|
||||
transcriptPath,
|
||||
agent,
|
||||
providerSessionKey
|
||||
}) => {
|
||||
window.__store?.getState().setAgentStatus(
|
||||
paneKey,
|
||||
{ state: 'working', prompt: 'finish the task', agentType: agent },
|
||||
agent === 'cursor' ? 'Cursor' : 'Codex',
|
||||
undefined,
|
||||
{ worktreeId: wtId },
|
||||
{
|
||||
providerSession: {
|
||||
key: providerSessionKey,
|
||||
id: providerSessionId,
|
||||
transcriptPath
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
{
|
||||
paneKey: descriptor.paneKey,
|
||||
worktreeId: descriptor.worktreeId,
|
||||
providerSessionId: PROVIDER_SESSION_ID,
|
||||
transcriptPath,
|
||||
agent,
|
||||
providerSessionKey
|
||||
}
|
||||
)
|
||||
|
||||
// Exercise quit capture: origin:'quit' changes the live record, triggering the
|
||||
// hydration-gated writer before polling persisted state.
|
||||
await page.evaluate(() => window.__store?.getState().captureAllSleepingAgentSessions('quit'))
|
||||
|
||||
// Why: the record reaches disk via the debounced session writer (150ms) plus
|
||||
// the main-process scheduleSave (up to 5s). Under CI event-loop starvation —
|
||||
// the same shard drifts renderer timers ~1s — both stages need headroom, so
|
||||
// poll to 30s (this suite's other readiness budget). On a miss, surface store
|
||||
// vs disk state to separate a lost write from a merely slow flush.
|
||||
const persistDeadline = Date.now() + 30_000
|
||||
let persisted = false
|
||||
while (Date.now() < persistDeadline) {
|
||||
if (persistedLiveRecordExists(session.userDataDir)) {
|
||||
persisted = true
|
||||
break
|
||||
}
|
||||
await page.waitForTimeout(250)
|
||||
}
|
||||
if (!persisted) {
|
||||
const storeRecords = await page.evaluate(
|
||||
() => window.__store?.getState().sleepingAgentSessionsByPaneKey
|
||||
)
|
||||
throw new Error(
|
||||
`Live sleeping-agent record was not persisted before force exit. store=${JSON.stringify(
|
||||
storeRecords
|
||||
)} disk=${JSON.stringify(
|
||||
readPersistedData(session.userDataDir).workspaceSession?.sleepingAgentSessionsByPaneKey
|
||||
)}`
|
||||
)
|
||||
}
|
||||
|
||||
const daemonPid = readDaemonPid(session.userDataDir)
|
||||
await forceKillElectronApp(firstApp)
|
||||
firstApp = null
|
||||
killPid(daemonPid)
|
||||
stripPersistedPtyOwnership(session.userDataDir)
|
||||
|
||||
const secondLaunch = await session.launch()
|
||||
secondApp = secondLaunch.app
|
||||
await waitForSessionReady(secondLaunch.page)
|
||||
await expect
|
||||
.poll(
|
||||
async () => secondLaunch.page.evaluate(() => window.__store?.getState().activeWorktreeId),
|
||||
{ timeout: 15_000 }
|
||||
)
|
||||
.toBe(worktreeId)
|
||||
await ensureTerminalVisible(secondLaunch.page)
|
||||
await waitForActiveTerminalManager(secondLaunch.page, 30_000)
|
||||
|
||||
await waitForTerminalOutput(secondLaunch.page, PROVIDER_SESSION_ID, 30_000)
|
||||
if (agent === 'cursor') {
|
||||
expect(
|
||||
await secondApp.evaluate(({ BrowserWindow }) =>
|
||||
BrowserWindow.getAllWindows().every((window) => !window.isVisible())
|
||||
)
|
||||
).toBe(true)
|
||||
const after = testInfo.outputPath('cursor-resume-after.png')
|
||||
await captureOutputRow(secondLaunch.page, `--resume ${PROVIDER_SESSION_ID}`, after)
|
||||
await testInfo.attach('Cursor fixture after cold resume dispatch', {
|
||||
path: after,
|
||||
contentType: 'image/png'
|
||||
})
|
||||
}
|
||||
|
||||
const terminalTabCount = await secondLaunch.page.evaluate(
|
||||
(wtId) => (window.__store?.getState().tabsByWorktree[wtId] ?? []).length,
|
||||
worktreeId
|
||||
)
|
||||
expect(terminalTabCount).toBe(2)
|
||||
} finally {
|
||||
if (secondApp) {
|
||||
await session.close(secondApp)
|
||||
}
|
||||
if (firstApp) {
|
||||
await forceKillElectronApp(firstApp)
|
||||
}
|
||||
await session.dispose()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user