- docs/skill.md teaches a Claude what Logbuch is for, how an entry sounds and in which order the calls go, served at GET /api/v1/skill and as a download next to the spec and the guide - a test keeps the skill from drifting: frontmatter, only endpoints that exist, house rules - resolveClient stamps last_used_at, the access list said never used even after eleven entries
60 lines
1.7 KiB
TypeScript
60 lines
1.7 KiB
TypeScript
import { readFileSync, readdirSync, statSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { describe, expect, it } from 'vitest'
|
|
|
|
const skill = readFileSync(join(process.cwd(), 'docs', 'skill.md'), 'utf8')
|
|
|
|
function routes(): string[] {
|
|
const found: string[] = []
|
|
|
|
function walk(dir: string, url: string) {
|
|
for (const name of readdirSync(dir)) {
|
|
const path = join(dir, name)
|
|
|
|
if (statSync(path).isDirectory()) {
|
|
walk(path, `${url}/${name}`)
|
|
continue
|
|
}
|
|
|
|
if (name === 'route.ts') {
|
|
found.push(url)
|
|
}
|
|
}
|
|
}
|
|
|
|
walk(join(process.cwd(), 'src', 'app', 'api', 'v1'), '/api/v1')
|
|
|
|
return found
|
|
}
|
|
|
|
describe('Skill für Claude', () => {
|
|
it('trägt Frontmatter mit Namen und Beschreibung', () => {
|
|
const head = skill.split('---')[1] ?? ''
|
|
|
|
expect(head).toMatch(/\nname: logbuch\n/u)
|
|
expect(head).toMatch(/\ndescription: .{40,}/u)
|
|
})
|
|
|
|
it('nennt nur Endpunkte, die es gibt', () => {
|
|
const known = new Set(routes().map(url => url.replace(/\/\[[^\]]+\]/gu, '')))
|
|
const mentioned = [...skill.matchAll(/`?(?:GET|POST|PUT|PATCH|DELETE) (\/api\/v1[a-z0-9./-]*)/giu)]
|
|
.map(match => match[1]!.replace(/\/(?::[a-z]+|<[a-z-]+>)/giu, '').replace(/\/$/u, ''))
|
|
|
|
expect(mentioned.length).toBeGreaterThan(5)
|
|
|
|
for (const url of new Set(mentioned)) {
|
|
expect(known.has(url), `${url} steht im Skill, aber nicht im Code`).toBe(true)
|
|
}
|
|
})
|
|
|
|
it('hält sich an die Hausregeln', () => {
|
|
expect(skill).not.toMatch(/[—–]/u)
|
|
expect(skill).not.toMatch(/\p{Extended_Pictographic}/u)
|
|
expect(skill).not.toMatch(/\b(fuer|ueber|koennen|muessen|loeschen|aendern|groesser|gehoert|laesst|veroeffentlich\w*)\b/iu)
|
|
})
|
|
|
|
it('sagt, dass ein Zugang nicht veröffentlichen kann', () => {
|
|
expect(skill).toMatch(/niemals veröffentlichen/u)
|
|
})
|
|
})
|