- lib/schemas/product-doc.ts: PRODUCT_DOC_FOLDERS/STATUSES + create/update/ toggle/frontmatter schemas (MAX_PRODUCT_DOC_CONTENT_LEN=100k) - lib/product-doc-folder.ts: DB UPPER_SNAKE ↔ API lowercase mapper (spiegel van lib/task-status.ts) - lib/product-doc-slug.ts: pure slugify + suggestSlug (dedupe-suffix) + ADR-sequence helpers (nextAdrPrefix, parseAdrNumber, suggestAdrSlug) - lib/schemas/product-doc-frontmatter-defaults.ts: per-folder UI-templates voor "Nieuwe doc"-dialog (last_updated weggelaten — server normaliseert bij save, zie T-1060) - __tests__: 37 tests groen (Zod-schemas + slug-helpers); de pre-existing worktree-env fail in idea-timeline-merge.test.ts blijft buiten scope Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
160 lines
4.5 KiB
TypeScript
160 lines
4.5 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
|
|
import {
|
|
PRODUCT_DOC_FOLDERS,
|
|
PRODUCT_DOC_STATUSES,
|
|
productDocCreateSchema,
|
|
productDocFolderToggleSchema,
|
|
productDocFrontmatterSchema,
|
|
productDocSlugSchema,
|
|
productDocUpdateSchema,
|
|
} from '@/lib/schemas/product-doc'
|
|
|
|
const validProductId = 'cmohrysyj0000rd17clnjy4tc'
|
|
|
|
describe('productDocSlugSchema', () => {
|
|
it('accepteert geldige slugs', () => {
|
|
expect(productDocSlugSchema.safeParse('deploy').success).toBe(true)
|
|
expect(productDocSlugSchema.safeParse('0001-context-decision').success).toBe(true)
|
|
expect(productDocSlugSchema.safeParse('a').success).toBe(true)
|
|
})
|
|
|
|
it('weigert hoofdletters, spaties en speciale tekens', () => {
|
|
expect(productDocSlugSchema.safeParse('Deploy').success).toBe(false)
|
|
expect(productDocSlugSchema.safeParse('deploy stappen').success).toBe(false)
|
|
expect(productDocSlugSchema.safeParse('deploy/stappen').success).toBe(false)
|
|
})
|
|
|
|
it('weigert slug die met streepje begint', () => {
|
|
expect(productDocSlugSchema.safeParse('-deploy').success).toBe(false)
|
|
})
|
|
|
|
it('weigert slug > 80 tekens', () => {
|
|
expect(productDocSlugSchema.safeParse('a'.repeat(81)).success).toBe(false)
|
|
expect(productDocSlugSchema.safeParse('a'.repeat(80)).success).toBe(true)
|
|
})
|
|
})
|
|
|
|
describe('productDocFrontmatterSchema', () => {
|
|
it('accepteert minimaal valide frontmatter', () => {
|
|
const r = productDocFrontmatterSchema.safeParse({ title: 'Deploy', status: 'draft' })
|
|
expect(r.success).toBe(true)
|
|
})
|
|
|
|
it('weigert ontbrekende title of status', () => {
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({ status: 'draft' }).success,
|
|
).toBe(false)
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({ title: 'Deploy' }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('weigert status die niet in de enum zit', () => {
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({ title: 'D', status: 'wip' }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('accepteert audience als string of array', () => {
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({
|
|
title: 'D',
|
|
status: 'draft',
|
|
audience: 'maintainer',
|
|
}).success,
|
|
).toBe(true)
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({
|
|
title: 'D',
|
|
status: 'draft',
|
|
audience: ['maintainer', 'contributor'],
|
|
}).success,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('weigert oversized title', () => {
|
|
expect(
|
|
productDocFrontmatterSchema.safeParse({
|
|
title: 'x'.repeat(201),
|
|
status: 'draft',
|
|
}).success,
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('productDocCreateSchema', () => {
|
|
const base = {
|
|
product_id: validProductId,
|
|
folder: 'runbooks' as const,
|
|
slug: 'deploy',
|
|
content_md: '---\ntitle: "Deploy"\nstatus: draft\n---\n\nbody',
|
|
}
|
|
|
|
it('accepteert geldige input', () => {
|
|
expect(productDocCreateSchema.safeParse(base).success).toBe(true)
|
|
})
|
|
|
|
it('weigert ongeldige folder', () => {
|
|
expect(
|
|
productDocCreateSchema.safeParse({ ...base, folder: 'wiki' }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('weigert ongeldige product_id (geen cuid)', () => {
|
|
expect(
|
|
productDocCreateSchema.safeParse({ ...base, product_id: 'not-a-cuid' }).success,
|
|
).toBe(false)
|
|
})
|
|
|
|
it('weigert leeg of te lang content_md', () => {
|
|
expect(productDocCreateSchema.safeParse({ ...base, content_md: '' }).success).toBe(false)
|
|
expect(
|
|
productDocCreateSchema.safeParse({ ...base, content_md: 'x'.repeat(100_001) }).success,
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('productDocUpdateSchema', () => {
|
|
it('accepteert valide content_md', () => {
|
|
expect(
|
|
productDocUpdateSchema.safeParse({ content_md: '---\ntitle: "x"\nstatus: draft\n---\n\nbody' })
|
|
.success,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('weigert leeg content_md', () => {
|
|
expect(productDocUpdateSchema.safeParse({ content_md: '' }).success).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('productDocFolderToggleSchema', () => {
|
|
it('accepteert valide toggle-input', () => {
|
|
expect(
|
|
productDocFolderToggleSchema.safeParse({
|
|
product_id: validProductId,
|
|
folder: 'api',
|
|
enabled: false,
|
|
}).success,
|
|
).toBe(true)
|
|
})
|
|
|
|
it('weigert ontbrekende enabled-vlag', () => {
|
|
expect(
|
|
productDocFolderToggleSchema.safeParse({
|
|
product_id: validProductId,
|
|
folder: 'api',
|
|
}).success,
|
|
).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('PRODUCT_DOC_FOLDERS + STATUSES', () => {
|
|
it('bevat exact 8 folders', () => {
|
|
expect(PRODUCT_DOC_FOLDERS).toHaveLength(8)
|
|
})
|
|
|
|
it('bevat exact 4 statussen', () => {
|
|
expect(PRODUCT_DOC_STATUSES).toHaveLength(4)
|
|
})
|
|
})
|