Files
accounted/lib/agent/chat/__tests__/system-prompt.test.ts
T
Jakob WennbergandClaude Sonnet 5 ec27228a8e style: remove em/en dashes repo-wide, add CLAUDE.md rule against them (#890)
Em dashes (—) and en dashes (–) had spread across comments, docs, tests,
and a few UI strings, reading as AI-generated boilerplate rather than
house style. Replaced each with punctuation matching its context: colon
for explanatory clauses, comma for asides, plain hyphen for numeric/legal
ranges (e.g. "21-23§"), "to"/"till" for date ranges, parentheses for
paired-dash asides. messages/en.json and messages/sv.json were fixed by
hand together to keep sv/en in sync.

Left untouched where the dash is the functional subject rather than
decorative punctuation: date-range-parser.ts's separator regex,
charset-repair.ts's CP1252 byte-mapping table (and its test), the SIE
encoding mojibake docs, generic-csv.ts's minus-sign normalizer, the
agent system-prompt files that already instruct against em dashes, and
a golden iXBRL test fixture compared byte-for-byte.

Also fixes two bugs surfaced along the way: an off-by-one in
ApiKeysPanel's scope-label split (a leftover from an earlier partial
pass), and a charset-repair test that had lost the literal en-dash it
exists to verify.

Regenerated the agent atom seed migration (skills:generate) since 27
SKILL.md files changed. Added a CLAUDE.md rule against em/en dashes,
with an explicit carve-out for the functional-dash cases above.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-04 15:58:06 +02:00

112 lines
4.8 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import type { AgentIntent } from '@/lib/agent/intents/types'
import { buildIdentityBlock } from '../system-prompt'
// buildIdentityBlock is the always-on Block 2 of the chat system prompt. Unlike
// the per-intent ground rules (which sit only in the first user message and
// fall out of salience deep in a conversation), this block is re-sent on every
// turn. These guards lock in the epistemics rules added after the agent
// confidently answered "matvaror är 12 %" from stale training memory: it
// dropped to 6 % in April 2026, and invented a "ränteintäkter från ALMI"
// concern by inferring a lending business from an SNI code.
type VatStatus = Parameters<typeof buildIdentityBlock>[0]['vatStatus']
// Minimal base-typed intent: buildIdentityBlock only reads id, sheetTitle and
// atoms.mode. (The concrete intents have narrow capture/template generics that
// don't unify with the base AgentIntent the builder expects; the real call site
// resolves intents through the registry as base-typed.)
const intent: AgentIntent = {
id: 'general.help',
buttonLabel: 'x',
sheetTitle: 'Fråga din assistent',
atoms: { mode: 'progressive', horizontal: [], includeCompanyVertical: false, includeCompanyModifiers: false },
tools: [],
model: 'claude-sonnet-4-6',
capture: async () => ({}),
promptTemplate: () => '',
}
function block(vatStatus: VatStatus): string {
return buildIdentityBlock({
intent,
companyId: 'c1',
companyName: 'Testbolaget AB',
firstName: 'Jakob',
profileSummary: null,
rankedMemory: [],
vatStatus,
today: '2026-01-01 (torsdag)',
// buildIdentityBlock never touches supabase; it's a pure render of args.
supabase: {} as unknown as SupabaseClient,
})
}
const VAT_STATES: VatStatus[] = [
null,
{ vat_registered: true, vat_number: 'SE556677889901' },
{ vat_registered: false, vat_number: null },
]
describe('chat system prompt: always-on epistemics rules', () => {
it('forces load-before-answer for regulatory figures, on every VAT status', () => {
for (const vs of VAT_STATES) {
const out = block(vs)
expect(out).toContain('# Säkerhet i sak: ladda reglerna, gissa aldrig från minnet')
// Must point at the load tool and demand reading before answering.
expect(out).toContain('gnubok_load_skill')
// The canonical staleness trap must be named so the rule is concrete,
// not abstract: a model answering food VAT "12 %" from memory is wrong.
expect(out).toContain('12 %→6 %')
}
})
it('kills the "I am sure" escape hatch and turns "are you sure?" into a verify signal', () => {
const out = block(null)
expect(out).toContain('ja, jag är säker')
expect(out).toContain('är du säker?')
// The instruction must be to load/verify, not to repeat the prior answer.
expect(out.toLowerCase()).toContain('upprepa')
})
it('forbids inferring the business from weak signals like SNI codes', () => {
const out = block(null)
expect(out).toContain('# Påstå inget om bolaget du inte grundat i data')
expect(out).toContain('SNI-kod')
// Resolve real uncertainty by reading data or asking: not by speculating.
expect(out).toMatch(/läsverktyg|fråga/i)
})
it('anchors relative-time reasoning to the supplied current date', () => {
// Without an explicit "today" the model dates "förra månaden" / overdue
// invoices / the current VAT period off its training cutoff. The date the
// caller passes must land verbatim in the always-on block.
const out = block(null)
expect(out).toContain('# Dagens datum')
expect(out).toContain('Idag är 2026-01-01 (torsdag).')
// Must tell the model to trust this over its own sense of "now".
expect(out).toContain('träningsdata')
})
it('addresses the user by their own tilltalsnamn, not owner/signatory names from the profile', () => {
// Regression: the agent answered "vad heter jag" with the registered
// firmatecknare's legal name from "Företagets profil" instead of the
// user's own chosen name. The role block must name the user (firstName)
// and explicitly demote company owner/signatory names.
const out = block(null)
expect(out).toContain('Jakob')
expect(out).toMatch(/tilltalsnamn/i)
expect(out).toContain('firmatecknare')
})
it('lets the agent read a pre-loaded atom directly instead of re-loading it', () => {
// Declarative intents pre-load swedish-vat etc. into Block 1, so the rule
// must not force a redundant gnubok_load_skill when the owning atom is
// already present. This nuance used to live only in the per-intent KÄLLOR
// line; it now lives here, in the single canonical epistemics home.
const out = block(null)
expect(out).toContain('redan laddad')
})
})