← back to ClawCoder
src/tests/lib/generator.test.ts
168 lines
import { describe, it, expect } from 'vitest'
import { PROFILES, getProfile } from '@/lib/profiles/data'
import { generateClaudeMd, type GeneratorVars } from '@/lib/generator/claude-md'
import { generateRulesFiles } from '@/lib/generator/rules'
import { generateSettingsJson, serializeSettingsJson } from '@/lib/generator/hooks'
import { generateBundle } from '@/lib/generator/bundle'
const testVars: GeneratorVars = {
projectName: 'TestApp',
projectDescription: 'A sample test application for unit testing.',
techStack: 'Next.js, TypeScript, PostgreSQL',
}
describe('Template Rendering', () => {
it('renders templates without leftover {{}} placeholders', () => {
for (const profile of PROFILES) {
const rendered = generateClaudeMd(profile, testVars)
const leftoverPlaceholders = rendered.match(/\{\{[^}]+\}\}/g)
expect(
leftoverPlaceholders,
`${profile.slug} has leftover placeholders: ${leftoverPlaceholders}`
).toBeNull()
}
})
it('includes the project name in rendered output', () => {
for (const profile of PROFILES) {
const rendered = generateClaudeMd(profile, testVars)
expect(
rendered,
`${profile.slug} should contain project name`
).toContain('TestApp')
}
})
it('includes the project description in rendered output', () => {
for (const profile of PROFILES) {
const rendered = generateClaudeMd(profile, testVars)
expect(
rendered,
`${profile.slug} should contain project description`
).toContain('A sample test application')
}
})
it('includes the tech stack in rendered output', () => {
for (const profile of PROFILES) {
const rendered = generateClaudeMd(profile, testVars)
expect(
rendered,
`${profile.slug} should contain tech stack`
).toContain('Next.js, TypeScript, PostgreSQL')
}
})
})
describe('Verbosity Progression', () => {
it('expert template output is shorter than beginner for same age group', () => {
const ageGroups = ['teen', 'young_adult', 'professional', 'pre_senior', 'senior'] as const
for (const age of ageGroups) {
const beginner = PROFILES.find(p => p.ageGroup === age && p.skillLevel === 'beginner')!
const expert = PROFILES.find(p => p.ageGroup === age && p.skillLevel === 'expert')!
const beginnerOutput = generateClaudeMd(beginner, testVars)
const expertOutput = generateClaudeMd(expert, testVars)
expect(
expertOutput.length,
`${age} expert output (${expertOutput.length} chars) should be shorter than beginner (${beginnerOutput.length} chars)`
).toBeLessThan(beginnerOutput.length)
}
})
})
describe('Rules Generation', () => {
it('generates rules for intermediate+ profiles that have them', () => {
// Intermediate and expert profiles should generally have rules files
const profilesWithRules = PROFILES.filter(p => p.rulesFiles.length > 0)
expect(profilesWithRules.length).toBeGreaterThan(0)
for (const profile of profilesWithRules) {
const rules = generateRulesFiles(profile)
expect(rules.length).toBeGreaterThan(0)
for (const rule of rules) {
expect(rule.path).toBeTruthy()
expect(rule.content).toBeTruthy()
expect(rule.path).toContain('.claude/rules/')
}
}
})
it('beginner profiles have no rules files (or very few)', () => {
const beginners = PROFILES.filter(p => p.skillLevel === 'beginner')
for (const profile of beginners) {
const rules = generateRulesFiles(profile)
// Beginners should have 0 rules files
expect(
rules.length,
`${profile.slug} (beginner) should have 0 rules files`
).toBe(0)
}
})
})
describe('Hooks Generation', () => {
it('generates valid settings JSON', () => {
for (const profile of PROFILES) {
const settings = generateSettingsJson(profile)
const json = serializeSettingsJson(settings)
// Should be valid JSON
expect(() => JSON.parse(json)).not.toThrow()
}
})
it('expert profiles with destructive command hooks have proper quoting', () => {
const expertProfiles = PROFILES.filter(p => p.skillLevel === 'expert')
for (const profile of expertProfiles) {
if (profile.hooks.PreToolUse) {
for (const rule of profile.hooks.PreToolUse) {
for (const hook of rule.hooks) {
// If the hook references TOOL_INPUT, it should use quoted form
if (hook.command.includes('TOOL_INPUT')) {
expect(
hook.command,
`${profile.slug} hook should use quoted "$TOOL_INPUT"`
).toContain('"$TOOL_INPUT"')
}
}
}
}
}
})
})
describe('Bundle Generation', () => {
it('generates a complete bundle for every profile', () => {
for (const profile of PROFILES) {
const bundle = generateBundle(profile, testVars)
expect(bundle.claudeMd, `${profile.slug}: claudeMd`).toBeTruthy()
expect(bundle.settingsJson, `${profile.slug}: settingsJson`).toBeTruthy()
expect(bundle.profile.slug, `${profile.slug}: profile.slug`).toBe(profile.slug)
expect(bundle.profile.displayName, `${profile.slug}: profile.displayName`).toBeTruthy()
expect(Array.isArray(bundle.rulesFiles), `${profile.slug}: rulesFiles`).toBe(true)
// claudeMd should not have leftover placeholders
expect(bundle.claudeMd.match(/\{\{[^}]+\}\}/g)).toBeNull()
// settingsJson should be valid JSON
expect(() => JSON.parse(bundle.settingsJson)).not.toThrow()
}
})
it('preserves profile metadata in the bundle', () => {
const profile = getProfile('professional-expert')!
const bundle = generateBundle(profile, testVars)
expect(bundle.profile.slug).toBe('professional-expert')
expect(bundle.profile.ageGroup).toBe('professional')
expect(bundle.profile.skillLevel).toBe('expert')
expect(bundle.profile.displayName).toBe('Professional Expert')
})
})