267ed6c1bb
An agent proposing a booking currently invents the account numbers, and a plausible guess (6071 instead of 6072, or the full cost instead of the 80% deductible share) produces a verifikat that posts and is wrong. This turns that into naming a reviewed template. Read from packs/ rather than the database on purpose: legal_note has no column in booking_template_library, so the database copy cannot answer "when does this template apply", and that note is the one field an agent cannot derive from account numbers. Carries the amount maths explicitly (vat lines from vat_rate, everything else from ratio) with an instruction not to fudge an amount to force a balance, plus notes that entity_type is binding, that a company may hold templates beyond this list, and that posting goes through the journal-entry tools so period locks and the balance check still apply. A load failure returns an explicit error rather than an empty list: an agent shown 12 of 26 templates concludes the other 14 do not exist and hand-rolls accounts for them. Verified the packs are actually reachable at runtime before relying on this: a production build traces all 26 YAML files into the serverless function, so the loader is not a local-only convenience. Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com> Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
69 lines
2.3 KiB
TypeScript
69 lines
2.3 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { dataResources, findResource, parseResourceQuery } from '../resources'
|
|
|
|
describe('mcp resource registry', () => {
|
|
it('exposes all data resources with required fields', () => {
|
|
expect(dataResources).toHaveLength(9)
|
|
const uris = dataResources.map((r) => r.uri).sort()
|
|
expect(uris).toEqual([
|
|
'Accounted://attention',
|
|
'Accounted://booking-templates',
|
|
'Accounted://capabilities',
|
|
'Accounted://chart-of-accounts',
|
|
'Accounted://company/current',
|
|
'Accounted://ledger/context',
|
|
'Accounted://period/active',
|
|
'Accounted://recent-activity',
|
|
'Accounted://settings/vat-treatments',
|
|
])
|
|
|
|
for (const r of dataResources) {
|
|
expect(r.name).toBeTruthy()
|
|
expect(r.description.length).toBeGreaterThan(20)
|
|
expect(r.mimeType).toBe('application/json')
|
|
expect(typeof r.read).toBe('function')
|
|
}
|
|
})
|
|
|
|
it('matches base URI ignoring query string', () => {
|
|
const r = findResource('Accounted://recent-activity?limit=5')
|
|
expect(r?.uri).toBe('Accounted://recent-activity')
|
|
})
|
|
|
|
it('returns null for unknown URI', () => {
|
|
expect(findResource('Accounted://does-not-exist')).toBeNull()
|
|
})
|
|
|
|
it('parses query params from URI', () => {
|
|
const q = parseResourceQuery('Accounted://recent-activity?limit=5&offset=10')
|
|
expect(q?.get('limit')).toBe('5')
|
|
expect(q?.get('offset')).toBe('10')
|
|
})
|
|
|
|
it('returns undefined when no query', () => {
|
|
expect(parseResourceQuery('Accounted://capabilities')).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe('vat-treatments resource', () => {
|
|
it('returns matrix for all customer types without DB access', async () => {
|
|
const r = findResource('Accounted://settings/vat-treatments')!
|
|
const result = (await r.read({
|
|
// Pure-function resource: no DB calls
|
|
supabase: undefined as never,
|
|
companyId: 'irrelevant',
|
|
userId: 'irrelevant',
|
|
scopes: [],
|
|
})) as { treatments: string[]; by_customer_type: Record<string, unknown> }
|
|
|
|
expect(result.treatments).toContain('standard_25')
|
|
expect(result.treatments).toContain('reverse_charge')
|
|
expect(Object.keys(result.by_customer_type)).toEqual([
|
|
'individual',
|
|
'swedish_business',
|
|
'eu_business',
|
|
'non_eu_business',
|
|
])
|
|
})
|
|
})
|