Files
claude-hub/test/unit/providers/claude/services/SessionManager.test.ts
Jonathan Flatt 346199ebbd feat: Implement combined test coverage for main project and CLI
- Add combined coverage script to merge lcov reports
- Update GitHub workflows to generate and upload combined coverage
- Install missing CLI dependencies (ora, yaml, cli-table3, mock-fs)
- Add initial tests for SessionManager and IssueHandler
- Exclude type-only files from coverage metrics
- Update jest config to exclude type files from coverage

This ensures Codecov receives coverage data from both the main project
and CLI subdirectory, providing accurate overall project coverage metrics.
2025-06-03 22:43:20 +00:00

324 lines
9.0 KiB
TypeScript

import { SessionManager } from '../../../../../src/providers/claude/services/SessionManager';
import { execSync, spawn } from 'child_process';
import type { ClaudeSession } from '../../../../../src/types/claude-orchestration';
// Mock child_process
jest.mock('child_process', () => ({
execSync: jest.fn(),
spawn: jest.fn()
}));
// Mock logger
jest.mock('../../../../../src/utils/logger', () => ({
createLogger: () => ({
info: jest.fn(),
error: jest.fn(),
debug: jest.fn()
})
}));
describe('SessionManager', () => {
let sessionManager: SessionManager;
const mockExecSync = execSync as jest.MockedFunction<typeof execSync>;
const mockSpawn = spawn as jest.MockedFunction<typeof spawn>;
beforeEach(() => {
jest.clearAllMocks();
sessionManager = new SessionManager();
// Setup default mocks
mockExecSync.mockReturnValue(Buffer.from(''));
mockSpawn.mockReturnValue({
stdout: { on: jest.fn() },
stderr: { on: jest.fn() },
on: jest.fn()
} as any);
});
describe('createContainer', () => {
it('should create a container for a session', async () => {
const session: ClaudeSession = {
id: 'test-session-123',
type: 'analysis',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Test requirements',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const containerName = await sessionManager.createContainer(session);
expect(containerName).toBe('claude-analysis-test-ses');
expect(mockExecSync).toHaveBeenCalledWith(expect.stringContaining('docker create'), {
stdio: 'pipe'
});
});
it('should handle errors when creating container', async () => {
const session: ClaudeSession = {
id: 'test-session-123',
type: 'analysis',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Test requirements',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
mockExecSync.mockImplementation(() => {
throw new Error('Docker error');
});
await expect(sessionManager.createContainer(session)).rejects.toThrow('Docker error');
});
});
describe('startSession', () => {
it('should start a session with a container', async () => {
const session: ClaudeSession = {
id: 'test-session-123',
type: 'implementation',
status: 'pending',
containerId: 'container-123',
project: {
repository: 'owner/repo',
requirements: 'Implement feature X',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
// Mock spawn to simulate successful execution
const mockProcess = {
stdout: {
on: jest.fn((event, cb) => {
if (event === 'data') cb(Buffer.from('Output line'));
})
},
stderr: { on: jest.fn() },
on: jest.fn((event, cb) => {
if (event === 'close') cb(0);
})
};
mockSpawn.mockReturnValue(mockProcess as any);
await sessionManager.startSession(session);
expect(mockExecSync).toHaveBeenCalledWith('docker start container-123', { stdio: 'pipe' });
expect(mockSpawn).toHaveBeenCalledWith(
'docker',
expect.arrayContaining(['exec', '-i', 'container-123', 'claude'])
);
});
it('should throw error if session has no container ID', async () => {
const session: ClaudeSession = {
id: 'test-session-123',
type: 'testing',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Test requirements',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
await expect(sessionManager.startSession(session)).rejects.toThrow(
'Session has no container ID'
);
});
});
describe('getSession', () => {
it('should return a session by ID', async () => {
const session: ClaudeSession = {
id: 'test-session-123',
type: 'review',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Review code',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
await sessionManager.createContainer(session);
const retrieved = sessionManager.getSession('test-session-123');
expect(retrieved).toBeDefined();
expect(retrieved?.id).toBe('test-session-123');
});
it('should return undefined for non-existent session', () => {
const retrieved = sessionManager.getSession('non-existent');
expect(retrieved).toBeUndefined();
});
});
describe('getAllSessions', () => {
it('should return all sessions', async () => {
const session1: ClaudeSession = {
id: 'session-1',
type: 'analysis',
status: 'pending',
project: {
repository: 'owner/repo1',
requirements: 'Analyze',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const session2: ClaudeSession = {
id: 'session-2',
type: 'implementation',
status: 'pending',
project: {
repository: 'owner/repo2',
requirements: 'Implement',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
await sessionManager.createContainer(session1);
await sessionManager.createContainer(session2);
const allSessions = sessionManager.getAllSessions();
expect(allSessions).toHaveLength(2);
expect(allSessions.map(s => s.id)).toEqual(['session-1', 'session-2']);
});
});
describe('getOrchestrationSessions', () => {
it('should return sessions for a specific orchestration', async () => {
const session1: ClaudeSession = {
id: 'orch-123-session-1',
type: 'analysis',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Analyze',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const session2: ClaudeSession = {
id: 'orch-123-session-2',
type: 'implementation',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Implement',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const otherSession: ClaudeSession = {
id: 'orch-456-session-1',
type: 'testing',
status: 'pending',
project: {
repository: 'owner/repo',
requirements: 'Test',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
await sessionManager.createContainer(session1);
await sessionManager.createContainer(session2);
await sessionManager.createContainer(otherSession);
const orchSessions = sessionManager.getOrchestrationSessions('orch-123');
expect(orchSessions).toHaveLength(2);
expect(orchSessions.map(s => s.id)).toEqual(['orch-123-session-1', 'orch-123-session-2']);
});
});
describe('queueSession', () => {
it('should start session immediately if no dependencies', async () => {
const session: ClaudeSession = {
id: 'test-session',
type: 'analysis',
status: 'pending',
containerId: 'container-123',
project: {
repository: 'owner/repo',
requirements: 'Analyze',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const mockProcess = {
stdout: { on: jest.fn() },
stderr: { on: jest.fn() },
on: jest.fn((event, cb) => {
if (event === 'close') cb(0);
})
};
mockSpawn.mockReturnValue(mockProcess as any);
await sessionManager.queueSession(session);
expect(mockExecSync).toHaveBeenCalledWith('docker start container-123', { stdio: 'pipe' });
});
it('should queue session if dependencies not met', async () => {
const depSession: ClaudeSession = {
id: 'dep-session',
type: 'analysis',
status: 'running',
project: {
repository: 'owner/repo',
requirements: 'Analyze',
constraints: []
},
dependencies: [],
createdAt: new Date()
};
const session: ClaudeSession = {
id: 'test-session',
type: 'implementation',
status: 'pending',
containerId: 'container-123',
project: {
repository: 'owner/repo',
requirements: 'Implement',
constraints: []
},
dependencies: ['dep-session'],
createdAt: new Date()
};
await sessionManager.createContainer(depSession);
await sessionManager.queueSession(session);
// Should not start immediately
expect(mockSpawn).not.toHaveBeenCalled();
});
});
});