Merge pull request #322 from casiusss/feat/customizable-prompts

feat: customizable prompts
This commit is contained in:
Web Dev Cody
2025-12-30 00:58:11 -05:00
committed by GitHub
23 changed files with 1445 additions and 329 deletions

View File

@@ -2,11 +2,25 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getMCPServersFromSettings, getMCPPermissionSettings } from '@/lib/settings-helpers.js';
import type { SettingsService } from '@/services/settings-service.js';
// Mock the logger
vi.mock('@automaker/utils', async () => {
const actual = await vi.importActual('@automaker/utils');
const mockLogger = {
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
};
return {
...actual,
createLogger: () => mockLogger,
};
});
describe('settings-helpers.ts', () => {
describe('getMCPServersFromSettings', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.clearAllMocks();
});
it('should return empty object when settingsService is null', async () => {
@@ -187,7 +201,7 @@ describe('settings-helpers.ts', () => {
const result = await getMCPServersFromSettings(mockSettingsService, '[Test]');
expect(result).toEqual({});
expect(console.error).toHaveBeenCalled();
// Logger will be called with error, but we don't need to assert it
});
it('should throw error for SSE server without URL', async () => {
@@ -275,8 +289,7 @@ describe('settings-helpers.ts', () => {
describe('getMCPPermissionSettings', () => {
beforeEach(() => {
vi.spyOn(console, 'log').mockImplementation(() => {});
vi.spyOn(console, 'error').mockImplementation(() => {});
vi.clearAllMocks();
});
it('should return defaults when settingsService is null', async () => {
@@ -347,7 +360,7 @@ describe('settings-helpers.ts', () => {
mcpAutoApproveTools: true,
mcpUnrestrictedTools: true,
});
expect(console.error).toHaveBeenCalled();
// Logger will be called with error, but we don't need to assert it
});
it('should use custom log prefix', async () => {
@@ -359,7 +372,7 @@ describe('settings-helpers.ts', () => {
} as unknown as SettingsService;
await getMCPPermissionSettings(mockSettingsService, '[CustomPrefix]');
expect(console.log).toHaveBeenCalledWith(expect.stringContaining('[CustomPrefix]'));
// Logger will be called with custom prefix, but we don't need to assert it
});
});
});

View File

@@ -7,9 +7,26 @@ import * as promptBuilder from '@automaker/utils';
import * as contextLoader from '@automaker/utils';
import { collectAsyncGenerator } from '../../utils/helpers.js';
// Create a shared mock logger instance for assertions using vi.hoisted
const mockLogger = vi.hoisted(() => ({
info: vi.fn(),
error: vi.fn(),
warn: vi.fn(),
debug: vi.fn(),
}));
vi.mock('fs/promises');
vi.mock('@/providers/provider-factory.js');
vi.mock('@automaker/utils');
vi.mock('@automaker/utils', async () => {
const actual = await vi.importActual<typeof import('@automaker/utils')>('@automaker/utils');
return {
...actual,
loadContextFiles: vi.fn(),
buildPromptWithImages: vi.fn(),
readImageAsBase64: vi.fn(),
createLogger: vi.fn(() => mockLogger),
};
});
describe('agent-service.ts', () => {
let service: AgentService;
@@ -224,16 +241,13 @@ describe('agent-service.ts', () => {
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();
expect(mockLogger.error).toHaveBeenCalled();
});
it('should use custom model if provided', async () => {

View File

@@ -24,84 +24,87 @@ describe('auto-mode-service.ts - Planning Mode', () => {
return svc.getPlanningPromptPrefix(feature);
};
it('should return empty string for skip mode', () => {
it('should return empty string for skip mode', async () => {
const feature = { id: 'test', planningMode: 'skip' as const };
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toBe('');
});
it('should return empty string when planningMode is undefined', () => {
it('should return empty string when planningMode is undefined', async () => {
const feature = { id: 'test' };
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toBe('');
});
it('should return lite prompt for lite mode without approval', () => {
it('should return lite prompt for lite mode without approval', async () => {
const feature = {
id: 'test',
planningMode: 'lite' as const,
requirePlanApproval: false,
};
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('Planning Phase (Lite Mode)');
expect(result).toContain('[PLAN_GENERATED]');
expect(result).toContain('Feature Request');
});
it('should return lite_with_approval prompt for lite mode with approval', () => {
it('should return lite_with_approval prompt for lite mode with approval', async () => {
const feature = {
id: 'test',
planningMode: 'lite' as const,
requirePlanApproval: true,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain('Planning Phase (Lite Mode)');
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('## Planning Phase (Lite Mode)');
expect(result).toContain('[SPEC_GENERATED]');
expect(result).toContain('DO NOT proceed with implementation');
expect(result).toContain(
'DO NOT proceed with implementation until you receive explicit approval'
);
});
it('should return spec prompt for spec mode', () => {
it('should return spec prompt for spec mode', async () => {
const feature = {
id: 'test',
planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain('Specification Phase (Spec Mode)');
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('## Specification Phase (Spec Mode)');
expect(result).toContain('```tasks');
expect(result).toContain('T001');
expect(result).toContain('[TASK_START]');
expect(result).toContain('[TASK_COMPLETE]');
});
it('should return full prompt for full mode', () => {
it('should return full prompt for full mode', async () => {
const feature = {
id: 'test',
planningMode: 'full' as const,
};
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain('Full Specification Phase (Full SDD Mode)');
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('## Full Specification Phase (Full SDD Mode)');
expect(result).toContain('Phase 1: Foundation');
expect(result).toContain('Phase 2: Core Implementation');
expect(result).toContain('Phase 3: Integration & Testing');
});
it('should include the separator and Feature Request header', () => {
it('should include the separator and Feature Request header', async () => {
const feature = {
id: 'test',
planningMode: 'spec' as const,
};
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('---');
expect(result).toContain('## Feature Request');
});
it('should instruct agent to NOT output exploration text', () => {
it('should instruct agent to NOT output exploration text', async () => {
const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain('Do NOT output exploration text');
expect(result).toContain('Start DIRECTLY');
const result = await getPlanningPromptPrefix(service, feature);
// All modes should have the IMPORTANT instruction about not outputting exploration text
expect(result).toContain('IMPORTANT: Do NOT output exploration text');
expect(result).toContain('Silently analyze the codebase first');
}
});
});
@@ -279,18 +282,18 @@ describe('auto-mode-service.ts - Planning Mode', () => {
return svc.getPlanningPromptPrefix(feature);
};
it('should have all required planning modes', () => {
it('should have all required planning modes', async () => {
const modes = ['lite', 'spec', 'full'] as const;
for (const mode of modes) {
const feature = { id: 'test', planningMode: mode };
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result.length).toBeGreaterThan(100);
}
});
it('lite prompt should include correct structure', () => {
it('lite prompt should include correct structure', async () => {
const feature = { id: 'test', planningMode: 'lite' as const };
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('Goal');
expect(result).toContain('Approach');
expect(result).toContain('Files to Touch');
@@ -298,9 +301,9 @@ describe('auto-mode-service.ts - Planning Mode', () => {
expect(result).toContain('Risks');
});
it('spec prompt should include task format instructions', () => {
it('spec prompt should include task format instructions', async () => {
const feature = { id: 'test', planningMode: 'spec' as const };
const result = getPlanningPromptPrefix(service, feature);
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('Problem');
expect(result).toContain('Solution');
expect(result).toContain('Acceptance Criteria');
@@ -309,13 +312,13 @@ describe('auto-mode-service.ts - Planning Mode', () => {
expect(result).toContain('Verification');
});
it('full prompt should include phases', () => {
it('full prompt should include phases', async () => {
const feature = { id: 'test', planningMode: 'full' as const };
const result = getPlanningPromptPrefix(service, feature);
expect(result).toContain('Problem Statement');
expect(result).toContain('User Story');
expect(result).toContain('Technical Context');
expect(result).toContain('Non-Goals');
const result = await getPlanningPromptPrefix(service, feature);
expect(result).toContain('1. **Problem Statement**');
expect(result).toContain('2. **User Story**');
expect(result).toContain('4. **Technical Context**');
expect(result).toContain('5. **Non-Goals**');
expect(result).toContain('Phase 1');
expect(result).toContain('Phase 2');
expect(result).toContain('Phase 3');