Files
accounted/extensions/general/mcp-server/__tests__/claude-plugin-refs.test.ts
T
Jakob WennbergandClaude Fable 5 e33cc2428d feat(plugin): official Claude Code plugin with marketplace and seven workflow skills (#1088)
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>
2026-07-21 12:59:57 +02:00

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)
}
}
})
})