Files
accounted/lib/agent/intents/__tests__/bokslut-step.test.ts
T

201 lines
7.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import type { SupabaseClient } from '@supabase/supabase-js'
import { bokslutStep } from '../bokslut-step'
// fiscal_periods has no `status` column: state is is_closed + locked_at
// (see lib/core/bookkeeping/period-service.ts and the period-lock triggers).
// Selecting `status` made PostgREST reject the whole query, so the bokslut
// intent never resolved a period and silently dropped the räkenskapsår from
// the prompt. The mocked Supabase harness happily returns whatever rows the
// test enqueues regardless of the column list, so these tests assert the
// SELECT string itself, plus the open/locked/closed/unknown prompt branches.
interface QueryResult {
data: unknown
error: unknown
}
interface RecordedSelect {
table: string
columns: string
}
function createRecordingSupabase(results: Record<string, QueryResult>) {
const selects: RecordedSelect[] = []
const buildChain = (table: string) => {
const chain = {
select: (columns: string) => {
selects.push({ table, columns })
return chain
},
eq: () => chain,
order: () => chain,
limit: () => chain,
maybeSingle: async (): Promise<QueryResult> =>
results[table] ?? { data: null, error: null },
}
return chain
}
const supabase = {
from: (table: string) => buildChain(table),
} as unknown as SupabaseClient
return { supabase, selects }
}
const COMPANY_ID = '22222222-2222-2222-2222-222222222222'
const PERIOD_ID = '33333333-3333-3333-3333-333333333333'
function periodRow(overrides: Record<string, unknown> = {}) {
return {
id: PERIOD_ID,
period_start: '2025-01-01',
period_end: '2025-12-31',
is_closed: false,
locked_at: null,
...overrides,
}
}
async function capture(results: Record<string, QueryResult>) {
const { supabase, selects } = createRecordingSupabase(results)
const captured = await bokslutStep.capture(
{ step_id: 'accruals', fiscal_year_end: null },
{ supabase, userId: 'user-1', companyId: COMPANY_ID },
)
return { captured, selects }
}
function render(captured: Awaited<ReturnType<typeof bokslutStep.capture>>) {
return bokslutStep.promptTemplate({ captured, profileSummary: null, activeMemory: [] })
}
describe('bokslut.step capture', () => {
it('selects the real fiscal_periods state columns, never a `status` column', async () => {
const { selects } = await capture({
fiscal_periods: { data: periodRow(), error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
const periodSelect = selects.find((s) => s.table === 'fiscal_periods')
expect(periodSelect).toBeDefined()
expect(periodSelect?.columns).toBe('id, period_start, period_end, is_closed, locked_at')
// The column does not exist: requesting it makes PostgREST 400 the query.
expect(periodSelect?.columns).not.toMatch(/\bstatus\b/)
})
it('maps an unlocked, unclosed period to status open', async () => {
const { captured } = await capture({
fiscal_periods: { data: periodRow(), error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
expect(captured.period_lookup_failed).toBe(false)
expect(captured.fiscal_period).toMatchObject({
id: PERIOD_ID,
period_start: '2025-01-01',
period_end: '2025-12-31',
status: 'open',
lock_date: null,
})
})
it('maps locked_at to status locked and is_closed to status closed', async () => {
const { captured: locked } = await capture({
fiscal_periods: { data: periodRow({ locked_at: '2026-02-01T10:00:00Z' }), error: null },
companies: { data: null, error: null },
})
expect(locked.fiscal_period?.status).toBe('locked')
expect(locked.fiscal_period?.lock_date).toBe('2026-02-01T10:00:00Z')
// is_closed wins over locked_at, same precedence as period-service.
const { captured: closed } = await capture({
fiscal_periods: {
data: periodRow({ is_closed: true, locked_at: '2026-02-01T10:00:00Z' }),
error: null,
},
companies: { data: null, error: null },
})
expect(closed.fiscal_period?.status).toBe('closed')
})
it('fails closed when the period lookup errors', async () => {
const { captured } = await capture({
fiscal_periods: { data: null, error: { message: 'column does not exist' } },
companies: { data: { entity_type: 'ab' }, error: null },
})
expect(captured.fiscal_period).toBeNull()
expect(captured.period_lookup_failed).toBe(true)
})
})
describe('bokslut.step prompt template', () => {
it('states the period is open without any lock warning', async () => {
const { captured } = await capture({
fiscal_periods: { data: periodRow(), error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
const out = render(captured)
expect(out).toContain('Räkenskapsår: 2025-01-01 → 2025-12-31 (status: öppen)')
expect(out).not.toContain('VARNING')
expect(out).not.toContain('Perioden är LÅST')
expect(out).not.toContain('Perioden är STÄNGD')
})
it('tells the agent not to propose bookings in a closed period', async () => {
const { captured } = await capture({
fiscal_periods: { data: periodRow({ is_closed: true }), error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
const out = render(captured)
expect(out).toContain('(status: stängd)')
expect(out).toContain('Perioden är STÄNGD')
expect(out).toContain('BFL 5 kap 5 §')
expect(out).toContain('Föreslå inga bokningar')
})
it('tells the agent a locked period must be unlocked first', async () => {
const { captured } = await capture({
fiscal_periods: { data: periodRow({ locked_at: '2026-02-01T10:00:00Z' }), error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
const out = render(captured)
expect(out).toContain('(status: låst)')
expect(out).toContain('Perioden är LÅST')
expect(out).toContain('2026-02-01T10:00:00Z')
})
it('never presents an unresolvable year as open', async () => {
const { captured: errored } = await capture({
fiscal_periods: { data: null, error: { message: 'boom' } },
companies: { data: { entity_type: 'ab' }, error: null },
})
const erroredOut = render(errored)
expect(erroredOut).toContain('uppslaget av räkenskapsåret misslyckades')
expect(erroredOut).toContain('OKÄND')
expect(erroredOut).toContain('Anta INTE att året är öppet')
expect(erroredOut).not.toContain('status: öppen')
const { captured: missing } = await capture({
fiscal_periods: { data: null, error: null },
companies: { data: { entity_type: 'ab' }, error: null },
})
const missingOut = render(missing)
expect(missingOut).toContain('inget räkenskapsår hittades')
expect(missingOut).toContain('Anta INTE att året är öppet')
expect(missingOut).not.toContain('status: öppen')
})
it('exposes gnubok_list_fiscal_periods so the fail-closed instruction is actionable', () => {
expect(bokslutStep.tools).toContain('gnubok_list_fiscal_periods')
expect(bokslutStep.tools).toContain('gnubok_year_end_readiness')
expect(bokslutStep.tools).toContain('gnubok_post_kontantmetod_cutoff')
})
})