test(agent-status): cover the closed-tab lift IPC and its renderer trigger

The lift's only production trigger — the renderer re-mirror call — and the IPC
handler that carries it had no test: reverting either left every suite green.
The server-side method was the sole covered hunk.

Also adds liftClosedAgentStatusTabs to the agentHookServer double; the handler
swallows its own errors, so the omitted field would have passed vacuously.
This commit is contained in:
Brennan Benson
2026-08-28 01:26:51 -07:00
parent 24d5e2bfef
commit 22685e4f31
2 changed files with 73 additions and 0 deletions
+62
View File
@@ -9,6 +9,7 @@ import { makePaneKey } from '../../shared/stable-pane-id'
const dropStatusEntry = vi.fn()
const dropStatusEntriesByTabPrefix = vi.fn()
const liftClosedAgentStatusTabs = vi.fn()
const retirePaneAuthority = vi.fn()
const transferPaneAuthority = vi.fn()
const canTransferPaneAuthority = vi.fn(() => true)
@@ -45,6 +46,7 @@ vi.mock('../agent-hooks/server', async () => {
agentHookServer: {
dropStatusEntry,
dropStatusEntriesByTabPrefix,
liftClosedAgentStatusTabs,
retirePaneAuthority,
transferPaneAuthority,
canTransferPaneAuthority,
@@ -106,6 +108,7 @@ vi.mock('../kimi/hook-service', () => ({
beforeEach(() => {
dropStatusEntry.mockReset()
dropStatusEntriesByTabPrefix.mockReset()
liftClosedAgentStatusTabs.mockReset()
retirePaneAuthority.mockReset()
transferPaneAuthority.mockReset()
canTransferPaneAuthority.mockReset()
@@ -389,6 +392,65 @@ describe('agentStatus:dropByTabPrefix IPC', () => {
})
})
describe('agentStatus:liftClosedTabs IPC', () => {
it('forwards validated tab ids so a returning tab stops being suppressed', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
const handler = onHandlers.get('agentStatus:liftClosedTabs')
expect(handler).toBeDefined()
handler!({}, ['tab-1', 'tab-2'])
expect(liftClosedAgentStatusTabs).toHaveBeenCalledWith(['tab-1', 'tab-2'])
})
it('ignores a payload that is not an array', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
const handler = onHandlers.get('agentStatus:liftClosedTabs')!
for (const value of ['tab-1', 123, undefined, null, {}] as unknown[]) {
expect(() => handler({}, value)).not.toThrow()
}
expect(liftClosedAgentStatusTabs).not.toHaveBeenCalled()
})
it('filters malformed ids out of an otherwise valid array', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
onHandlers.get('agentStatus:liftClosedTabs')!({}, [
'tab-1',
123,
null,
'tab-1:leaf',
' leading-space',
'x'.repeat(161)
])
// Why assert the exact argument: a filter that let a malformed id through would still
// "have been called", so only the forwarded value proves the boundary held.
expect(liftClosedAgentStatusTabs).toHaveBeenCalledWith(['tab-1'])
})
it('skips the server entirely when every id is rejected', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
onHandlers.get('agentStatus:liftClosedTabs')!({}, ['', 'tab-1:leaf', 'x'.repeat(161)])
expect(liftClosedAgentStatusTabs).not.toHaveBeenCalled()
})
it('removes any existing listener before registering the lift channel', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
registerAgentHookHandlers()
expect(removeAllListeners).toHaveBeenCalledWith('agentStatus:liftClosedTabs')
})
})
describe('agent pane authority IPC', () => {
it('retires one validated pane authority', async () => {
const { registerAgentHookHandlers } = await import('./agent-hooks')
@@ -273,16 +273,23 @@ function observeSidebar(store: TestStore, now: number): SidebarObservation {
}
}
const liftClosedTabs = vi.fn()
describe('a host-retracted paired tab leaves no ghost agent row behind', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(T0)
resetWebSessionTabsSnapshotFreshnessForTests()
resetRendererOwnedAgentStatusPanesForTests()
liftClosedTabs.mockClear()
// Why stub the bridge: lifting only the renderer marker leaves the tab suppressed in the
// main-process closed-tab set, which gates every hook event upstream of the renderer.
vi.stubGlobal('window', { ...globalThis.window, api: { agentStatus: { liftClosedTabs } } })
})
afterEach(() => {
vi.useRealTimers()
vi.unstubAllGlobals()
resetRendererOwnedAgentStatusPanesForTests()
})
@@ -591,6 +598,10 @@ describe('a host-retracted paired tab leaves no ghost agent row behind', () => {
store.getState().recentlyClosedAgentStatusTabIds[mirrorTabId(RETRACTED_TAB)],
'a re-mirrored tab id kept its closed-tab marker'
).toBeUndefined()
expect(
liftClosedTabs.mock.calls.flatMap(([tabIds]) => tabIds as string[]),
'the renderer lifted its own marker but left the tab suppressed in the main process'
).toContain(mirrorTabId(RETRACTED_TAB))
// The returning pane's byte-derived status must land again.
const release2 = replayClientByteStatus(