profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM

Commit

T12: end-to-end smoke test

Boots the app with mock providers, drives the answer route to
completion, generates the spec pack into a temp target dir, and
asserts all six files exist, every remaining [S<n>] marker resolves
against sources.json, and HANDOFF.md contains "claude". Final check:
npm run typecheck, npm test, and npm run build all exit 0 — all 12
tasks in spec/TASKS.md are now complete.
commit 417a3a2

2 changed files with +82 and −1

Jump to a changed file
  1. server/smoke.test.ts +81 −0
  2. spec/TASKS.md +1 −1
added server/smoke.test.ts +81 −0
@@ -0,0 +1,81 @@
1 +import { mkdtemp, readFile, rm } from 'node:fs/promises'
2 +import { tmpdir } from 'node:os'
3 +import path from 'node:path'
4 +import { afterAll, beforeAll, describe, expect, it } from 'vitest'
5 +import { CATEGORY_IDS, type AnswerResponse, type GenerateResponse, type Session } from '../shared/types'
6 +import { buildApp } from './app'
7 +import { createLlmMock } from './providers/llmMock'
8 +import { SessionStore } from './store/sessionStore'
9 +
10 +describe('end-to-end smoke test', () => {
11 + let storeDir: string
12 + let targetDir: string
13 +
14 + beforeAll(async () => {
15 + storeDir = await mkdtemp(path.join(tmpdir(), 'voicetask-smoke-store-'))
16 + targetDir = await mkdtemp(path.join(tmpdir(), 'voicetask-smoke-target-'))
17 + })
18 +
19 + afterAll(async () => {
20 + await rm(storeDir, { recursive: true, force: true })
21 + await rm(targetDir, { recursive: true, force: true })
22 + })
23 +
24 + it('interviews to completion with mocks and generates a valid spec pack', async () => {
25 + const app = buildApp({ store: new SessionStore(storeDir), llm: createLlmMock() })
26 +
27 + const session = await app
28 + .inject({ method: 'POST', url: '/api/sessions', payload: { name: 'smoke project', targetDir } })
29 + .then((r) => r.json<Session>())
30 +
31 + let done = false
32 + for (let i = 0; i < CATEGORY_IDS.length && !done; i++) {
33 + const res = await app.inject({
34 + method: 'POST',
35 + url: `/api/sessions/${session.id}/answer`,
36 + payload: { text: `answer number ${i + 1}` },
37 + })
38 + expect(res.statusCode).toBe(200)
39 + done = res.json<AnswerResponse>().turn.done
40 + }
41 + expect(done).toBe(true)
42 +
43 + const generateRes = await app.inject({
44 + method: 'POST',
45 + url: `/api/sessions/${session.id}/generate`,
46 + payload: {},
47 + })
48 + expect(generateRes.statusCode).toBe(200)
49 + const generated = generateRes.json<GenerateResponse>()
50 +
51 + const expectedFiles = [
52 + 'spec/SPEC.md',
53 + 'spec/PLAN.md',
54 + 'spec/TASKS.md',
55 + 'spec/VERIFICATION.md',
56 + 'spec/HANDOFF.md',
57 + 'spec/sources.json',
58 + ]
59 + expect(generated.files.sort()).toEqual([...expectedFiles].sort())
60 +
61 + const sources = JSON.parse(await readFile(path.join(targetDir, 'spec/sources.json'), 'utf8')) as Record<
62 + string,
63 + { text: string; ts: string }
64 + >
65 +
66 + const markerFiles = ['SPEC.md', 'PLAN.md', 'TASKS.md', 'VERIFICATION.md', 'HANDOFF.md']
67 + for (const file of markerFiles) {
68 + const content = await readFile(path.join(targetDir, 'spec', file), 'utf8')
69 + const markers = [...content.matchAll(/\[S(\d+)\]/g)]
70 + for (const marker of markers) {
71 + const segmentId = `S${marker[1]}`
72 + expect(sources).toHaveProperty(segmentId)
73 + }
74 + }
75 +
76 + const handoff = await readFile(path.join(targetDir, 'spec/HANDOFF.md'), 'utf8')
77 + expect(handoff).toContain('claude')
78 +
79 + await app.close()
80 + })
81 +})
modified spec/TASKS.md +1 −1
@@ -57,7 +57,7 @@Work strictly in order unless a task's Depends line allows otherwise. One task a
57 57 - Depends: T5, T7
58 58 - Verify: `npm test` (parser tests incl. empty file; route test: import then next turn asks a blocker question).
59 59
60 -- [ ] T12 End-to-end smoke test
60 +- [x] T12 End-to-end smoke test
61 61 - Single vitest test: boot the server with mocks, create session (temp target dir), answer until done via the answer route, generate, assert all six files exist, all remaining `[S<n>]` markers resolve against sources.json, and HANDOFF.md contains the string `claude`.
62 62 - Depends: T9, T10
63 63 - Verify: `npm test` runs it green; then `npm run typecheck`, `npm test`, `npm run build` all exit 0 as the final full check.