Files
accounted/extensions/general/mcp-server/__tests__/coded-domain-errors.test.ts
T
Jakob Wennberg 523beaa18b fix(mcp): give the missing-chart-accounts refusal a code agents can dispatch on (#2075)
getStructuredError resolves an error with no `code` to UNKNOWN_ERROR, whose
registry text is the constant "Något gick fel. Försök igen." So an agent
branching on `code` saw "unknown" for a failure whose own message already
named the accounts and the two tools that fix it.

On production that was 40 of the create_voucher failures in 60 days.

Everything needed already existed: the ACCOUNTS_NOT_IN_CHART code, its
registry entry, its remediation pointing at the chart-of-accounts resource,
and a typed error class that storno-service already throws. This path just
never attached the code.

Attached with Object.assign rather than by throwing AccountsNotInChartError,
because that class builds its own generic English message from the account
list and would discard the richer Swedish one, which is the more useful half.

Pinned by a test asserting the same message WITHOUT the code still resolves to
UNKNOWN_ERROR, so this cannot silently regress.

Not fixed here: the MCP server has ~155 bare Swedish-prose throws against 3
uses of codedError(), so most domain failures remain undispatchable. This
fixes the one instance telemetry actually proved rather than converting 155
sites blind.

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-31 22:01:27 +02:00

65 lines
3.0 KiB
TypeScript

/**
* Domain failures must carry a stable code, not just Swedish prose.
*
* `getStructuredError` resolves an error with no `code` to UNKNOWN_ERROR, whose
* registry message is the constant "Något gick fel. Försök igen." An agent
* branching on `code` therefore sees "unknown" even when the message it was
* handed says exactly what to do.
*
* Production, 60 days, bot actors excluded: UNKNOWN_ERROR was 645 of 1 024
* real-agent failures. 40 of those were gnubok_create_voucher refusing a
* booking because the BAS accounts were not active in the company's chart, a
* failure that already had a registry code (ACCOUNTS_NOT_IN_CHART) with a
* remediation, and a message naming the two tools that fix it. The code was
* simply never attached.
*
* The MCP server currently has ~155 bare `throw new Error('<Swedish prose>')`
* against 3 uses of codedError(), so this is one instance of a broad pattern.
* This file pins the instance that telemetry proved, and the shape any future
* fix should follow.
*/
import { describe, it, expect } from 'vitest'
import { getStructuredError } from '@/lib/errors/get-structured-error'
import { ACCOUNTS_NOT_IN_CHART } from '@/lib/bookkeeping/errors'
/** The exact shape extensions/general/mcp-server/server.ts now throws. */
function missingAccountsError(accounts: string[]) {
return Object.assign(
new Error(
`Kan inte skapa verifikation. Konton saknas i kontoplanen och finns inte i BAS 2026: ${accounts.join(', ')}. ` +
'Skapa kontot med gnubok_create_account, aktivera det med gnubok_update_account, eller välj andra konton.',
),
{ code: ACCOUNTS_NOT_IN_CHART, accountNumbers: accounts },
)
}
describe('missing chart accounts is a dispatchable failure', () => {
it('resolves to ACCOUNTS_NOT_IN_CHART, not UNKNOWN_ERROR', () => {
expect(getStructuredError(missingAccountsError(['4010'])).code).toBe('ACCOUNTS_NOT_IN_CHART')
})
it('would have been UNKNOWN_ERROR without the code', () => {
// The regression this pins. Same message, no code: an agent gets prose it
// cannot branch on, which is what production showed for 40 calls.
const uncoded = new Error(
'Kan inte skapa verifikation. Konton saknas i kontoplanen och finns inte i BAS 2026: 4010.',
)
expect(getStructuredError(uncoded).code).toBe('UNKNOWN_ERROR')
})
it('carries the registry remediation the agent can act on', () => {
const structured = getStructuredError(missingAccountsError(['3010']))
expect(structured.remediation?.description ?? '').toMatch(/activate/i)
})
it('keeps the richer message that names the remedy tools', () => {
// The registry's own text is the generic "Konton saknas i kontoplanen.".
// The thrown message is better because it names both the accounts and the
// tools, so it must survive being coded.
const structured = getStructuredError(missingAccountsError(['7971']))
const text = `${structured.message_sv} ${structured.message_en}`
expect(text).toContain('gnubok_create_account')
expect(text).toContain('7971')
})
})