Files
automaker/apps/server/tests/unit/services/agent-service.test.ts
Test User ce78165b59 fix: update test expectations for file read calls in agent-service
- Adjusted the test to reflect the addition of queue state file reading, increasing the expected number of file read calls from 2 to 3.
- Updated comments for clarity regarding the file reading process in the agent-service tests.
2025-12-26 11:17:21 -05:00

351 lines
9.9 KiB
TypeScript

import { describe, it, expect, vi, beforeEach } from 'vitest';
import { AgentService } from '@/services/agent-service.js';
import { ProviderFactory } from '@/providers/provider-factory.js';
import * as fs from 'fs/promises';
import * as imageHandler from '@automaker/utils';
import * as promptBuilder from '@automaker/utils';
import * as contextLoader from '@automaker/utils';
import { collectAsyncGenerator } from '../../utils/helpers.js';
vi.mock('fs/promises');
vi.mock('@/providers/provider-factory.js');
vi.mock('@automaker/utils');
describe('agent-service.ts', () => {
let service: AgentService;
const mockEvents = {
subscribe: vi.fn(),
emit: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
service = new AgentService('/test/data', mockEvents as any);
// Mock loadContextFiles to return empty context by default
vi.mocked(contextLoader.loadContextFiles).mockResolvedValue({
files: [],
formattedPrompt: '',
});
});
describe('initialize', () => {
it('should create state directory', async () => {
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.initialize();
expect(fs.mkdir).toHaveBeenCalledWith(expect.stringContaining('agent-sessions'), {
recursive: true,
});
});
});
describe('startConversation', () => {
it('should create new session with empty messages', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
const result = await service.startConversation({
sessionId: 'session-1',
workingDirectory: '/test/dir',
});
expect(result.success).toBe(true);
expect(result.messages).toEqual([]);
expect(result.sessionId).toBe('session-1');
});
it('should load existing session', async () => {
const existingMessages = [
{
id: 'msg-1',
role: 'user',
content: 'Hello',
timestamp: '2024-01-01T00:00:00Z',
},
];
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify(existingMessages));
const result = await service.startConversation({
sessionId: 'session-1',
workingDirectory: '/test/dir',
});
expect(result.success).toBe(true);
expect(result.messages).toEqual(existingMessages);
});
it('should use process.cwd() if no working directory provided', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
const result = await service.startConversation({
sessionId: 'session-1',
});
expect(result.success).toBe(true);
});
it('should reuse existing session if already started', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
// Start session first time
await service.startConversation({
sessionId: 'session-1',
});
// Start again with same ID
const result = await service.startConversation({
sessionId: 'session-1',
});
expect(result.success).toBe(true);
// First call reads session file, metadata file, and queue state file (3 calls)
// Second call should reuse in-memory session (no additional calls)
expect(fs.readFile).toHaveBeenCalledTimes(3);
});
});
describe('sendMessage', () => {
beforeEach(async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.startConversation({
sessionId: 'session-1',
workingDirectory: '/test/dir',
});
});
it('should throw if session not found', async () => {
await expect(
service.sendMessage({
sessionId: 'nonexistent',
message: 'Hello',
})
).rejects.toThrow('Session nonexistent not found');
});
it('should process message and stream responses', async () => {
const mockProvider = {
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: 'assistant',
message: {
role: 'assistant',
content: [{ type: 'text', text: 'Response' }],
},
};
yield {
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
content: 'Hello',
hasImages: false,
});
const result = await service.sendMessage({
sessionId: 'session-1',
message: 'Hello',
workingDirectory: '/custom/dir',
});
expect(result.success).toBe(true);
expect(mockEvents.emit).toHaveBeenCalled();
});
it('should handle images in message', async () => {
const mockProvider = {
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(imageHandler.readImageAsBase64).mockResolvedValue({
base64: 'base64data',
mimeType: 'image/png',
filename: 'test.png',
originalPath: '/path/test.png',
});
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
content: 'Check image',
hasImages: true,
});
await service.sendMessage({
sessionId: 'session-1',
message: 'Check this',
imagePaths: ['/path/test.png'],
});
expect(imageHandler.readImageAsBase64).toHaveBeenCalledWith('/path/test.png');
});
it('should handle failed image loading gracefully', async () => {
const mockProvider = {
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(imageHandler.readImageAsBase64).mockRejectedValue(new Error('Image not found'));
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
content: 'Check image',
hasImages: false,
});
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
await service.sendMessage({
sessionId: 'session-1',
message: 'Check this',
imagePaths: ['/path/test.png'],
});
expect(consoleSpy).toHaveBeenCalled();
consoleSpy.mockRestore();
});
it('should use custom model if provided', async () => {
const mockProvider = {
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
content: 'Hello',
hasImages: false,
});
await service.sendMessage({
sessionId: 'session-1',
message: 'Hello',
model: 'claude-sonnet-4-20250514',
});
expect(ProviderFactory.getProviderForModel).toHaveBeenCalledWith('claude-sonnet-4-20250514');
});
it('should save session messages', async () => {
const mockProvider = {
getName: () => 'claude',
executeQuery: async function* () {
yield {
type: 'result',
subtype: 'success',
};
},
};
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
vi.mocked(promptBuilder.buildPromptWithImages).mockResolvedValue({
content: 'Hello',
hasImages: false,
});
await service.sendMessage({
sessionId: 'session-1',
message: 'Hello',
});
expect(fs.writeFile).toHaveBeenCalled();
});
});
describe('stopExecution', () => {
it('should stop execution for a session', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
await service.startConversation({
sessionId: 'session-1',
});
// Should return success
const result = await service.stopExecution('session-1');
expect(result.success).toBeDefined();
});
});
describe('getHistory', () => {
it('should return message history', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
await service.startConversation({
sessionId: 'session-1',
});
const history = service.getHistory('session-1');
expect(history).toBeDefined();
expect(history?.messages).toEqual([]);
});
it('should handle non-existent session', () => {
const history = service.getHistory('nonexistent');
expect(history).toBeDefined(); // Returns error object
});
});
describe('clearSession', () => {
it('should clear session messages', async () => {
const error: any = new Error('ENOENT');
error.code = 'ENOENT';
vi.mocked(fs.readFile).mockRejectedValue(error);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
await service.startConversation({
sessionId: 'session-1',
});
await service.clearSession('session-1');
const history = service.getHistory('session-1');
expect(history?.messages).toEqual([]);
expect(fs.writeFile).toHaveBeenCalled();
});
});
});