profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
blockers.test.ts 4,694 bytes
1 import { mkdtemp, rm, writeFile } from 'node:fs/promises'
2 import { tmpdir } from 'node:os'
3 import path from 'node:path'
4 import type { FastifyInstance } from 'fastify'
5 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
6 import type { AnswerResponse, BlockersResponse, Session } from '../../shared/types'
7 import { buildApp } from '../app'
8 import { createLlmMock } from '../providers/llmMock'
9 import { SessionStore } from '../store/sessionStore'
10
11 const BLOCKED_MD = `# BLOCKED
12
13 ## B1: which auth provider
14 - Task: T4
15 - Question: Should sign-in use email/password or an OAuth provider?
16 - Options considered: email/password, Google OAuth
17 - Continued with: implemented email/password
18
19 ## B2: rate limit thresholds
20 - Task: T6
21 - Question: What is the max requests per minute per user?
22 - Options considered: 60, 120
23 - Continued with: skipped task
24 `
25
26 describe('blockers route', () => {
27 let storeDir: string
28 let targetDir: string
29 let app: FastifyInstance
30
31 beforeEach(async () => {
32 storeDir = await mkdtemp(path.join(tmpdir(), 'voicetask-store-'))
33 targetDir = await mkdtemp(path.join(tmpdir(), 'voicetask-target-'))
34 app = buildApp({ store: new SessionStore(storeDir), llm: createLlmMock() })
35 })
36
37 afterEach(async () => {
38 await app.close()
39 await rm(storeDir, { recursive: true, force: true })
40 await rm(targetDir, { recursive: true, force: true })
41 })
42
43 async function createSession(): Promise<Session> {
44 return app
45 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir } })
46 .then((r) => r.json<Session>())
47 }
48
49 it('reports no questions and changes nothing when BLOCKED.md is missing', async () => {
50 const session = await createSession()
51 const res = await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/blockers`, payload: {} })
52 expect(res.statusCode).toBe(200)
53 expect(res.json<BlockersResponse>()).toEqual({ questions: [] })
54
55 const reloaded = await app
56 .inject({ method: 'GET', url: `/api/sessions/${session.id}` })
57 .then((r) => r.json<Session>())
58 expect(reloaded.segments).toHaveLength(0)
59 expect(reloaded.openBlockers).toEqual([])
60 })
61
62 it('reports no questions when BLOCKED.md has no entries', async () => {
63 const session = await createSession()
64 await writeFile(path.join(targetDir, 'BLOCKED.md'), '# BLOCKED\n\nNo entries yet.\n', 'utf8')
65
66 const res = await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/blockers`, payload: {} })
67 expect(res.json<BlockersResponse>()).toEqual({ questions: [] })
68 })
69
70 it('imports blockers, asks the first immediately, and queues the rest', async () => {
71 const session = await createSession()
72 await writeFile(path.join(targetDir, 'BLOCKED.md'), BLOCKED_MD, 'utf8')
73
74 const res = await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/blockers`, payload: {} })
75 expect(res.statusCode).toBe(200)
76 const body = res.json<BlockersResponse>()
77 expect(body.questions).toEqual([
78 'Should sign-in use email/password or an OAuth provider?',
79 'What is the max requests per minute per user?',
80 ])
81
82 const afterImport = await app
83 .inject({ method: 'GET', url: `/api/sessions/${session.id}` })
84 .then((r) => r.json<Session>())
85 expect(afterImport.segments).toHaveLength(1)
86 expect(afterImport.segments[0]).toMatchObject({
87 speaker: 'interviewer',
88 text: 'Should sign-in use email/password or an OAuth provider?',
89 })
90 expect(afterImport.openBlockers).toEqual(['What is the max requests per minute per user?'])
91 expect(afterImport.status).toBe('interviewing')
92 })
93
94 it('asks the next queued blocker question instead of a normal coverage question', async () => {
95 const session = await createSession()
96 await writeFile(path.join(targetDir, 'BLOCKED.md'), BLOCKED_MD, 'utf8')
97 await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/blockers`, payload: {} })
98
99 const res = await app.inject({
100 method: 'POST',
101 url: `/api/sessions/${session.id}/answer`,
102 payload: { text: 'email/password' },
103 })
104 const body = res.json<AnswerResponse>()
105 expect(body.turn.nextQuestion).toBe('What is the max requests per minute per user?')
106 expect(body.turn.done).toBe(false)
107
108 const reloaded = await app
109 .inject({ method: 'GET', url: `/api/sessions/${session.id}` })
110 .then((r) => r.json<Session>())
111 expect(reloaded.openBlockers).toEqual([])
112 })
113
114 it('returns 404 for an unknown session', async () => {
115 const res = await app.inject({ method: 'POST', url: '/api/sessions/nonexistent/blockers', payload: {} })
116 expect(res.statusCode).toBe(404)
117 })
118 })
119