profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
export.test.ts 4,161 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 type { Session } from '../../shared/types'
7 import { buildApp } from '../app'
8 import { createLlmMock } from '../providers/llmMock'
9 import { SessionStore } from '../store/sessionStore'
10
11 describe('export routes', () => {
12 let storeDir: string
13 let targetDir: string
14 let app: FastifyInstance
15
16 beforeEach(async () => {
17 storeDir = await mkdtemp(path.join(tmpdir(), 'voicetask-store-'))
18 targetDir = await mkdtemp(path.join(tmpdir(), 'voicetask-target-'))
19 app = buildApp({ store: new SessionStore(storeDir), llm: createLlmMock() })
20 })
21
22 afterEach(async () => {
23 await app.close()
24 await rm(storeDir, { recursive: true, force: true })
25 await rm(targetDir, { recursive: true, force: true })
26 })
27
28 async function createSessionWithAnAnswer(): Promise<Session> {
29 const created = await app
30 .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'Family Recipes', targetDir } })
31 .then((r) => r.json<Session>())
32 await app.inject({
33 method: 'POST',
34 url: `/api/sessions/${created.id}/answer`,
35 payload: { text: 'a recipe app for my family' },
36 })
37 return app.inject({ method: 'GET', url: `/api/sessions/${created.id}` }).then((r) => r.json<Session>())
38 }
39
40 it('404s the zip export before the spec pack has been generated', async () => {
41 const session = await createSessionWithAnAnswer()
42 const res = await app.inject({ method: 'GET', url: `/api/sessions/${session.id}/export/spec-pack.zip` })
43 expect(res.statusCode).toBe(404)
44 })
45
46 it('reports spec pack readiness through HEAD', async () => {
47 const session = await createSessionWithAnAnswer()
48 const url = `/api/sessions/${session.id}/export/spec-pack.zip`
49
50 const before = await app.inject({ method: 'HEAD', url })
51 expect(before.statusCode).toBe(404)
52
53 await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/generate`, payload: {} })
54
55 const after = await app.inject({ method: 'HEAD', url })
56 expect(after.statusCode).toBe(200)
57 })
58
59 it('downloads a valid zip of the spec pack after generating', async () => {
60 const session = await createSessionWithAnAnswer()
61 await app.inject({ method: 'POST', url: `/api/sessions/${session.id}/generate`, payload: {} })
62
63 const res = await app.inject({ method: 'GET', url: `/api/sessions/${session.id}/export/spec-pack.zip` })
64 expect(res.statusCode).toBe(200)
65 expect(res.headers['content-type']).toBe('application/zip')
66 expect(res.headers['content-disposition']).toContain('family-recipes-spec-pack.zip')
67
68 expect(res.rawPayload.readUInt32LE(0)).toBe(0x04034b50)
69 expect(res.rawPayload.readUInt32LE(res.rawPayload.length - 22)).toBe(0x06054b50)
70 for (const file of ['SPEC.md', 'PLAN.md', 'TASKS.md', 'VERIFICATION.md', 'HANDOFF.md', 'sources.json']) {
71 expect(res.rawPayload.includes(Buffer.from(file, 'utf8'))).toBe(true)
72 }
73 })
74
75 it('returns 404 for the zip export on an unknown session', async () => {
76 const res = await app.inject({ method: 'GET', url: '/api/sessions/nonexistent/export/spec-pack.zip' })
77 expect(res.statusCode).toBe(404)
78 })
79
80 it('downloads the transcript as markdown at any point in the session', async () => {
81 const session = await createSessionWithAnAnswer()
82 const res = await app.inject({ method: 'GET', url: `/api/sessions/${session.id}/export/transcript.md` })
83 expect(res.statusCode).toBe(200)
84 expect(res.headers['content-type']).toContain('text/markdown')
85 expect(res.headers['content-disposition']).toContain('family-recipes-transcript.md')
86 expect(res.body).toContain('# Family Recipes: Interview Transcript')
87 expect(res.body).toContain('**You:** a recipe app for my family')
88 })
89
90 it('returns 404 for the transcript export on an unknown session', async () => {
91 const res = await app.inject({ method: 'GET', url: '/api/sessions/nonexistent/export/transcript.md' })
92 expect(res.statusCode).toBe(404)
93 })
94 })
95