import { afterEach, describe, expect, it, vi } from 'vitest' import { readFile } from 'node:fs/promises' import { join } from 'node:path' import { runCleanupBeforeDeadline, settleCleanupPhases, waitForCleanup } from './shutdown' afterEach(() => { vi.useRealTimers() }) describe('waitForCleanup', () => { it('detaches runtime ownership before ordinary runtime disposal on Desktop exit', async () => { const source = await readFile( join(process.cwd(), 'src', 'main', 'index.ts'), 'utf8' ) const cleanup = source.match( /const cleanup = settleCleanupPhases\(\[[\s\S]*?\s+globalShortcut/u )?.[0] ?? '' expect(cleanup).toContain('runtime?.detachForApplicationExit()') expect(cleanup).toContain( 'selectedRuntimeManager?.detachForApplicationExit()' ) expect(cleanup.indexOf('runtime?.detachForApplicationExit()')).toBeLessThan( cleanup.indexOf('runtime?.dispose()') ) const storageClose = cleanup.indexOf('desktopStorage?.close()') expect(storageClose).toBeGreaterThan(0) for (const drain of [ 'applicationStartup', 'runtimeTemporaryCleanup', 'removeIpcHandlers?.()', 'startupSubagentService?.dispose()', 'imageGenerationService?.dispose()', 'nativeClientCoordinator?.dispose()', 'startupContextManager?.dispose()', 'runtime?.dispose()', 'selectedRuntimeManager?.dispose()', 'terminalSessionManager?.dispose()', 'managedRemoteExecutionServices?.dispose()', 'knowledgeGateway?.dispose()', 'knowledgeService?.dispose()', 'documentParsingService?.dispose()' ]) { expect(cleanup.indexOf(drain), drain).toBeGreaterThanOrEqual(0) expect(cleanup.indexOf(drain), drain).toBeLessThan(storageClose) } expect(source).not.toContain('assistantDatabase?.close()') const quitting = source.slice(source.indexOf("app.on('before-quit'")) expect(quitting.indexOf('runtimeTemporaryCleanupController.abort()')).toBeLessThan( quitting.indexOf('const cleanup = settleCleanupPhases') ) expect(quitting).toContain('clearTimeout(runtimeTemporaryCleanupTimer)') }) it('reports completed and failed cleanup as settled', async () => { await expect( waitForCleanup(Promise.resolve(), 100) ).resolves.toBe(true) await expect( waitForCleanup(Promise.reject(new Error('cleanup failed')), 100) ).resolves.toBe(true) }) it('stops waiting after the shutdown deadline', async () => { vi.useFakeTimers() const result = waitForCleanup(new Promise(() => {}), 100) await vi.advanceTimersByTimeAsync(100) await expect(result).resolves.toBe(false) }) it('runs dependent cleanup phases in order despite failures', async () => { const order: string[] = [] await settleCleanupPhases([ [ async () => { order.push('ipc') throw new Error('cleanup failed') } ], [ async () => { order.push('gateway') } ], [ async () => { order.push('knowledge') } ] ]) expect(order).toEqual(['ipc', 'gateway', 'knowledge']) }) it('finalizes databases only after cleanup beats the deadline', async () => { const finalize = vi.fn() await expect( runCleanupBeforeDeadline(Promise.resolve(), 100, finalize) ).resolves.toBe(true) expect(finalize).toHaveBeenCalledOnce() vi.useFakeTimers() const timedOutFinalize = vi.fn() const result = runCleanupBeforeDeadline( new Promise(() => {}), 100, timedOutFinalize ) await vi.advanceTimersByTimeAsync(100) await expect(result).resolves.toBe(false) expect(timedOutFinalize).not.toHaveBeenCalled() }) })