lib: idea-code generator + plan_md yaml-frontmatter parser (M12 T-494)
- lib/idea-code.ts: pure formatIdeaCode helper (client-safe, no prisma) - lib/idea-code-server.ts: atomic nextIdeaCode via Prisma row-lock, accepts optional TransactionClient for combining with idea.create - lib/idea-plan-parser.ts: parsePlanMd extracts ---yaml---/body, runs yaml.parse + ideaPlanMdFrontmatterSchema, returns line-info on failure; CRLF-tolerant - adds yaml@^2.8.4 dependency - 8 unit tests (parser happy/missing/yaml-error/zod-error/empty-stories/CRLF; formatIdeaCode pad-3 + overflow) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
bba3f11269
commit
dfee518996
7 changed files with 236 additions and 4 deletions
21
__tests__/lib/idea-code.test.ts
Normal file
21
__tests__/lib/idea-code.test.ts
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { formatIdeaCode } from '@/lib/idea-code'
|
||||
|
||||
describe('formatIdeaCode', () => {
|
||||
it('pads to 3 digits', () => {
|
||||
expect(formatIdeaCode(1)).toBe('IDEA-001')
|
||||
expect(formatIdeaCode(42)).toBe('IDEA-042')
|
||||
expect(formatIdeaCode(999)).toBe('IDEA-999')
|
||||
})
|
||||
|
||||
it('does not truncate beyond pad-width', () => {
|
||||
expect(formatIdeaCode(1000)).toBe('IDEA-1000')
|
||||
expect(formatIdeaCode(99999)).toBe('IDEA-99999')
|
||||
})
|
||||
})
|
||||
|
||||
// Integration-style concurrency-test op nextIdeaCode is in
|
||||
// __tests__/integration/ tests die de echte DB raken (zie M12 verificatie-stap).
|
||||
// Hier alleen de pure formatter; de increment-logica leunt op Prisma's
|
||||
// row-lock in $transaction die we per-database vertrouwen.
|
||||
103
__tests__/lib/idea-plan-parser.test.ts
Normal file
103
__tests__/lib/idea-plan-parser.test.ts
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
import { describe, it, expect } from 'vitest'
|
||||
|
||||
import { parsePlanMd } from '@/lib/idea-plan-parser'
|
||||
|
||||
const VALID = `---
|
||||
pbi:
|
||||
title: Test PBI
|
||||
priority: 2
|
||||
stories:
|
||||
- title: Eerste flow
|
||||
priority: 2
|
||||
tasks:
|
||||
- title: Setup
|
||||
priority: 2
|
||||
implementation_plan: |
|
||||
1. Doe X
|
||||
2. Doe Y
|
||||
---
|
||||
|
||||
# Overwegingen
|
||||
|
||||
Dit is de body, niet geparsed.
|
||||
`
|
||||
|
||||
describe('parsePlanMd', () => {
|
||||
it('parses a valid plan', () => {
|
||||
const r = parsePlanMd(VALID)
|
||||
expect(r.ok).toBe(true)
|
||||
if (r.ok) {
|
||||
expect(r.plan.pbi.title).toBe('Test PBI')
|
||||
expect(r.plan.stories).toHaveLength(1)
|
||||
expect(r.plan.stories[0].tasks).toHaveLength(1)
|
||||
expect(r.plan.stories[0].tasks[0].implementation_plan).toContain('Doe X')
|
||||
expect(r.body).toContain('# Overwegingen')
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects when frontmatter is missing', () => {
|
||||
const r = parsePlanMd('# Just markdown\n\nNo frontmatter here.')
|
||||
expect(r.ok).toBe(false)
|
||||
if (!r.ok) {
|
||||
expect(r.errors[0].line).toBe(1)
|
||||
expect(r.errors[0].message).toMatch(/frontmatter/i)
|
||||
}
|
||||
})
|
||||
|
||||
it('reports yaml syntax error with line info', () => {
|
||||
const broken = `---
|
||||
pbi:
|
||||
title: Test
|
||||
priority: [unclosed
|
||||
stories:
|
||||
- foo
|
||||
---
|
||||
|
||||
body
|
||||
`
|
||||
const r = parsePlanMd(broken)
|
||||
expect(r.ok).toBe(false)
|
||||
if (!r.ok) {
|
||||
expect(r.errors[0].message.length).toBeGreaterThan(0)
|
||||
}
|
||||
})
|
||||
|
||||
it('reports schema-validation error when pbi-section missing', () => {
|
||||
const noPbi = `---
|
||||
stories:
|
||||
- title: x
|
||||
priority: 2
|
||||
tasks:
|
||||
- title: y
|
||||
priority: 2
|
||||
---
|
||||
|
||||
body
|
||||
`
|
||||
const r = parsePlanMd(noPbi)
|
||||
expect(r.ok).toBe(false)
|
||||
if (!r.ok) {
|
||||
expect(r.errors.some((e) => e.message.includes('pbi'))).toBe(true)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects empty stories array', () => {
|
||||
const noStories = `---
|
||||
pbi:
|
||||
title: x
|
||||
priority: 2
|
||||
stories: []
|
||||
---
|
||||
|
||||
body
|
||||
`
|
||||
const r = parsePlanMd(noStories)
|
||||
expect(r.ok).toBe(false)
|
||||
})
|
||||
|
||||
it('handles CRLF line endings', () => {
|
||||
const crlf = VALID.replace(/\n/g, '\r\n')
|
||||
const r = parsePlanMd(crlf)
|
||||
expect(r.ok).toBe(true)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue