Ships an installable Claude Code plugin (/plugin marketplace add erp-mafia/accounted) that bundles the MCP connection (OAuth, zero-key) with seven short workflow skills following the Swedish bookkeeping rhythm: start, bookkeep, check, month-close, vat, payroll, year-end. Wrappers are deliberately thin: they ground in the agent briefing and Accounted:// resources, load server-side workflow skills and regulatory atoms via gnubok_load_skill at need, and stage every write for user approval. No knowledge is duplicated into the plugin. A vitest cross-checks every skill slug, atom id, resource URI, and tool name the wrappers reference against the MCP server source, so a server rename fails CI instead of a user's chat session. Assessment and follow-ups in dev_docs/claude_plugin.md (local, dev_docs is unpublished by design). Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
72 lines
2.6 KiB
TypeScript
72 lines
2.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
import { readFileSync, readdirSync } from 'node:fs'
|
|
import { join } from 'node:path'
|
|
import { workflowSkills } from '../skills'
|
|
import { dataResources } from '../resources'
|
|
import { discoverAtoms } from '@/scripts/lib/atom-discovery'
|
|
|
|
/**
|
|
* The Claude Code plugin (claude-plugin/) ships thin wrapper skills whose value
|
|
* is that every slug, resource URI, and tool name they mention resolves against
|
|
* this MCP server. A rename on the server side must fail here, not in a user's
|
|
* chat session.
|
|
*/
|
|
|
|
const repoRoot = join(__dirname, '..', '..', '..', '..')
|
|
const skillsDir = join(repoRoot, 'claude-plugin', 'skills')
|
|
|
|
function readPluginSkills(): { file: string; body: string }[] {
|
|
return readdirSync(skillsDir).map((dir) => {
|
|
const file = join(skillsDir, dir, 'SKILL.md')
|
|
return { file: `${dir}/SKILL.md`, body: readFileSync(file, 'utf8') }
|
|
})
|
|
}
|
|
|
|
const pluginSkills = readPluginSkills()
|
|
const serverSource = readFileSync(join(__dirname, '..', 'server.ts'), 'utf8')
|
|
|
|
describe('claude-plugin wrapper references', () => {
|
|
it('ships the seven v1 skills', () => {
|
|
expect(pluginSkills.map((s) => s.file).sort()).toEqual([
|
|
'bookkeep/SKILL.md',
|
|
'check/SKILL.md',
|
|
'month-close/SKILL.md',
|
|
'payroll/SKILL.md',
|
|
'start/SKILL.md',
|
|
'vat/SKILL.md',
|
|
'year-end/SKILL.md',
|
|
])
|
|
})
|
|
|
|
it('every gnubok_load_skill slug resolves to a workflow skill or a registry atom', async () => {
|
|
const workflowSlugs = new Set(workflowSkills.map((s) => s.slug))
|
|
const atomIds = new Set((await discoverAtoms(repoRoot)).map((a) => a.id))
|
|
for (const { file, body } of pluginSkills) {
|
|
for (const [, slug] of body.matchAll(/gnubok_load_skill\("([^"]+)"\)/g)) {
|
|
const known = workflowSlugs.has(slug) || atomIds.has(slug)
|
|
expect(known, `${file} references unknown skill slug "${slug}"`).toBe(true)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('every Accounted:// resource URI exists on the server', () => {
|
|
const uris = new Set(dataResources.map((r) => r.uri))
|
|
for (const { file, body } of pluginSkills) {
|
|
for (const [uri] of body.matchAll(/Accounted:\/\/[a-z/-]+/g)) {
|
|
expect(uris.has(uri), `${file} references unknown resource "${uri}"`).toBe(true)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('every gnubok_* tool name exists on the server', () => {
|
|
for (const { file, body } of pluginSkills) {
|
|
for (const [tool] of body.matchAll(/gnubok_[a-z_]+/g)) {
|
|
expect(
|
|
serverSource.includes(`name: '${tool}'`),
|
|
`${file} references unknown tool "${tool}"`,
|
|
).toBe(true)
|
|
}
|
|
}
|
|
})
|
|
})
|