Files
accounted/lib/agent/chat/__tests__/system-prompt.test.ts
T
MattssonandClaude Opus 4.8 2a8bf9b42e Bug/year end numbers (#744)
* fix(bookkeeping): allow creating a fiscal year that fills an interior gap

Fiscal-period creation only allowed chaining a new räkenskapsår before the
earliest or after the latest existing period, so a company with a gap between
years (e.g. 2024 + 2026 from an SIE import, missing 2025) could not create the
missing year — it failed with "New period must chain before the earliest or
after the latest existing period".

Generalise forward chaining onto the new period's immediate predecessor, which
covers both appending a new latest year and filling an interior gap. The
"prior year must be locked" guard now applies only to true appends, not gap
fills (a backfill, like backward chaining). previous_period_id is set to the
predecessor and the successor is relinked so the BFNAR 2013:2 continuity chain
stays intact. The create dialog suggests the missing year (capped so it never
overlaps the next period), the settings page seeds the dialog at the earliest
gap, and the default suggested name is now "Räkenskapsår <year>".

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(bookkeeping): omföra föregående års resultat (2099 → 2098) at year-end

Year-end closing posts the result to 2099 "Årets resultat" and the opening
balance carried it forward on 2099 every year, so 2099 accumulated across
years and the prior result never moved off "Årets resultat".

executeYearEndClosing now posts a separate "Omföring av föregående års
resultat" verifikat (Dr 2099 / Cr 2098 for a profit, reversed for a loss)
into the new period after the continuity check passes, so 2099 starts each
year at zero. Kept as a standalone entry rather than folded into the opening
balance so the IB stays a faithful mirror of the prior UB and IB/UB
continuity still holds. Aktiebolag only; idempotent; no-op when 2099 is flat.
The 2098 → 2091/2898 disposition (bolagsstämma decision) is intentionally
left to a separate step.

- new source_type 'result_appropriation' (migration + type + Zod enum)
- generateResultAppropriation helper (planner + poster) wired as step 11
- ResultStep surfaces the omföring voucher
- unit tests + pg-real invariant
- scripts/repair-result-appropriation.ts: retroactive catch-up (dry-run default)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(transactions): shadow-detect date-drift duplicate bank transactions

The content-dedup bridge buckets on exact (date, ore), so the same
transaction re-imported with a booking date that drifted a day lands in
a different bucket and slips past every dedup layer. Add a measure-only
("shadow") detector that flags would-be +/-1-day duplicates and counts
them, without changing what is inserted - so the gap can be validated on
real data before any enforcement, mirroring the scope-drift shadow.

- shiftIsoDate(): pure, deterministic adjacent-date helper
- ingest: DEDUP_DATE_DRIFT_MODE flag (default on), pre-loop bucket
  snapshot, per-row gate with desc-bridge + cross-channel-symmetry
  signals; logs shadow_date_drift_candidates, never alters inserts
- fail-safe date guard so the measurement can never abort an import
- regression tests for both signals, account/window/distinct guards,
  no-double-count, and the malformed-date fail-safe

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(bookkeeping): anonymize a customer reference in fiscal-period tests

Remove a real customer name ("AXMD AB") from regression-test comments;
no logic change.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(workflows): enhance Docker image scanning and caching mechanisms

* fix(bookkeeping): enhance year-end result appropriation handling and error reporting

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 18:22:09 +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')
})
})