profileShare

rasmusjy / voicetask

Read-only snapshot

No repository description.

main default branch 105 files Expires Sep 13, 2026, 9:06 AM
generate.test.ts 3,489 bytes
1 import { mkdtemp, readdir, readFile, rm } from 'node:fs/promises'
2 import { tmpdir } from 'node:os'
3 import path from 'node:path'
4 import { afterEach, beforeEach, describe, expect, it } from 'vitest'
5 import { initialCoverage, type Session } from '../../shared/types'
6 import { createLlmMock } from '../providers/llmMock'
7 import { GenerateFilesExistError, generateSpecPack } from './generate'
8
9 function makeSession(targetDir: string): Session {
10 return {
11 id: 'sess-1',
12 name: 'my project',
13 targetDir,
14 createdAt: new Date().toISOString(),
15 segments: [{ id: 'S1', ts: new Date().toISOString(), speaker: 'user', text: 'we are building a todo app' }],
16 coverage: initialCoverage(),
17 summary: '',
18 status: 'done',
19 openBlockers: [],
20 }
21 }
22
23 describe('generateSpecPack', () => {
24 let targetDir: string
25
26 beforeEach(async () => {
27 targetDir = await mkdtemp(path.join(tmpdir(), 'voicetask-target-'))
28 })
29
30 afterEach(async () => {
31 await rm(targetDir, { recursive: true, force: true })
32 })
33
34 it('writes all six spec pack files', async () => {
35 const session = makeSession(targetDir)
36 const result = await generateSpecPack(createLlmMock(), session)
37
38 expect(result.files.sort()).toEqual(
39 [
40 'spec/SPEC.md',
41 'spec/PLAN.md',
42 'spec/TASKS.md',
43 'spec/VERIFICATION.md',
44 'spec/HANDOFF.md',
45 'spec/sources.json',
46 ].sort(),
47 )
48
49 for (const file of result.files) {
50 const content = await readFile(path.join(targetDir, file), 'utf8')
51 expect(content.length).toBeGreaterThan(0)
52 }
53 })
54
55 it('emits sources.json mapping segment ids to text and timestamp', async () => {
56 const session = makeSession(targetDir)
57 await generateSpecPack(createLlmMock(), session)
58 const sources = JSON.parse(await readFile(path.join(targetDir, 'spec/sources.json'), 'utf8'))
59 expect(sources.S1).toEqual({ text: 'we are building a todo app', ts: session.segments[0].ts })
60 })
61
62 it('removes the bogus [S999] marker and appends [unverified], keeping the valid [S1] marker', async () => {
63 const session = makeSession(targetDir)
64 const result = await generateSpecPack(createLlmMock(), session)
65 const specContent = await readFile(path.join(targetDir, 'spec/SPEC.md'), 'utf8')
66
67 expect(specContent).toContain('[S1]')
68 expect(specContent).not.toContain('[S999]')
69 expect(specContent).toContain('[unverified]')
70 expect(result.warnings.some((w) => w.includes('SPEC.md'))).toBe(true)
71 })
72
73 it('fails on a second run without overwrite, and stores nothing new', async () => {
74 const session = makeSession(targetDir)
75 await generateSpecPack(createLlmMock(), session)
76 await expect(generateSpecPack(createLlmMock(), session)).rejects.toBeInstanceOf(GenerateFilesExistError)
77 })
78
79 it('creates a backup dir and regenerates when overwrite is set', async () => {
80 const session = makeSession(targetDir)
81 await generateSpecPack(createLlmMock(), session)
82 const result = await generateSpecPack(createLlmMock(), session, { overwrite: true })
83 expect(result.files.length).toBe(6)
84
85 const specDirEntries = await readdir(path.join(targetDir, 'spec'))
86 expect(specDirEntries.some((entry) => entry.startsWith('backup-'))).toBe(true)
87 })
88
89 it('fails when the target directory does not exist', async () => {
90 const session = makeSession(path.join(targetDir, 'does-not-exist'))
91 await expect(generateSpecPack(createLlmMock(), session)).rejects.toThrow()
92 })
93 })
94