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>
106 lines
3.9 KiB
TypeScript
106 lines
3.9 KiB
TypeScript
import { describe, it, expect } from 'vitest'
|
|
import { bookingPacksResource } from '../resources/booking-packs'
|
|
import { findResource } from '../resources/index'
|
|
|
|
/**
|
|
* The value of this resource is that an agent stops guessing account numbers.
|
|
* These tests pin the parts that make that true: the slug is present so a
|
|
* template can be NAMED, the statutory note survives (it exists nowhere else,
|
|
* the database has no column for it), and a load failure is distinguishable
|
|
* from an empty catalogue.
|
|
*/
|
|
|
|
interface PackPayload {
|
|
templates: Array<{
|
|
slug: string
|
|
name: string
|
|
description: string
|
|
legal_note?: string
|
|
category: string
|
|
entity_type: string
|
|
lines: Array<{ account: string; side: string; type: string; ratio?: number; vat_rate?: number }>
|
|
}>
|
|
how_to_apply: Record<string, string>
|
|
notes: Record<string, string>
|
|
error?: string
|
|
}
|
|
|
|
const read = () => bookingPacksResource.read({} as never) as Promise<PackPayload>
|
|
|
|
describe('booking packs MCP resource', () => {
|
|
it('is registered and resolvable by URI', () => {
|
|
expect(findResource(bookingPacksResource.uri)).toBe(bookingPacksResource)
|
|
})
|
|
|
|
it('exposes every template with its slug, the lookup key an agent names', async () => {
|
|
const { templates } = await read()
|
|
expect(templates.length).toBe(26)
|
|
for (const t of templates) {
|
|
expect(t.slug, `${t.name} has no slug`).toMatch(/^[a-z0-9]+(-[a-z0-9]+)*$/)
|
|
expect(t.lines.length).toBeGreaterThanOrEqual(2)
|
|
}
|
|
expect(new Set(templates.map((t) => t.slug)).size).toBe(templates.length)
|
|
})
|
|
|
|
it('carries the statutory note, which exists nowhere else', async () => {
|
|
const { templates } = await read()
|
|
// legal_note has no column in booking_template_library, so if this resource
|
|
// dropped it the information would be unreachable to an agent entirely.
|
|
const withNote = templates.filter((t) => t.legal_note)
|
|
expect(withNote.length).toBeGreaterThan(0)
|
|
expect(withNote.some((t) => t.slug.startsWith('periodiseringsfond'))).toBe(true)
|
|
})
|
|
|
|
it('orders templates by meta.order, matching what the user sees', async () => {
|
|
const { templates } = await read()
|
|
// The gallery and the docs sort on meta.order; an agent reading a different
|
|
// order would describe "the first template" as something else again.
|
|
const slugs = templates.map((t) => t.slug)
|
|
expect(slugs).toEqual([...new Set(slugs)])
|
|
expect(slugs[0]).toBeTruthy()
|
|
})
|
|
|
|
it('keeps account numbers as strings', async () => {
|
|
const { templates } = await read()
|
|
for (const t of templates) {
|
|
for (const l of t.lines) {
|
|
expect(typeof l.account, `${t.slug} ${l.account}`).toBe('string')
|
|
expect(l.account).toMatch(/^\d{4}$/)
|
|
}
|
|
}
|
|
})
|
|
|
|
it('states the amount maths, so an agent does not invent a split', async () => {
|
|
const { how_to_apply } = await read()
|
|
expect(how_to_apply.vat_line).toContain('vat_rate')
|
|
expect(how_to_apply.business_line).toContain('ratio')
|
|
// The explicit instruction not to fudge an amount to force a balance.
|
|
expect(how_to_apply.balance).toMatch(/do not adjust/i)
|
|
})
|
|
|
|
it('tells an agent not to post from here', async () => {
|
|
const { notes } = await read()
|
|
expect(notes.posting).toMatch(/journal-entry tools/)
|
|
})
|
|
|
|
it('warns that a company may have its own templates beyond this list', async () => {
|
|
const { notes } = await read()
|
|
expect(notes.company_templates).toBeTruthy()
|
|
})
|
|
|
|
it('every vat line carries vat_rate and every other line carries ratio', async () => {
|
|
const { templates } = await read()
|
|
for (const t of templates) {
|
|
for (const l of t.lines) {
|
|
if (l.type === 'vat') {
|
|
expect(l.vat_rate, `${t.slug}: vat line without vat_rate`).toBeDefined()
|
|
expect(l.ratio).toBeUndefined()
|
|
} else {
|
|
expect(l.ratio, `${t.slug}: ${l.type} line without ratio`).toBeDefined()
|
|
expect(l.vat_rate).toBeUndefined()
|
|
}
|
|
}
|
|
}
|
|
})
|
|
})
|