mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-01-30 06:12:03 +00:00
test: add comprehensive unit tests for IdeationService
- Add 28 unit tests covering all major IdeationService functionality - Test session management (start, get, stop, running state) - Test idea CRUD operations (create, read, update, delete, archive) - Test idea to feature conversion with user stories and notes - Test project analysis and caching - Test prompt management and filtering - Test AI-powered suggestion generation - Mock all external dependencies (fs, platform, utils, providers) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Web Dev Cody <webdevcody@users.noreply.github.com>
This commit is contained in:
763
apps/server/tests/unit/services/ideation-service.test.ts
Normal file
763
apps/server/tests/unit/services/ideation-service.test.ts
Normal file
@@ -0,0 +1,763 @@
|
||||
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
||||
import { IdeationService } from '@/services/ideation-service.js';
|
||||
import type { EventEmitter } from '@/lib/events.js';
|
||||
import type { SettingsService } from '@/services/settings-service.js';
|
||||
import type { FeatureLoader } from '@/services/feature-loader.js';
|
||||
import * as secureFs from '@/lib/secure-fs.js';
|
||||
import * as platform from '@automaker/platform';
|
||||
import * as utils from '@automaker/utils';
|
||||
import type {
|
||||
CreateIdeaInput,
|
||||
UpdateIdeaInput,
|
||||
Idea,
|
||||
IdeationSession,
|
||||
StartSessionOptions,
|
||||
} from '@automaker/types';
|
||||
import { ProviderFactory } from '@/providers/provider-factory.js';
|
||||
|
||||
// Mock dependencies
|
||||
vi.mock('@/lib/secure-fs.js');
|
||||
vi.mock('@automaker/platform');
|
||||
vi.mock('@automaker/utils');
|
||||
vi.mock('@/providers/provider-factory.js');
|
||||
vi.mock('@/lib/sdk-options.js', () => ({
|
||||
createChatOptions: vi.fn(() => ({
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
systemPrompt: 'test prompt',
|
||||
})),
|
||||
validateWorkingDirectory: vi.fn(),
|
||||
}));
|
||||
|
||||
describe('IdeationService', () => {
|
||||
let service: IdeationService;
|
||||
let mockEvents: EventEmitter;
|
||||
let mockSettingsService: SettingsService;
|
||||
let mockFeatureLoader: FeatureLoader;
|
||||
const testProjectPath = '/test/project';
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
// Create mock event emitter
|
||||
mockEvents = {
|
||||
emit: vi.fn(),
|
||||
on: vi.fn(),
|
||||
off: vi.fn(),
|
||||
removeAllListeners: vi.fn(),
|
||||
} as unknown as EventEmitter;
|
||||
|
||||
// Create mock settings service
|
||||
mockSettingsService = {} as SettingsService;
|
||||
|
||||
// Create mock feature loader
|
||||
mockFeatureLoader = {
|
||||
getAll: vi.fn().mockResolvedValue([]),
|
||||
} as unknown as FeatureLoader;
|
||||
|
||||
// Mock platform functions
|
||||
vi.mocked(platform.ensureIdeationDir).mockResolvedValue(undefined);
|
||||
vi.mocked(platform.getIdeaDir).mockReturnValue('/test/project/.automaker/ideation/ideas/idea-123');
|
||||
vi.mocked(platform.getIdeaPath).mockReturnValue(
|
||||
'/test/project/.automaker/ideation/ideas/idea-123/idea.json'
|
||||
);
|
||||
vi.mocked(platform.getIdeasDir).mockReturnValue('/test/project/.automaker/ideation/ideas');
|
||||
vi.mocked(platform.getIdeationSessionPath).mockReturnValue(
|
||||
'/test/project/.automaker/ideation/sessions/session-123.json'
|
||||
);
|
||||
vi.mocked(platform.getIdeationSessionsDir).mockReturnValue(
|
||||
'/test/project/.automaker/ideation/sessions'
|
||||
);
|
||||
vi.mocked(platform.getIdeationAnalysisPath).mockReturnValue(
|
||||
'/test/project/.automaker/ideation/analysis.json'
|
||||
);
|
||||
|
||||
// Mock utils
|
||||
vi.mocked(utils.loadContextFiles).mockResolvedValue({
|
||||
formattedPrompt: 'Test context',
|
||||
files: [],
|
||||
});
|
||||
vi.mocked(utils.isAbortError).mockReturnValue(false);
|
||||
|
||||
service = new IdeationService(mockEvents, mockSettingsService, mockFeatureLoader);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Session Management Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Session Management', () => {
|
||||
describe('startSession', () => {
|
||||
it('should create a new session with default options', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const session = await service.startSession(testProjectPath);
|
||||
|
||||
expect(session).toBeDefined();
|
||||
expect(session.id).toMatch(/^session-/);
|
||||
expect(session.projectPath).toBe(testProjectPath);
|
||||
expect(session.status).toBe('active');
|
||||
expect(session.createdAt).toBeDefined();
|
||||
expect(session.updatedAt).toBeDefined();
|
||||
expect(platform.ensureIdeationDir).toHaveBeenCalledWith(testProjectPath);
|
||||
expect(secureFs.writeFile).toHaveBeenCalled();
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith('ideation:session-started', {
|
||||
sessionId: session.id,
|
||||
projectPath: testProjectPath,
|
||||
});
|
||||
});
|
||||
|
||||
it('should create session with custom options', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const options: StartSessionOptions = {
|
||||
promptCategory: 'features',
|
||||
promptId: 'new-features',
|
||||
};
|
||||
|
||||
const session = await service.startSession(testProjectPath, options);
|
||||
|
||||
expect(session.promptCategory).toBe('features');
|
||||
expect(session.promptId).toBe('new-features');
|
||||
});
|
||||
|
||||
it('should send initial message if provided in options', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify({ features: [] }));
|
||||
|
||||
// Mock provider
|
||||
const mockProvider = {
|
||||
executeQuery: vi.fn().mockReturnValue({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
result: 'AI response',
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
|
||||
|
||||
const options: StartSessionOptions = {
|
||||
initialMessage: 'Hello, AI!',
|
||||
};
|
||||
|
||||
await service.startSession(testProjectPath, options);
|
||||
|
||||
// Give time for the async message to process
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
|
||||
expect(mockProvider.executeQuery).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSession', () => {
|
||||
it('should return null for non-existent session', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const result = await service.getSession(testProjectPath, 'non-existent');
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
|
||||
it('should return active session from memory', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const session = await service.startSession(testProjectPath);
|
||||
const retrieved = await service.getSession(testProjectPath, session.id);
|
||||
|
||||
expect(retrieved).toBeDefined();
|
||||
expect(retrieved?.id).toBe(session.id);
|
||||
expect(retrieved?.messages).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load session from disk if not in memory', async () => {
|
||||
const mockSession: IdeationSession = {
|
||||
id: 'session-123',
|
||||
projectPath: testProjectPath,
|
||||
status: 'active',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const sessionData = {
|
||||
session: mockSession,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(sessionData));
|
||||
|
||||
const result = await service.getSession(testProjectPath, 'session-123');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.id).toBe('session-123');
|
||||
});
|
||||
});
|
||||
|
||||
describe('stopSession', () => {
|
||||
it('should stop an active session', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const session = await service.startSession(testProjectPath);
|
||||
await service.stopSession(session.id);
|
||||
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith('ideation:session-ended', {
|
||||
sessionId: session.id,
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle stopping non-existent session gracefully', async () => {
|
||||
await expect(service.stopSession('non-existent')).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('isSessionRunning', () => {
|
||||
it('should return false for non-existent session', () => {
|
||||
expect(service.isSessionRunning('non-existent')).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for idle session', async () => {
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const session = await service.startSession(testProjectPath);
|
||||
expect(service.isSessionRunning(session.id)).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Ideas CRUD Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Ideas CRUD', () => {
|
||||
describe('createIdea', () => {
|
||||
it('should create a new idea with required fields', async () => {
|
||||
vi.mocked(secureFs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const input: CreateIdeaInput = {
|
||||
title: 'Test Idea',
|
||||
description: 'This is a test idea',
|
||||
category: 'features',
|
||||
};
|
||||
|
||||
const idea = await service.createIdea(testProjectPath, input);
|
||||
|
||||
expect(idea).toBeDefined();
|
||||
expect(idea.id).toMatch(/^idea-/);
|
||||
expect(idea.title).toBe('Test Idea');
|
||||
expect(idea.description).toBe('This is a test idea');
|
||||
expect(idea.category).toBe('features');
|
||||
expect(idea.status).toBe('raw');
|
||||
expect(idea.impact).toBe('medium');
|
||||
expect(idea.effort).toBe('medium');
|
||||
expect(secureFs.mkdir).toHaveBeenCalled();
|
||||
expect(secureFs.writeFile).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should create idea with all optional fields', async () => {
|
||||
vi.mocked(secureFs.mkdir).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const input: CreateIdeaInput = {
|
||||
title: 'Full Idea',
|
||||
description: 'Complete idea',
|
||||
category: 'features',
|
||||
status: 'refined',
|
||||
impact: 'high',
|
||||
effort: 'low',
|
||||
conversationId: 'conv-123',
|
||||
sourcePromptId: 'prompt-123',
|
||||
userStories: ['Story 1', 'Story 2'],
|
||||
notes: 'Additional notes',
|
||||
};
|
||||
|
||||
const idea = await service.createIdea(testProjectPath, input);
|
||||
|
||||
expect(idea.status).toBe('refined');
|
||||
expect(idea.impact).toBe('high');
|
||||
expect(idea.effort).toBe('low');
|
||||
expect(idea.conversationId).toBe('conv-123');
|
||||
expect(idea.sourcePromptId).toBe('prompt-123');
|
||||
expect(idea.userStories).toEqual(['Story 1', 'Story 2']);
|
||||
expect(idea.notes).toBe('Additional notes');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIdeas', () => {
|
||||
it('should return empty array when ideas directory does not exist', async () => {
|
||||
vi.mocked(secureFs.access).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const ideas = await service.getIdeas(testProjectPath);
|
||||
|
||||
expect(ideas).toEqual([]);
|
||||
});
|
||||
|
||||
it('should load all ideas from disk', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.readdir).mockResolvedValue([
|
||||
{ name: 'idea-1', isDirectory: () => true } as any,
|
||||
{ name: 'idea-2', isDirectory: () => true } as any,
|
||||
]);
|
||||
|
||||
const idea1: Idea = {
|
||||
id: 'idea-1',
|
||||
title: 'Idea 1',
|
||||
description: 'First idea',
|
||||
category: 'features',
|
||||
status: 'raw',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
const idea2: Idea = {
|
||||
id: 'idea-2',
|
||||
title: 'Idea 2',
|
||||
description: 'Second idea',
|
||||
category: 'bugs',
|
||||
status: 'refined',
|
||||
impact: 'high',
|
||||
effort: 'low',
|
||||
createdAt: '2024-01-02T00:00:00.000Z',
|
||||
updatedAt: '2024-01-02T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile)
|
||||
.mockResolvedValueOnce(JSON.stringify(idea1))
|
||||
.mockResolvedValueOnce(JSON.stringify(idea2));
|
||||
|
||||
const ideas = await service.getIdeas(testProjectPath);
|
||||
|
||||
expect(ideas).toHaveLength(2);
|
||||
expect(ideas[0].id).toBe('idea-2'); // Sorted by updatedAt descending
|
||||
expect(ideas[1].id).toBe('idea-1');
|
||||
});
|
||||
|
||||
it('should skip invalid idea files', async () => {
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.readdir).mockResolvedValue([
|
||||
{ name: 'idea-1', isDirectory: () => true } as any,
|
||||
{ name: 'idea-2', isDirectory: () => true } as any,
|
||||
]);
|
||||
|
||||
const validIdea: Idea = {
|
||||
id: 'idea-1',
|
||||
title: 'Valid Idea',
|
||||
description: 'Valid',
|
||||
category: 'features',
|
||||
status: 'raw',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile)
|
||||
.mockResolvedValueOnce(JSON.stringify(validIdea))
|
||||
.mockRejectedValueOnce(new Error('Invalid JSON'));
|
||||
|
||||
const ideas = await service.getIdeas(testProjectPath);
|
||||
|
||||
expect(ideas).toHaveLength(1);
|
||||
expect(ideas[0].id).toBe('idea-1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getIdea', () => {
|
||||
it('should return idea by id', async () => {
|
||||
const mockIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Test Idea',
|
||||
description: 'Test',
|
||||
category: 'features',
|
||||
status: 'raw',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));
|
||||
|
||||
const idea = await service.getIdea(testProjectPath, 'idea-123');
|
||||
|
||||
expect(idea).toBeDefined();
|
||||
expect(idea?.id).toBe('idea-123');
|
||||
});
|
||||
|
||||
it('should return null for non-existent idea', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const idea = await service.getIdea(testProjectPath, 'non-existent');
|
||||
|
||||
expect(idea).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('updateIdea', () => {
|
||||
it('should update idea fields', async () => {
|
||||
const existingIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Original Title',
|
||||
description: 'Original',
|
||||
category: 'features',
|
||||
status: 'raw',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(existingIdea));
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const updates: UpdateIdeaInput = {
|
||||
title: 'Updated Title',
|
||||
status: 'refined',
|
||||
};
|
||||
|
||||
const updated = await service.updateIdea(testProjectPath, 'idea-123', updates);
|
||||
|
||||
expect(updated).toBeDefined();
|
||||
expect(updated?.title).toBe('Updated Title');
|
||||
expect(updated?.status).toBe('refined');
|
||||
expect(updated?.description).toBe('Original'); // Unchanged
|
||||
expect(updated?.updatedAt).not.toBe('2024-01-01T00:00:00.000Z'); // Should be updated
|
||||
});
|
||||
|
||||
it('should return null for non-existent idea', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const updated = await service.updateIdea(testProjectPath, 'non-existent', {
|
||||
title: 'New Title',
|
||||
});
|
||||
|
||||
expect(updated).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteIdea', () => {
|
||||
it('should delete idea directory', async () => {
|
||||
vi.mocked(secureFs.rm).mockResolvedValue(undefined);
|
||||
|
||||
await service.deleteIdea(testProjectPath, 'idea-123');
|
||||
|
||||
expect(secureFs.rm).toHaveBeenCalledWith(
|
||||
expect.stringContaining('idea-123'),
|
||||
expect.objectContaining({ recursive: true })
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle non-existent idea gracefully', async () => {
|
||||
vi.mocked(secureFs.rm).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
await expect(service.deleteIdea(testProjectPath, 'non-existent')).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('archiveIdea', () => {
|
||||
it('should set idea status to archived', async () => {
|
||||
const existingIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Test',
|
||||
description: 'Test',
|
||||
category: 'features',
|
||||
status: 'raw',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(existingIdea));
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
|
||||
const archived = await service.archiveIdea(testProjectPath, 'idea-123');
|
||||
|
||||
expect(archived).toBeDefined();
|
||||
expect(archived?.status).toBe('archived');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Conversion Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Idea to Feature Conversion', () => {
|
||||
describe('convertToFeature', () => {
|
||||
it('should convert idea to feature with basic fields', async () => {
|
||||
const mockIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Add Dark Mode',
|
||||
description: 'Implement dark mode theme',
|
||||
category: 'features',
|
||||
status: 'refined',
|
||||
impact: 'high',
|
||||
effort: 'medium',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));
|
||||
|
||||
const feature = await service.convertToFeature(testProjectPath, 'idea-123');
|
||||
|
||||
expect(feature).toBeDefined();
|
||||
expect(feature.id).toMatch(/^feature-/);
|
||||
expect(feature.title).toBe('Add Dark Mode');
|
||||
expect(feature.description).toBe('Implement dark mode theme');
|
||||
expect(feature.category).toBe('ui'); // features -> ui mapping
|
||||
expect(feature.status).toBe('backlog');
|
||||
});
|
||||
|
||||
it('should include user stories in feature description', async () => {
|
||||
const mockIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Test',
|
||||
description: 'Base description',
|
||||
category: 'features',
|
||||
status: 'refined',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
userStories: ['As a user, I want X', 'As a user, I want Y'],
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));
|
||||
|
||||
const feature = await service.convertToFeature(testProjectPath, 'idea-123');
|
||||
|
||||
expect(feature.description).toContain('Base description');
|
||||
expect(feature.description).toContain('## User Stories');
|
||||
expect(feature.description).toContain('As a user, I want X');
|
||||
expect(feature.description).toContain('As a user, I want Y');
|
||||
});
|
||||
|
||||
it('should include notes in feature description', async () => {
|
||||
const mockIdea: Idea = {
|
||||
id: 'idea-123',
|
||||
title: 'Test',
|
||||
description: 'Base description',
|
||||
category: 'features',
|
||||
status: 'refined',
|
||||
impact: 'medium',
|
||||
effort: 'medium',
|
||||
notes: 'Important implementation notes',
|
||||
createdAt: '2024-01-01T00:00:00.000Z',
|
||||
updatedAt: '2024-01-01T00:00:00.000Z',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockIdea));
|
||||
|
||||
const feature = await service.convertToFeature(testProjectPath, 'idea-123');
|
||||
|
||||
expect(feature.description).toContain('Base description');
|
||||
expect(feature.description).toContain('## Notes');
|
||||
expect(feature.description).toContain('Important implementation notes');
|
||||
});
|
||||
|
||||
it('should throw error for non-existent idea', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
await expect(service.convertToFeature(testProjectPath, 'non-existent')).rejects.toThrow(
|
||||
'Idea non-existent not found'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Project Analysis Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Project Analysis', () => {
|
||||
describe('analyzeProject', () => {
|
||||
it('should analyze project and generate suggestions', async () => {
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(
|
||||
JSON.stringify({
|
||||
name: 'test-project',
|
||||
dependencies: {},
|
||||
})
|
||||
);
|
||||
vi.mocked(secureFs.writeFile).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(secureFs.readdir).mockResolvedValue([]);
|
||||
|
||||
const result = await service.analyzeProject(testProjectPath);
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result.projectPath).toBe(testProjectPath);
|
||||
expect(result.analyzedAt).toBeDefined();
|
||||
expect(result.suggestions).toBeDefined();
|
||||
expect(Array.isArray(result.suggestions)).toBe(true);
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith(
|
||||
'ideation:analysis',
|
||||
expect.objectContaining({
|
||||
type: 'ideation:analysis-started',
|
||||
})
|
||||
);
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith(
|
||||
'ideation:analysis',
|
||||
expect.objectContaining({
|
||||
type: 'ideation:analysis-complete',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should emit error event on failure', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('Read failed'));
|
||||
|
||||
await expect(service.analyzeProject(testProjectPath)).rejects.toThrow();
|
||||
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith(
|
||||
'ideation:analysis',
|
||||
expect.objectContaining({
|
||||
type: 'ideation:analysis-error',
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCachedAnalysis', () => {
|
||||
it('should return cached analysis if exists', async () => {
|
||||
const mockAnalysis = {
|
||||
projectPath: testProjectPath,
|
||||
analyzedAt: '2024-01-01T00:00:00.000Z',
|
||||
totalFiles: 10,
|
||||
suggestions: [],
|
||||
summary: 'Test summary',
|
||||
};
|
||||
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify(mockAnalysis));
|
||||
|
||||
const result = await service.getCachedAnalysis(testProjectPath);
|
||||
|
||||
expect(result).toEqual(mockAnalysis);
|
||||
});
|
||||
|
||||
it('should return null if cache does not exist', async () => {
|
||||
vi.mocked(secureFs.readFile).mockRejectedValue(new Error('ENOENT'));
|
||||
|
||||
const result = await service.getCachedAnalysis(testProjectPath);
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Prompt Management Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Prompt Management', () => {
|
||||
describe('getPromptCategories', () => {
|
||||
it('should return list of prompt categories', () => {
|
||||
const categories = service.getPromptCategories();
|
||||
|
||||
expect(Array.isArray(categories)).toBe(true);
|
||||
expect(categories.length).toBeGreaterThan(0);
|
||||
expect(categories[0]).toHaveProperty('id');
|
||||
expect(categories[0]).toHaveProperty('label');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getAllPrompts', () => {
|
||||
it('should return all guided prompts', () => {
|
||||
const prompts = service.getAllPrompts();
|
||||
|
||||
expect(Array.isArray(prompts)).toBe(true);
|
||||
expect(prompts.length).toBeGreaterThan(0);
|
||||
expect(prompts[0]).toHaveProperty('id');
|
||||
expect(prompts[0]).toHaveProperty('category');
|
||||
expect(prompts[0]).toHaveProperty('title');
|
||||
expect(prompts[0]).toHaveProperty('prompt');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPromptsByCategory', () => {
|
||||
it('should return prompts filtered by category', () => {
|
||||
const allPrompts = service.getAllPrompts();
|
||||
const firstCategory = allPrompts[0].category;
|
||||
|
||||
const filtered = service.getPromptsByCategory(firstCategory);
|
||||
|
||||
expect(Array.isArray(filtered)).toBe(true);
|
||||
filtered.forEach((prompt) => {
|
||||
expect(prompt.category).toBe(firstCategory);
|
||||
});
|
||||
});
|
||||
|
||||
it('should return empty array for non-existent category', () => {
|
||||
const filtered = service.getPromptsByCategory('non-existent-category' as any);
|
||||
|
||||
expect(filtered).toEqual([]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// Suggestions Generation Tests
|
||||
// ============================================================================
|
||||
|
||||
describe('Suggestion Generation', () => {
|
||||
describe('generateSuggestions', () => {
|
||||
it('should generate suggestions for a prompt', async () => {
|
||||
vi.mocked(secureFs.readFile).mockResolvedValue(JSON.stringify({}));
|
||||
|
||||
const mockProvider = {
|
||||
executeQuery: vi.fn().mockReturnValue({
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
type: 'result',
|
||||
subtype: 'success',
|
||||
result: JSON.stringify([
|
||||
{
|
||||
title: 'Add user authentication',
|
||||
description: 'Implement auth',
|
||||
category: 'security',
|
||||
impact: 'high',
|
||||
effort: 'high',
|
||||
},
|
||||
]),
|
||||
};
|
||||
},
|
||||
}),
|
||||
};
|
||||
|
||||
vi.mocked(ProviderFactory.getProviderForModel).mockReturnValue(mockProvider as any);
|
||||
|
||||
const prompts = service.getAllPrompts();
|
||||
const firstPrompt = prompts[0];
|
||||
|
||||
const suggestions = await service.generateSuggestions(
|
||||
testProjectPath,
|
||||
firstPrompt.id,
|
||||
'features',
|
||||
5
|
||||
);
|
||||
|
||||
expect(Array.isArray(suggestions)).toBe(true);
|
||||
expect(mockEvents.emit).toHaveBeenCalledWith(
|
||||
'ideation:suggestions',
|
||||
expect.objectContaining({
|
||||
type: 'started',
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw error for non-existent prompt', async () => {
|
||||
await expect(
|
||||
service.generateSuggestions(testProjectPath, 'non-existent', 'features', 5)
|
||||
).rejects.toThrow('Prompt non-existent not found');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user