Files
accounted/lib/pending-operations/__tests__/should-auto-commit.test.ts
T
Mattsson bb855d2ddc Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports

* feat(auth): enhance API key scopes and add bookkeeping write scope

- Updated transaction write scope description to include additional tools.
- Enhanced reports read scope description to reflect new functionality.
- Introduced bookkeeping write scope with relevant description.
- Updated SCOPE_GROUPS to include bookkeeping domain.
- Modified TOOL_SCOPE_MAP to include new bookkeeping operations.
- Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution.

feat(tests): add unit tests for MCP resource registry

- Created tests for data resources to ensure all required fields are present.
- Added tests for resource query parsing and retrieval.

feat(resources): implement MCP resources for company and accounting data

- Added capabilities resource to expose API key capabilities based on granted scopes.
- Implemented chart of accounts resource to retrieve active BAS chart.
- Created company current resource to fetch active company details.
- Developed active fiscal period resource to check posting eligibility.
- Implemented recent activity resource to fetch latest journal entries, invoices, and transactions.
- Added VAT treatments resource to provide available VAT rates per customer type.

feat(pending-operations): introduce risk tiers for operations

- Added risk level classification for pending operations to determine auto-commit eligibility.
- Implemented functions to classify operation risk levels and identify high-risk operations.

feat(migrations): add actor model and risk tier to pending operations

- Updated pending_operations table to include actor type and risk level columns.
- Enhanced audit_log to mirror actor information for compliance.
- Modified validate_and_increment_api_key function to return actor details.
- Expanded operation types in pending_operations to include new high-risk operations.

* feat: add auto-commit functionality for low-risk pending operations

- Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings.
- Created commitPendingOperation function to handle execution of pending operations with consistent status updates.
- Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds.
- Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality.
- Added SQL migration to update the database schema for new auto-commit settings.

* feat(idempotency): implement idempotency key handling for safe retries and cleanup

* feat: expand API key scopes and pending operations for bookkeeping

- Added 'suppliers:write' scope to API key scopes for supplier invoice management.
- Updated SCOPE_GROUPS to include the new 'suppliers:write' scope.
- Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice.
- Implemented corresponding commit functions for the new operations in the pending operations module.
- Enhanced PendingOperation type to include actor model and risk level attributes.
- Added tests for new functionality, ensuring proper behavior and constraints in the database.

* feat: implement unlockPeriod functionality and related tests

* feat: add agent auto-commit settings and related functionality

* feat: add attention resource with comprehensive summary of outstanding tasks

* feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes
2026-05-04 11:12:29 +02:00

155 lines
5.3 KiB
TypeScript

import { describe, it, expect, vi } from 'vitest'
import { shouldAutoCommit } from '../should-auto-commit'
function mockSettingsClient(settings: { agent_auto_commit_enabled?: boolean; agent_auto_commit_max_amount?: number | null } | null) {
return {
from: vi.fn().mockReturnValue({
select: vi.fn().mockReturnValue({
eq: vi.fn().mockReturnValue({
maybeSingle: vi.fn().mockResolvedValue(
settings === null
? { data: null, error: null }
: { data: settings, error: null }
),
}),
}),
}),
} as never
}
describe('shouldAutoCommit', () => {
it('rejects high-risk ops regardless of any other config', async () => {
const supabase = mockSettingsClient({ agent_auto_commit_enabled: true })
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'send_invoice',
actorType: 'api_key',
})
expect(decision.eligible).toBe(false)
expect(decision.risk_level).toBe('high')
expect(decision.reason).toContain('high-risk')
})
it('rejects user actors (they approve via UI)', async () => {
const supabase = mockSettingsClient({ agent_auto_commit_enabled: true })
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'user',
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('approve via the UI')
})
it('rejects when company has not opted in', async () => {
const supabase = mockSettingsClient({ agent_auto_commit_enabled: false })
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('not opted in')
})
it('rejects medium-risk ops in current phase', async () => {
const supabase = mockSettingsClient({ agent_auto_commit_enabled: true })
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'categorize_transaction',
actorType: 'api_key',
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('low-risk')
})
it('approves low-risk ops from api_key with company opt-in', async () => {
const supabase = mockSettingsClient({ agent_auto_commit_enabled: true })
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
})
expect(decision.eligible).toBe(true)
expect(decision.risk_level).toBe('low')
})
it('blocks low-risk op when amount exceeds threshold', async () => {
const supabase = mockSettingsClient({
agent_auto_commit_enabled: true,
agent_auto_commit_max_amount: 1000,
})
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
amount: 5000,
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('exceeds')
})
it('approves low-risk op when amount within threshold', async () => {
const supabase = mockSettingsClient({
agent_auto_commit_enabled: true,
agent_auto_commit_max_amount: 1000,
})
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
amount: 500,
})
expect(decision.eligible).toBe(true)
})
it('approves low-risk op when amount missing (no threshold check applies)', async () => {
const supabase = mockSettingsClient({
agent_auto_commit_enabled: true,
agent_auto_commit_max_amount: 1000,
})
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
// amount intentionally undefined
})
expect(decision.eligible).toBe(true)
})
it('cron actors auto-commit non-high-risk without DB lookup', async () => {
const supabase = mockSettingsClient(null)
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'cron',
})
expect(decision.eligible).toBe(true)
expect(decision.reason).toContain('Cron')
})
it('cron actors still rejected for high-risk ops', async () => {
const supabase = mockSettingsClient(null)
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'send_invoice',
actorType: 'cron',
})
expect(decision.eligible).toBe(false)
})
it('falls back to human approval when settings missing', async () => {
const supabase = mockSettingsClient(null)
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('Could not read company settings')
})
it('treats negative amounts (refunds) by absolute value', async () => {
const supabase = mockSettingsClient({
agent_auto_commit_enabled: true,
agent_auto_commit_max_amount: 1000,
})
const decision = await shouldAutoCommit(supabase, 'company-1', {
operationType: 'create_customer',
actorType: 'api_key',
amount: -5000,
})
expect(decision.eligible).toBe(false)
expect(decision.reason).toContain('exceeds')
})
})