import { describe, expect, test, vi } from 'vitest'; import { abortManagedCodexProcess } from '../../src/codex-process'; import { teardownWorkspace } from '../../src/workspace-teardown'; describe('workspace teardown', () => { test('event failure does not skip abort, stop, removal, or release', async () => { const calls: string[] = []; const record = (name: string) => vi.fn(() => calls.push(name)); const result = await teardownWorkspace({ stopHeartbeat: record('heartbeat'), removeActive: record('remove'), appendEvent: vi.fn().mockRejectedValue(new Error('event failed')), abortAgent: record('abort'), markStopped: vi.fn().mockRejectedValue(new Error('mutation failed')), closeAgent: record('close'), stopContainer: vi.fn().mockRejectedValue(new Error('container failed')), release: record('release'), }); expect(calls).toEqual(['heartbeat', 'remove', 'abort', 'close', 'release']); expect(result.errors).toHaveLength(3); }); test('removes active state before asynchronous cancellation work', async () => { const active = new Set(['job-1']); let activeDuringAbort = true; await teardownWorkspace({ stopHeartbeat: vi.fn(), removeActive: () => active.delete('job-1'), abortAgent: () => { activeDuringAbort = active.has('job-1'); }, release: vi.fn(), }); expect(activeDuringAbort).toBe(false); expect(active.has('job-1')).toBe(false); }); test('Codex cancellation invokes isolated kill and leaves no active workspace', async () => { const active = new Set(['job-1']); const execute = vi.fn().mockResolvedValue({ exitCode: 0, output: '' }); await teardownWorkspace({ stopHeartbeat: vi.fn(), removeActive: () => active.delete('job-1'), abortAgent: async () => await abortManagedCodexProcess({ jobId: 'job-1', pidFile: '/home/alice/.codex/job-1.pid', execute, }), release: vi.fn(), }); expect(execute).toHaveBeenCalledOnce(); expect(active.has('job-1')).toBe(false); }); });