import { describe, it, expect } from '@jest/globals';
import { getPrompts, getPrompt, PROMPTS } from '../src/prompts/index.js';
describe('Prompts', () => {
describe('getPrompts', () => {
it('should return all 8 prompts', () => {
const prompts = getPrompts();
expect(prompts).toHaveLength(8);
});
it('should return prompts with required fields', () => {
const prompts = getPrompts();
for (const prompt of prompts) {
expect(prompt).toHaveProperty('name');
expect(prompt).toHaveProperty('description');
expect(typeof prompt.name).toBe('string');
expect(typeof prompt.description).toBe('string');
expect(prompt.name.length).toBeGreaterThan(0);
expect(prompt.description.length).toBeGreaterThan(0);
}
});
it('should return prompts with arguments array', () => {
const prompts = getPrompts();
for (const prompt of prompts) {
expect(Array.isArray(prompt.arguments)).toBe(true);
}
});
it('should not expose buildMessages function', () => {
const prompts = getPrompts();
for (const prompt of prompts) {
expect(prompt).not.toHaveProperty('buildMessages');
}
});
it('should have unique prompt names', () => {
const prompts = getPrompts();
const names = prompts.map((p) => p.name);
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(names.length);
});
});
describe('getPrompt', () => {
it('should throw for unknown prompt', () => {
expect(() => getPrompt('nonexistent', {})).toThrow('Unknown prompt: nonexistent');
});
it('should return description and messages', () => {
const result = getPrompt('reframe', { problem: 'Test problem' });
expect(result).toHaveProperty('description');
expect(result).toHaveProperty('messages');
expect(Array.isArray(result.messages)).toBe(true);
expect(result.messages.length).toBeGreaterThan(0);
});
it('should throw for missing required arguments', () => {
expect(() => getPrompt('perspectives', {})).toThrow();
expect(() => getPrompt('perspectives', { problem: 'test' })).toThrow(); // missing perspectives
});
it('should wrap errors with prompt context', () => {
try {
getPrompt('perspectives', {});
fail('Should have thrown');
} catch (e) {
expect((e as Error).message).toMatch(/^Failed to build prompt "perspectives":/);
expect((e as Error).message).toContain('problem argument is required');
}
});
it('should handle empty string arguments as missing', () => {
expect(() => getPrompt('perspectives', { problem: '', perspectives: 'test' })).toThrow(
'problem argument is required'
);
});
it('should handle whitespace-only arguments as valid', () => {
// Whitespace is truthy in JS, so it passes validation (this is intentional)
const result = getPrompt('perspectives', { problem: ' ', perspectives: 'test' });
expect(result.messages).toHaveLength(1);
});
});
describe('perspectives prompt', () => {
it('should require problem and perspectives arguments', () => {
expect(() => getPrompt('perspectives', {})).toThrow('problem argument is required');
expect(() => getPrompt('perspectives', { problem: 'test' })).toThrow(
'perspectives argument is required'
);
});
it('should build valid messages with required args', () => {
const result = getPrompt('perspectives', {
problem: 'Test problem',
perspectives: 'security, performance',
});
expect(result.messages).toHaveLength(1);
expect(result.messages[0].role).toBe('user');
expect(result.messages[0].content).toHaveProperty('type', 'text');
});
it('should include optional context in message', () => {
const result = getPrompt('perspectives', {
problem: 'Test problem',
perspectives: 'security',
context: 'Additional context',
});
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('Additional context');
});
});
describe('assumptions prompt', () => {
it('should require plan argument', () => {
expect(() => getPrompt('assumptions', {})).toThrow('plan argument is required');
});
it('should build valid messages', () => {
const result = getPrompt('assumptions', { plan: 'Test plan' });
expect(result.messages).toHaveLength(1);
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('Test plan');
});
it('should include optional constraints and concerns', () => {
const result = getPrompt('assumptions', {
plan: 'Test plan',
constraints: 'Must be fast',
concerns: 'Worried about scale',
});
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('Must be fast');
expect(text).toContain('Worried about scale');
});
});
describe('blindspots prompt', () => {
it('should require proposal argument', () => {
expect(() => getPrompt('blindspots', {})).toThrow('proposal argument is required');
});
it('should build valid messages', () => {
const result = getPrompt('blindspots', { proposal: 'Test proposal' });
expect(result.messages).toHaveLength(1);
});
});
describe('tradeoffs prompt', () => {
it('should require options and criteria arguments', () => {
expect(() => getPrompt('tradeoffs', {})).toThrow('options argument is required');
expect(() => getPrompt('tradeoffs', { options: 'A, B' })).toThrow(
'criteria argument is required'
);
});
it('should build valid messages with required args', () => {
const result = getPrompt('tradeoffs', {
options: 'Option A, Option B',
criteria: 'cost, speed',
});
expect(result.messages).toHaveLength(1);
});
});
describe('red_team prompt', () => {
it('should require target argument', () => {
expect(() => getPrompt('red_team', {})).toThrow('target argument is required');
});
it('should build valid messages', () => {
const result = getPrompt('red_team', { target: 'Test system' });
expect(result.messages).toHaveLength(1);
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('Test system');
});
});
describe('reframe prompt', () => {
it('should require problem argument', () => {
expect(() => getPrompt('reframe', {})).toThrow('problem argument is required');
});
it('should include three reframing types', () => {
const result = getPrompt('reframe', { problem: 'Test problem' });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('HIGHER ABSTRACTION');
expect(text).toContain('INVERSION');
expect(text).toContain('SIMPLIFICATION');
});
});
describe('architecture prompt', () => {
it('should require design, workloads, and priorities arguments', () => {
expect(() => getPrompt('architecture', {})).toThrow('design argument is required');
expect(() => getPrompt('architecture', { design: 'test' })).toThrow(
'workloads argument is required'
);
expect(() => getPrompt('architecture', { design: 'test', workloads: 'test' })).toThrow(
'priorities argument is required'
);
});
it('should build valid messages with all required args', () => {
const result = getPrompt('architecture', {
design: 'Microservices',
workloads: '1000 req/s',
priorities: 'latency, cost',
});
expect(result.messages).toHaveLength(1);
});
it('should include cross-cutting concerns', () => {
const result = getPrompt('architecture', {
design: 'test',
workloads: 'test',
priorities: 'test',
});
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('Scalability');
expect(text).toContain('Reliability');
expect(text).toContain('Operational Complexity');
expect(text).toContain('Developer Experience');
expect(text).toContain('Cost Efficiency');
});
});
describe('diverge_converge prompt', () => {
it('should require challenge argument', () => {
expect(() => getPrompt('diverge_converge', {})).toThrow('challenge argument is required');
});
it('should include diverge and converge phases', () => {
const result = getPrompt('diverge_converge', { challenge: 'Test challenge' });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('PHASE 1: DIVERGE');
expect(text).toContain('PHASE 2: CONVERGE');
});
it('should use default width and criteria when not provided', () => {
const result = getPrompt('diverge_converge', { challenge: 'Test challenge' });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain('balanced');
expect(text).toContain('feasibility, impact, and effort required');
});
});
describe('PROMPTS registry', () => {
it('should have all expected prompts', () => {
const expectedNames = [
'perspectives',
'assumptions',
'blindspots',
'tradeoffs',
'red_team',
'reframe',
'architecture',
'diverge_converge',
];
for (const name of expectedNames) {
expect(PROMPTS).toHaveProperty(name);
}
});
it('should have buildMessages function for all prompts', () => {
for (const [name, prompt] of Object.entries(PROMPTS)) {
expect(typeof prompt.buildMessages).toBe('function');
expect(prompt.name).toBe(name);
}
});
});
describe('MCP spec compliance', () => {
it('should return messages with valid role (user or assistant)', () => {
const result = getPrompt('reframe', { problem: 'test' });
for (const message of result.messages) {
expect(['user', 'assistant']).toContain(message.role);
}
});
it('should return messages with valid content type', () => {
const result = getPrompt('reframe', { problem: 'test' });
for (const message of result.messages) {
const content = message.content as { type: string; text?: string };
expect(['text', 'image', 'resource']).toContain(content.type);
if (content.type === 'text') {
expect(typeof content.text).toBe('string');
}
}
});
it('should preserve user input in generated messages', () => {
const userInput = 'Special chars: <>&"\' and unicode: 日本語';
const result = getPrompt('reframe', { problem: userInput });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain(userInput);
});
it('should handle very long inputs without truncation', () => {
const longInput = 'x'.repeat(50000);
const result = getPrompt('reframe', { problem: longInput });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain(longInput);
});
it('should handle inputs with newlines and special whitespace', () => {
const multilineInput = 'Line 1\nLine 2\n\tTabbed line\r\nWindows line';
const result = getPrompt('reframe', { problem: multilineInput });
const text = (result.messages[0].content as { type: string; text: string }).text;
expect(text).toContain(multilineInput);
});
});
});