Files
accounted/lib/import/__tests__/sie-import-coverage.test.ts
T
Jakob Wennberg bc61862e76 feat(agent): telemetry + CI-gate quick wins from the "AI systems that ship" audit (#677)
* feat(agent): telemetry completeness + durability, CI gates, commit_method provenance

Quick wins from the "Building AI systems that ship" audit:

- mcp.tool_called gains errorMessage (message_sv, truncated 500 chars) on
  all failure exits; new mcp.skill_loaded event on every gnubok_load_skill
  (all tiers) so atom usage is finally measurable
- event_log: (event_type, created_at) index; cleanup cron keeps
  mcp.*/agent.* telemetry 180 days (delivery events stay 30)
- CI: lint ratchet (npm run check:lint — 60 legacy errors baselined,
  fails only on NEW errors) and a pg-real coverage gate (migrations
  touching trigger/RPC/RLS/DEFERRABLE require a *.pg.test.ts change;
  escape hatch: -- pg-test: covered-by/skip)
- journal_entries.commit_method CHECK widened with 'api_key'/'agent';
  the MCP approve path records 'api_key' truthfully instead of
  'user_accept' (agent_first_vision §8 P0-1). 'agent' is reserved — ALL
  MCP traffic (incl. claude.ai OAuth, whose access_token is a minted
  API key) authenticates as api_key today

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

* feat(import): derive opening balances from prior-year #UB when SIE lacks #IB (#675)

SIE files exported without #IB 0 rows (only #UB -1) previously imported
with zero opening balances. getEffectiveOpeningBalances() now derives IB
from prior-year UB for balance-sheet accounts when explicit #IB is
absent, surfaces the derivation as an info issue in the import preview,
and excludes share-capital vouchers from opening-balance detection.
Detection regexes are shared between parser and importer so the two
checks cannot drift. 507 lib/import tests pass.

(Authored in a parallel session in this checkout; included per request.)

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

* fix(review): address PR #677 bot findings — RoPA entry, execFileSync, gate scope note

Triage of the compliance-swarm + Greptile findings:

Applied:
- .compliance/ropa.yaml: new mcp.telemetry processing activity declaring
  the 180-day mcp.*/agent.* retention, lawful basis, data categories, and
  the no-args/no-results minimisation (ISO A.8.10, GDPR Art.5(1)(c) —
  the retention split is now formally documented, referenced from the cron)
- check-pg-test-coverage.mjs: execFileSync with argv array — no shell, so
  a hostile base-ref can't inject (ASVS V13.2.1); verified an injection
  attempt exits 2 without executing
- check-pg-test-coverage.mjs: documented the PR-level (not per-migration)
  scope of the gate so reviewers know to check coverage per migration when
  a PR carries several risky migrations (Greptile P2)

Acknowledged, no change:
- errorMessage PII risk: messages are domain-mapped strings; event_log
  already persists far richer delivery payloads under the same RLS; now
  declared in ropa.yaml
- cron error envelope: errorResponse maps to the canonical safe envelope
  and the endpoint is CRON_SECRET-gated
- two-pass delete "partial state": TTL deletes are idempotent — the next
  daily run sweeps whatever a failed pass left behind
- skill_loaded actorLabel/sessionId: mirrors the pre-existing
  mcp.tool_called payload; sessionId is the join key the analytics exist for

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

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 15:47:13 +02:00

340 lines
10 KiB
TypeScript

/**
* Regression suite for the Lookma AB support case (2026-05-28).
*
* The bug: gnubok_import_sie + executeSIEImport accepted mappings that
* couldn't cover a single account in the file. The per-voucher loop then
* silently skipped every verifikation, finalizeImportRecord marked the
* sie_imports row 'completed' with transactions_count=0, and the partial
* unique index on (company_id, file_hash) held the slot — blocking retry.
*
* The fix layers three guards:
* 1. Stage-time refusal in gnubok_import_sie (covered in
* extensions/general/mcp-server/__tests__/import-sie-stage.test.ts).
* 2. Defense-in-depth refusal in executeSIEImport (this file).
* 3. Finalizer downgrade of any 0-entry success to 'failed' (this file).
*/
import { describe, it, expect } from 'vitest'
import { executeSIEImport, finalizeImportRecord } from '../sie-import'
import { createQueuedMockSupabase } from '@/tests/helpers'
import type { ParsedSIEFile, AccountMapping, ImportResult } from '../types'
import type { SupabaseClient } from '@supabase/supabase-js'
function makeParsedFile(overrides?: Partial<ParsedSIEFile>): ParsedSIEFile {
return {
header: {
sieType: 4,
flagga: 0,
program: 'TestProg',
programVersion: '1.0',
generatedDate: '2024-01-01',
format: 'PC8',
companyName: 'Lookma Mock AB',
orgNumber: '5567201701',
address: null,
fiscalYears: [{ yearIndex: 0, start: '2024-01-01', end: '2024-12-31' }],
currency: 'SEK',
kontoPlanType: null,
},
accounts: [
{ number: '1930', name: 'Företagskonto' },
{ number: '6110', name: 'Kontorsmaterial' },
],
openingBalances: [{ yearIndex: 0, account: '1930', amount: 50000 }],
closingBalances: [],
resultBalances: [],
vouchers: [
{
series: 'A',
number: 1,
date: new Date(2024, 0, 15),
description: 'Inköp',
lines: [
{ account: '6110', amount: 1000 },
{ account: '1930', amount: -1000 },
],
},
],
issues: [],
stats: {
totalAccounts: 2,
totalVouchers: 1,
totalTransactionLines: 2,
fiscalYearStart: '2024-01-01',
fiscalYearEnd: '2024-12-31',
},
...overrides,
}
}
function makeMapping(source: string, target: string | null): AccountMapping {
return {
sourceAccount: source,
sourceName: `Account ${source}`,
targetAccount: target as string,
targetName: target ? `Target ${target}` : '',
confidence: target ? 1 : 0,
matchType: target ? 'exact' : 'manual',
isOverride: false,
}
}
describe('executeSIEImport — defense-in-depth coverage check', () => {
it('refuses to insert a sie_imports row when mappings is empty', async () => {
const { supabase } = createQueuedMockSupabase()
const parsed = makeParsedFile()
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
parsed,
[],
{
filename: 'lookma.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: false,
importTransactions: true,
},
)
expect(result.success).toBe(false)
expect(result.importId).toBeNull()
expect(result.errors.join(' ')).toMatch(/täcker inga konton/i)
})
it('refuses when mappings exist but cover none of the file\'s accounts', async () => {
const { supabase } = createQueuedMockSupabase()
const parsed = makeParsedFile()
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
parsed,
[makeMapping('9999', '9999')],
{
filename: 'wrong.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: false,
importTransactions: true,
},
)
expect(result.success).toBe(false)
expect(result.importId).toBeNull()
expect(result.errors.join(' ')).toMatch(/täcker inga konton/i)
})
it('still rejects mappings with targetAccount=null (existing guard)', async () => {
const { supabase } = createQueuedMockSupabase()
const parsed = makeParsedFile()
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
parsed,
[makeMapping('6110', null), makeMapping('1930', null)],
{
filename: 'half.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: false,
importTransactions: true,
},
)
expect(result.success).toBe(false)
expect(result.errors.join(' ')).toMatch(/not mapped/i)
})
})
describe('finalizeImportRecord — 0-entry downgrade', () => {
it('flips a 0-entry success to status=failed and records the reason', async () => {
const { supabase } = createQueuedMockSupabase()
const result: ImportResult = {
success: true,
importId: 'imp-1',
fiscalPeriodId: 'fp-1',
openingBalanceEntryId: null,
journalEntriesCreated: 0,
journalEntryIds: [],
errors: [],
warnings: ['100 verifikationer hoppades över med ej mappade konton'],
replacedPriorImport: null,
}
await finalizeImportRecord(
supabase as unknown as SupabaseClient,
'imp-1',
'company-1',
result,
'#dummy',
)
expect(result.success).toBe(false)
expect(result.errors.join(' ')).toMatch(/0 verifikationer/i)
})
it('leaves a successful run with entries alone', async () => {
const { supabase } = createQueuedMockSupabase()
const result: ImportResult = {
success: true,
importId: 'imp-2',
fiscalPeriodId: 'fp-2',
openingBalanceEntryId: null,
journalEntriesCreated: 42,
journalEntryIds: Array(42).fill('je'),
errors: [],
warnings: [],
replacedPriorImport: null,
}
await finalizeImportRecord(
supabase as unknown as SupabaseClient,
'imp-2',
'company-1',
result,
'#dummy',
)
expect(result.success).toBe(true)
expect(result.errors).toEqual([])
})
it('leaves a 0-voucher run alone when an OB entry was created', async () => {
const { supabase } = createQueuedMockSupabase()
const result: ImportResult = {
success: true,
importId: 'imp-3',
fiscalPeriodId: 'fp-3',
openingBalanceEntryId: 'ob-1',
journalEntriesCreated: 1,
journalEntryIds: ['ob-1'],
errors: [],
warnings: [],
replacedPriorImport: null,
}
await finalizeImportRecord(
supabase as unknown as SupabaseClient,
'imp-3',
'company-1',
result,
'#dummy',
)
expect(result.success).toBe(true)
})
})
describe('executeSIEImport — coverage check with derived IB (issue #675)', () => {
// SIE type 1/2-style file: no vouchers, no #IB 0 — only #UB -1. The
// current-year IB must be derived from #UB -1, and the derived accounts
// must feed the coverage guard (before the fix this set was empty, so the
// guard never inspected UB-1-only files at all).
function makeUb1OnlyFile(): ParsedSIEFile {
return makeParsedFile({
openingBalances: [],
closingBalances: [
{ yearIndex: -1, account: '1930', amount: 37400.78 },
{ yearIndex: -1, account: '2010', amount: -37400.78 },
],
vouchers: [],
stats: {
totalAccounts: 2,
totalVouchers: 0,
totalTransactionLines: 0,
fiscalYearStart: '2024-01-01',
fiscalYearEnd: '2024-12-31',
},
})
}
it('refuses when mappings cover none of the derived IB accounts', async () => {
const { supabase } = createQueuedMockSupabase()
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
makeUb1OnlyFile(),
[makeMapping('9999', '9999')],
{
filename: 'ub1-only.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: true,
importTransactions: true,
},
)
expect(result.success).toBe(false)
expect(result.importId).toBeNull()
expect(result.errors.join(' ')).toMatch(/täcker inga konton/i)
})
it('passes the coverage guard when mappings cover the derived IB accounts', async () => {
const { supabase, enqueueMany } = createQueuedMockSupabase()
// Past the guard the flow proceeds: dup check → stale cleanup → pending
// record insert → chart fetch → period-dup check → find fiscal period
// (null → clean stop with a NON-coverage error, which is all this test
// needs to prove).
enqueueMany([
{ data: null }, // checkDuplicateImport
{ data: null }, // cleanupStaleImportRecords delete
{ data: { id: 'imp-1' } }, // createPendingImportRecord insert
{ data: [] }, // syncMappedAccounts chart fetch
{ data: null }, // chart insert (missing accounts)
{ data: null }, // checkDuplicatePeriodImport
{ data: null }, // find existing fiscal period → stops here
])
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
makeUb1OnlyFile(),
[makeMapping('1930', '1930'), makeMapping('2010', '2010')],
{
filename: 'ub1-only.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: true,
importTransactions: true,
},
)
expect(result.errors.join(' ')).not.toMatch(/täcker inga konton/i)
expect(result.errors.join(' ')).toMatch(/No matching fiscal period found/i)
})
it('skips the IB accounts in the guard when importOpeningBalances is false', async () => {
const { supabase } = createQueuedMockSupabase()
const result = await executeSIEImport(
supabase as unknown as SupabaseClient,
'company-1',
'user-1',
makeUb1OnlyFile(),
[makeMapping('9999', '9999')],
{
filename: 'ub1-only.se',
fileContent: '#dummy',
createFiscalPeriod: false,
importOpeningBalances: false,
importTransactions: true,
},
)
// No vouchers + IB import disabled → sourceAccountsInFile is empty and
// the guard does not fire (existing semantics preserved).
expect(result.errors.join(' ')).not.toMatch(/täcker inga konton/i)
})
})