llmMock.ts
2,667 bytes
| 1 | import { CATEGORY_IDS, type Coverage, type InterviewTurn } from '../../shared/types' |
|---|---|
| 2 | import type { GenerateFileRequest, InterviewContext, InterviewLlm } from './types' |
| 3 | |
| 4 | export function createLlmMock(): InterviewLlm { |
| 5 | return { |
| 6 | async nextTurn(context: InterviewContext): Promise<InterviewTurn> { |
| 7 | return mockNextTurn(context) |
| 8 | }, |
| 9 | async generateFile(request: GenerateFileRequest): Promise<string> { |
| 10 | return mockFileContent(request.file) |
| 11 | }, |
| 12 | } |
| 13 | } |
| 14 | |
| 15 | function mockNextTurn(context: InterviewContext): InterviewTurn { |
| 16 | const lastUserSegment = [...context.segments].reverse().find((s) => s.speaker === 'user') |
| 17 | if (lastUserSegment && lastUserSegment.text.trim().toLowerCase() === 'done') { |
| 18 | return { |
| 19 | coverage: context.coverage, |
| 20 | nextQuestion: 'Understood, wrapping up the interview.', |
| 21 | contradiction: null, |
| 22 | done: true, |
| 23 | summaryUpdate: null, |
| 24 | } |
| 25 | } |
| 26 | |
| 27 | const clearedCount = CATEGORY_IDS.filter((category) => context.coverage[category] === 'clear').length |
| 28 | const categoryToClear = CATEGORY_IDS[clearedCount] |
| 29 | |
| 30 | if (!categoryToClear) { |
| 31 | return { |
| 32 | coverage: context.coverage, |
| 33 | nextQuestion: 'All categories covered, ready to generate the spec pack.', |
| 34 | contradiction: null, |
| 35 | done: true, |
| 36 | summaryUpdate: null, |
| 37 | } |
| 38 | } |
| 39 | |
| 40 | const coverage: Coverage = { ...context.coverage, [categoryToClear]: 'clear' } |
| 41 | const nextCategory = CATEGORY_IDS[clearedCount + 1] |
| 42 | const done = nextCategory === undefined |
| 43 | |
| 44 | return { |
| 45 | coverage, |
| 46 | nextQuestion: done |
| 47 | ? 'All categories covered, ready to generate the spec pack.' |
| 48 | : `[mock] Tell me about ${nextCategory}.`, |
| 49 | contradiction: null, |
| 50 | done, |
| 51 | summaryUpdate: null, |
| 52 | } |
| 53 | } |
| 54 | |
| 55 | function mockFileContent(file: GenerateFileRequest['file']): string { |
| 56 | switch (file) { |
| 57 | case 'SPEC.md': |
| 58 | return [ |
| 59 | '# SPEC: Mock', |
| 60 | '', |
| 61 | '## Functional requirements', |
| 62 | '', |
| 63 | '- FR-001: The mock system does the first thing. [S1]', |
| 64 | '- FR-002: The mock system does the second thing. [S999]', |
| 65 | '', |
| 66 | ].join('\n') |
| 67 | case 'PLAN.md': |
| 68 | return '# PLAN: Mock\n\nMinimal mock plan.\n' |
| 69 | case 'TASKS.md': |
| 70 | return [ |
| 71 | '# TASKS: Mock', |
| 72 | '', |
| 73 | '- [ ] T1 Mock task', |
| 74 | ' - Verify: `npm test` exits 0.', |
| 75 | '', |
| 76 | ].join('\n') |
| 77 | case 'VERIFICATION.md': |
| 78 | return '# VERIFICATION: Mock\n\nRun `npm test`.\n' |
| 79 | case 'HANDOFF.md': |
| 80 | return [ |
| 81 | '# HANDOFF: Mock', |
| 82 | '', |
| 83 | 'Launch a sandboxed session with:', |
| 84 | '', |
| 85 | '```', |
| 86 | 'claude "Work through spec/TASKS.md in order."', |
| 87 | '```', |
| 88 | '', |
| 89 | ].join('\n') |
| 90 | } |
| 91 | } |
| 92 | |