profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
sessions.test.ts 4,774 bytes
1 import { mkdtemp, rm } 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 { CATEGORY_IDS, type AnswerResponse, type Session } from '../../shared/types'
7 import { createLlmMock } from '../providers/llmMock'
8 import { SessionStore } from '../store/sessionStore'
9 import { buildApp } from '../app'
10
11 describe('session routes', () => {
12 let baseDir: string
13 let app: FastifyInstance
14
15 beforeEach(async () => {
16 baseDir = await mkdtemp(path.join(tmpdir(), 'voicetask-routes-'))
17 app = buildApp({ store: new SessionStore(baseDir), llm: createLlmMock() })
18 })
19
20 afterEach(async () => {
21 await app.close()
22 await rm(baseDir, { recursive: true, force: true })
23 })
24
25 it('creates a session', async () => {
26 const res = await app.inject({
27 method: 'POST',
28 url: '/api/sessions',
29 payload: { name: 'my project', targetDir: '/tmp/target' },
30 })
31 expect(res.statusCode).toBe(201)
32 const session = res.json<Session>()
33 expect(session.name).toBe('my project')
34 expect(session.status).toBe('interviewing')
35 })
36
37 it('lists and fetches sessions', async () => {
38 const created = await app
39 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir: '/tmp/p' } })
40 .then((r) => r.json<Session>())
41
42 const list = await app.inject({ method: 'GET', url: '/api/sessions' })
43 expect(list.json()).toEqual([
44 { id: created.id, name: 'p', status: 'interviewing', createdAt: created.createdAt },
45 ])
46
47 const fetched = await app.inject({ method: 'GET', url: `/api/sessions/${created.id}` })
48 expect(fetched.json<Session>()).toEqual(created)
49
50 const missing = await app.inject({ method: 'GET', url: '/api/sessions/nonexistent' })
51 expect(missing.statusCode).toBe(404)
52 })
53
54 it('deletes a session', async () => {
55 const created = await app
56 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir: '/tmp/p' } })
57 .then((r) => r.json<Session>())
58
59 const deleted = await app.inject({ method: 'DELETE', url: `/api/sessions/${created.id}` })
60 expect(deleted.statusCode).toBe(204)
61
62 const fetched = await app.inject({ method: 'GET', url: `/api/sessions/${created.id}` })
63 expect(fetched.statusCode).toBe(404)
64
65 const again = await app.inject({ method: 'DELETE', url: `/api/sessions/${created.id}` })
66 expect(again.statusCode).toBe(404)
67 })
68
69 it('answering produces a user segment, an interviewer segment, and a coverage update', async () => {
70 const created = await app
71 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir: '/tmp/p' } })
72 .then((r) => r.json<Session>())
73
74 const res = await app.inject({
75 method: 'POST',
76 url: `/api/sessions/${created.id}/answer`,
77 payload: { text: 'we are building a todo app' },
78 })
79 expect(res.statusCode).toBe(200)
80 const body = res.json<AnswerResponse>()
81 expect(body.segment.speaker).toBe('user')
82 expect(body.segment.text).toBe('we are building a todo app')
83 expect(body.turn.coverage[CATEGORY_IDS[0]]).toBe('clear')
84 expect(body.turn.done).toBe(false)
85
86 const session = await app
87 .inject({ method: 'GET', url: `/api/sessions/${created.id}` })
88 .then((r) => r.json<Session>())
89 expect(session.segments).toHaveLength(2)
90 expect(session.segments[0]).toMatchObject({ id: 'S1', speaker: 'user' })
91 expect(session.segments[1]).toMatchObject({ id: 'S2', speaker: 'interviewer' })
92 expect(session.coverage).toEqual(body.turn.coverage)
93 expect(session.status).toBe('interviewing')
94 })
95
96 it('ends the session when the answer is exactly "done"', async () => {
97 const created = await app
98 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir: '/tmp/p' } })
99 .then((r) => r.json<Session>())
100
101 const res = await app.inject({
102 method: 'POST',
103 url: `/api/sessions/${created.id}/answer`,
104 payload: { text: 'done' },
105 })
106 const body = res.json<AnswerResponse>()
107 expect(body.turn.done).toBe(true)
108
109 const session = await app
110 .inject({ method: 'GET', url: `/api/sessions/${created.id}` })
111 .then((r) => r.json<Session>())
112 expect(session.status).toBe('done')
113 })
114
115 it('rejects invalid answer payloads', async () => {
116 const created = await app
117 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'p', targetDir: '/tmp/p' } })
118 .then((r) => r.json<Session>())
119
120 const res = await app.inject({
121 method: 'POST',
122 url: `/api/sessions/${created.id}/answer`,
123 payload: {},
124 })
125 expect(res.statusCode).toBe(400)
126 })
127 })
128