From a379f04db075869ef6ee770cde596091933be5bd Mon Sep 17 00:00:00 2001 From: Merge Sim Date: Mon, 7 Sep 2026 16:36:13 -0700 Subject: [PATCH] test(codex): cover stop publication lifecycle and per-task refusal --- .../codex-structured-background-terminals.ts | 1 + ...tructured-session-background-tasks.test.ts | 65 ++++++++++++++----- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/src/main/codex/codex-structured-background-terminals.ts b/src/main/codex/codex-structured-background-terminals.ts index f6c6ea4d8f5..34a82f8e21f 100644 --- a/src/main/codex/codex-structured-background-terminals.ts +++ b/src/main/codex/codex-structured-background-terminals.ts @@ -201,6 +201,7 @@ export function createCodexBackgroundTerminalChannel(deps: { const publish = (sessionId: string, session: CodexSession): void => { const state = session.backgroundTerminals.state if ( + session.ended || deps.sessions.get(sessionId) !== session || statesEqual(published.get(session) ?? null, state) ) { diff --git a/src/main/codex/codex-structured-session-background-tasks.test.ts b/src/main/codex/codex-structured-session-background-tasks.test.ts index 1f0912c05e6..12d257004ad 100644 --- a/src/main/codex/codex-structured-session-background-tasks.test.ts +++ b/src/main/codex/codex-structured-session-background-tasks.test.ts @@ -210,25 +210,32 @@ describe('CodexStructuredSessionAdapter background terminals', () => { expect(codex.connections[0].calls.filter((call) => call.method === LIST)).toHaveLength(1) }) - it('reports an unconfirmed stop when the host refuses to clean', async () => { - const codex = fakeCodex() - codex.routes[CLEAN] = refuse(CLEAN) - const { adapter, published } = await acquired(codex) - completeTurn(codex) - await settle() + it.each([CLEAN, TERMINATE])( + 'publishes capability loss when %s is unsupported', + async (method) => { + const codex = fakeCodex() + codex.routes[method] = refuse(method) + const { adapter, published } = await acquired(codex) + completeTurn(codex) + await settle() - expect(published).toHaveLength(1) - expect(published[0]?.state).toBe('monitoring') - await expect( - adapter.stopBackgroundTasks?.({ sessionId: 'session-1', fence: 7 }) - ).resolves.toEqual({ cancelled: false }) - expect(adapter.backgroundTaskState?.('session-1')).toBeNull() - expect(published).toHaveLength(2) - expect(published.at(-1)).toBeNull() - completeTurn(codex) - await settle() - expect(published).toHaveLength(2) - }) + expect(published).toHaveLength(1) + expect(published[0]?.state).toBe('monitoring') + await expect( + adapter.stopBackgroundTasks?.({ + sessionId: 'session-1', + fence: 7, + ...(method === TERMINATE ? { taskId: 'proc-1' } : {}) + }) + ).resolves.toEqual({ cancelled: false }) + expect(adapter.backgroundTaskState?.('session-1')).toBeNull() + expect(published).toHaveLength(2) + expect(published.at(-1)).toBeNull() + completeTurn(codex) + await settle() + expect(published).toHaveLength(2) + } + ) }) it('does not revive a disabled capability from an in-flight list', async () => { @@ -259,6 +266,28 @@ describe('CodexStructuredSessionAdapter background terminals', () => { expect(codex.connections[0].calls).toHaveLength(calls) }) + it('does not publish a delayed stop from a replaced session', async () => { + const codex = fakeCodex() + const { adapter, published } = await acquired(codex) + completeTurn(codex) + await settle() + let rejectStop!: (error: Error) => void + codex.routes[CLEAN] = () => + new Promise((_resolve, reject) => { + rejectStop = reject + }) + const stopping = adapter.stopBackgroundTasks({ sessionId: 'session-1', fence: 7 }) + await settle() + await adapter.closeSession('session-1') + await adapter.acquire({ identity: identity(), fence: 8, spawnToken: 'spawn-10' }) + const delivered = published.length + rejectStop(new CodexAppServerUnsupportedError('method not found')) + await stopping + await settle() + expect(published).toHaveLength(delivered) + expect(adapter.backgroundTaskState('session-1')).toBeNull() + }) + it('refuses a stop aimed at a superseded child', async () => { const codex = fakeCodex() const { adapter } = await acquired(codex)