import { beforeEach, describe, expect, it, vi } from 'vitest'; const hoisted = vi.hoisted(() => { const files = new Map< string, { contents: string; size?: number; symbolicLink?: boolean; } >(); const norm = (p: string): string => p.replace(/\\/g, '/'); const lstat = vi.fn(async (filePath: string) => { const entry = files.get(norm(filePath)); if (!entry) { const error = new Error('ENOENT') as NodeJS.ErrnoException; error.code = 'ENOENT'; throw error; } return { isFile: () => !entry.symbolicLink, isSymbolicLink: () => Boolean(entry.symbolicLink), size: entry.size ?? Buffer.byteLength(entry.contents, 'utf8'), }; }); const readFile = vi.fn(async (filePath: string) => { const entry = files.get(norm(filePath)); if (!entry) { const error = new Error('ENOENT') as NodeJS.ErrnoException; error.code = 'ENOENT'; throw error; } return entry.contents; }); const access = vi.fn(async (filePath: string) => { const entry = files.get(norm(filePath)); if (!entry) { const error = new Error('ENOENT') as NodeJS.ErrnoException; error.code = 'ENOENT'; throw error; } }); const rm = vi.fn(async (filePath: string) => { files.delete(norm(filePath)); }); return { files, lstat, readFile, access, rm }; }); vi.mock('fs', async (importOriginal) => { const actual = await importOriginal(); return { ...actual, promises: { ...actual.promises, lstat: hoisted.lstat, readFile: hoisted.readFile, access: hoisted.access, rm: hoisted.rm, }, }; }); vi.mock('../../../../src/main/utils/pathDecoder', () => ({ getTeamsBasePath: () => '/mock/teams', })); import { choosePreferredLaunchSnapshot, readBootstrapLaunchSnapshot, readBootstrapRealTaskSubmissionState, readBootstrapRuntimeState, } from '../../../../src/main/services/team/TeamBootstrapStateReader'; describe('TeamBootstrapStateReader', () => { beforeEach(() => { hoisted.files.clear(); hoisted.lstat.mockClear(); hoisted.readFile.mockClear(); hoisted.access.mockClear(); hoisted.rm.mockClear(); }); it('rejects symlink bootstrap-state files', async () => { hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: '{}', symbolicLink: true, }); await expect(readBootstrapLaunchSnapshot('demo')).resolves.toBeNull(); await expect(readBootstrapRuntimeState('demo')).resolves.toBeNull(); }); it('projects active bootstrap-state into runtime progress', async () => { const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1700000001000); const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-123', teamName: 'demo', ownerPid: 4242, startedAt: 1700000000000, updatedAt: 1700000000500, phase: 'acquiring_bootstrap_lock', members: [{ name: 'alice', status: 'pending' }], }), }); hoisted.files.set('/mock/teams/demo/bootstrap-journal.jsonl', { contents: [ JSON.stringify({ ts: 1, type: 'phase', runId: 'run-123', phase: 'loading_existing_state' }), JSON.stringify({ ts: 2, type: 'lock', runId: 'run-123', action: 'acquired', ownerPid: 4242 }), JSON.stringify({ ts: 3, type: 'member', runId: 'run-123', name: 'alice', action: 'spawn_started' }), ].join('\n'), }); await expect(readBootstrapRuntimeState('demo')).resolves.toEqual({ teamName: 'demo', isAlive: false, runId: 'run-123', progress: { runId: 'run-123', teamName: 'demo', state: 'configuring', message: 'Acquiring deterministic bootstrap lock', warnings: [ 'Recent deterministic bootstrap events: bootstrap phase: loading_existing_state | bootstrap lock acquired (pid 4242) | alice: spawn_started', ], startedAt: '2023-11-14T22:13:20.000Z', updatedAt: '2023-11-14T22:13:20.500Z', pid: 4242, }, }); killSpy.mockRestore(); nowSpy.mockRestore(); }); it('surfaces unreadable bootstrap journal as a warning without breaking active recovery', async () => { const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-123', teamName: 'demo', ownerPid: 4242, startedAt: 1700000000000, updatedAt: 1700000000500, phase: 'spawning_members', members: [{ name: 'alice', status: 'pending' }], }), }); hoisted.files.set('/mock/teams/demo/bootstrap-journal.jsonl', { contents: '{invalid-json', }); await expect(readBootstrapRuntimeState('demo')).resolves.toMatchObject({ teamName: 'demo', isAlive: false, runId: 'run-123', progress: { state: 'assembling', message: 'Spawning teammate runtimes (1)', warnings: [ 'Persisted deterministic bootstrap journal is unreadable because bootstrap-journal.jsonl is invalid, truncated, or inaccessible.', ], }, }); killSpy.mockRestore(); }); it('ignores terminal bootstrap-state for runtime recovery projection', async () => { hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-123', teamName: 'demo', startedAt: 1700000000000, updatedAt: 1700000000500, phase: 'completed', terminal: { status: 'completed', finishedAt: 1700000000500, }, members: [{ name: 'alice', status: 'registered' }], }), }); await expect(readBootstrapRuntimeState('demo')).resolves.toBeNull(); }); it('reads persisted real-task submission state', async () => { hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-123', teamName: 'demo', startedAt: 1700000000000, updatedAt: 1700000000500, phase: 'completed', realTaskSubmissionState: 'submitted', members: [], }), }); await expect(readBootstrapRealTaskSubmissionState('demo')).resolves.toBe('submitted'); }); it('classifies dead bootstrap owner as failed launch snapshot instead of pending', async () => { const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1700000300000); const killSpy = vi .spyOn(process, 'kill') .mockImplementation(() => { const error = new Error('ESRCH') as NodeJS.ErrnoException; error.code = 'ESRCH'; throw error; }); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-dead', teamName: 'demo', ownerPid: 777, startedAt: 1700000000000, updatedAt: 1700000000000, phase: 'spawning_members', members: [{ name: 'alice', status: 'registered' }], }), }); await expect(readBootstrapLaunchSnapshot('demo')).resolves.toMatchObject({ launchPhase: 'finished', members: { alice: { launchState: 'failed_to_start', hardFailure: true, hardFailureReason: 'bootstrap owner pid 777 is gone and persisted bootstrap state is stale', }, }, }); killSpy.mockRestore(); nowSpy.mockRestore(); }); it('projects dead bootstrap owner into failed runtime progress', async () => { const nowSpy = vi.spyOn(Date, 'now').mockReturnValue(1700000201000); const killSpy = vi .spyOn(process, 'kill') .mockImplementation(() => { const error = new Error('ESRCH') as NodeJS.ErrnoException; error.code = 'ESRCH'; throw error; }); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: JSON.stringify({ version: 1, runId: 'run-dead', teamName: 'demo', ownerPid: 777, startedAt: 1700000000000, updatedAt: 1700000200000, phase: 'spawning_members', members: [{ name: 'alice', status: 'registered' }], }), }); await expect(readBootstrapRuntimeState('demo')).resolves.toMatchObject({ teamName: 'demo', isAlive: false, runId: 'run-dead', progress: { state: 'failed', message: 'Deterministic bootstrap owner exited before bootstrap completed', error: 'bootstrap owner pid 777 is gone before bootstrap reached a terminal state', }, }); killSpy.mockRestore(); nowSpy.mockRestore(); }); it('projects degraded runtime progress when bootstrap-state is unreadable but lock owner is alive', async () => { const killSpy = vi.spyOn(process, 'kill').mockImplementation(() => true as never); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: '{invalid-json', }); hoisted.files.set('/mock/teams/demo/.bootstrap.lock/metadata.json', { contents: JSON.stringify({ pid: 4242, runId: 'run-lock', requestHash: 'hash-1', ownerStartedAt: 1700000000000, createdAt: 1700000000100, nonce: 'nonce-1', }), }); hoisted.files.set('/mock/teams/demo/bootstrap-journal.jsonl', { contents: [ JSON.stringify({ ts: 2, type: 'phase', runId: 'run-lock', phase: 'spawning_members', }), JSON.stringify({ ts: 3, type: 'member', runId: 'run-lock', name: 'alice', action: 'spawn_started', }), ].join('\n'), }); await expect(readBootstrapRuntimeState('demo')).resolves.toMatchObject({ teamName: 'demo', isAlive: false, runId: 'run-lock', progress: { state: 'assembling', message: 'Spawning teammate runtimes (degraded recovery)', messageSeverity: 'warning', pid: 4242, warnings: [ 'Persisted deterministic bootstrap state is unreadable because bootstrap-state.json is invalid, truncated, or inaccessible.', 'Recent deterministic bootstrap events: bootstrap phase: spawning_members | alice: spawn_started', ], }, }); killSpy.mockRestore(); }); it('projects degraded failed runtime progress when bootstrap-state is unreadable and lock owner is dead', async () => { const killSpy = vi .spyOn(process, 'kill') .mockImplementation(() => { const error = new Error('ESRCH') as NodeJS.ErrnoException; error.code = 'ESRCH'; throw error; }); hoisted.files.set('/mock/teams/demo/bootstrap-state.json', { contents: '{invalid-json', }); hoisted.files.set('/mock/teams/demo/.bootstrap.lock/metadata.json', { contents: JSON.stringify({ pid: 7331, runId: 'run-dead-lock', requestHash: 'hash-2', ownerStartedAt: 1700000000000, createdAt: 1700000000100, nonce: 'nonce-2', }), }); await expect(readBootstrapRuntimeState('demo')).resolves.toMatchObject({ teamName: 'demo', isAlive: false, runId: 'run-dead-lock', progress: { state: 'failed', message: 'Deterministic bootstrap recovery failed because persisted bootstrap state is unreadable and the bootstrap owner is gone', messageSeverity: 'warning', pid: 7331, }, }); killSpy.mockRestore(); }); it('prefers the newer launch snapshot when bootstrap snapshot is stale', () => { const preferred = choosePreferredLaunchSnapshot( { updatedAt: '2026-04-06T10:00:00.000Z', kind: 'bootstrap' }, { updatedAt: '2026-04-06T10:05:00.000Z', kind: 'launch' } ); expect(preferred).toEqual({ updatedAt: '2026-04-06T10:05:00.000Z', kind: 'launch', }); }); });