profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
interview.test.ts 2,991 bytes
1 import { describe, expect, it } from 'vitest'
2 import { initialCoverage, type InterviewTurn, type Segment, type Session } from '../../shared/types'
3 import type { InterviewContext, InterviewLlm } from '../providers/types'
4 import { runInterviewTurn } from './interview'
5 import { RECENT_SEGMENT_WINDOW } from './summary'
6
7 function makeSession(segments: Segment[]): Session {
8 return {
9 id: 'sess-1',
10 name: 'proj',
11 targetDir: '/tmp/proj',
12 createdAt: new Date().toISOString(),
13 segments,
14 coverage: initialCoverage(),
15 summary: 'earlier summary',
16 status: 'interviewing',
17 openBlockers: [],
18 }
19 }
20
21 function segment(id: string, speaker: Segment['speaker'], text: string): Segment {
22 return { id, ts: new Date().toISOString(), speaker, text }
23 }
24
25 function stubLlm(turn: InterviewTurn, capture?: (ctx: InterviewContext) => void): InterviewLlm {
26 return {
27 async nextTurn(context) {
28 capture?.(context)
29 return turn
30 },
31 async generateFile() {
32 throw new Error('not used in this test')
33 },
34 }
35 }
36
37 describe('runInterviewTurn', () => {
38 it('short-circuits with done:true when the last user answer is exactly "done"', async () => {
39 const session = makeSession([segment('S1', 'user', 'Done')])
40 const llm = stubLlm({
41 coverage: initialCoverage(),
42 nextQuestion: 'should not be used',
43 contradiction: null,
44 done: false,
45 summaryUpdate: null,
46 })
47 const turn = await runInterviewTurn(llm, session)
48 expect(turn.done).toBe(true)
49 expect(turn.coverage).toEqual(session.coverage)
50 })
51
52 it('passes a contradiction from the LLM straight through unchanged', async () => {
53 const session = makeSession([segment('S1', 'user', 'we only support web')])
54 const contradiction = { segmentIds: ['S1', 'S3'], description: 'web-only vs mobile-only' }
55 const llm = stubLlm({
56 coverage: initialCoverage(),
57 nextQuestion: 'Which is it, web or mobile?',
58 contradiction,
59 done: false,
60 summaryUpdate: null,
61 })
62 const turn = await runInterviewTurn(llm, session)
63 expect(turn.contradiction).toEqual(contradiction)
64 })
65
66 it('sends only the last 40 segments plus the running summary once the transcript grows past the window', async () => {
67 const segments = Array.from({ length: RECENT_SEGMENT_WINDOW + 7 }, (_, i) =>
68 segment(`S${i + 1}`, i % 2 === 0 ? 'user' : 'interviewer', `text ${i + 1}`),
69 )
70 const session = makeSession(segments)
71 let seenContext: InterviewContext | undefined
72 const llm = stubLlm(
73 {
74 coverage: initialCoverage(),
75 nextQuestion: 'next?',
76 contradiction: null,
77 done: false,
78 summaryUpdate: null,
79 },
80 (ctx) => {
81 seenContext = ctx
82 },
83 )
84
85 await runInterviewTurn(llm, session)
86
87 expect(seenContext?.segments).toHaveLength(RECENT_SEGMENT_WINDOW)
88 expect(seenContext?.segments[0].id).toBe('S8')
89 expect(seenContext?.summary).toBe('earlier summary')
90 })
91 })
92