Files
accounted/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts
T
53c4d0d194 feat(mcp): approve the SIE import in the drop card + post-import efterkontroll in the skill (#1963)
E2E #10 (the first fully successful drop-card run) left the staged
import stranded until the user prodded the agent, and the agent then
improvised an excellent post-import audit (skattekonto reconciliation,
missing 1630, over-stated payroll liabilities). Codify both:

1. The drop card now carries the approval: after staging, the button
   becomes 'Godkänn bokföringen' with a two-click BFL confirmation
   (confirmed=true armed on the deliberate second click, exactly the
   pending-operations widget pattern), then 'Bokfört' + a ui/updateContext
   pointing the agent at trial balance and voucher-gap follow-ups.

2. Skill: when the user writes after a card was shown, the FIRST call is
   list_pending_operations (an empty ledger does not mean the file never
   arrived); new Step 4b 'efterkontroll' codifies the audit pass
   (trial balance vs SIE, skattekonto vs 1630 with 8423/8314/6992 for
   ränta/avgifter, auto-created bank account names, underlag coverage,
   voucher gaps); memory-first extended with memory-back (save orgnr,
   bank, fiscal year, moms period after creation so the next
   conversation needs zero questions).

Co-authored-by: Jakob Wennberg <311770904+jakobwennberg-oss@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-26 21:57:45 +02:00

56 lines
2.3 KiB
TypeScript

/**
* SIE drop widget: registration and wiring. The byte path itself (drop →
* preflight → import via tools/call with file_content_base64 + sha256) is
* asserted structurally on the HTML; the sha256/cap semantics live in
* sie-preflight.test.ts.
*/
import { describe, expect, it } from 'vitest'
import { tools } from '../server'
import { findUiWidget } from '../widgets'
describe('SIE drop widget', () => {
const widget = findUiWidget('ui://sie-drop/app.html')
it('is registered and renders the drop zone', () => {
expect(widget).toBeDefined()
expect(widget?.html).toContain('<!DOCTYPE html>')
expect(widget?.html).toContain('Importera bokföring')
expect(widget?.html).toContain("addEventListener('drop'")
})
it('carries the approval in-card: two-click BFL confirm on the staged import', () => {
const html = widget!.html
expect(html).toContain("callTool('gnubok_approve_pending_operation'")
// High-risk approve arms confirmed=true only on the second, deliberate
// click: the human acknowledgment, never a default.
expect(html).toContain('args.confirmed = true')
expect(html).toContain('BFL 5 kap 5')
})
it('passes exact bytes through tools/call with a sha256, never retyped or fetched', () => {
const html = widget!.html
expect(html).toContain("callTool('gnubok_sie_preflight'")
expect(html).toContain("callTool('gnubok_import_sie'")
expect(html).toContain('file_content_base64')
expect(html).toContain('sha256')
expect(html).toContain("crypto.subtle.digest('SHA-256'")
// No network from the iframe: bytes travel via the host bridge only.
expect(html).not.toContain('fetch(')
expect(html).not.toContain('XMLHttpRequest')
})
it('performs the ui/initialize handshake and narrates via ui/updateContext', () => {
const html = widget!.html
expect(html).toContain("sendRequest('ui/initialize'")
expect(html).toContain("sendNotification('ui/notifications/initialized')")
expect(html).toContain("sendNotification('ui/updateContext'")
})
it('is attached definition-level to gnubok_create_sie_upload', () => {
const tool = tools.find((t) => t.name === 'gnubok_create_sie_upload')!
expect((tool as { _meta?: { ui: { resourceUri: string } } })._meta).toEqual({
ui: { resourceUri: 'ui://sie-drop/app.html' },
})
})
})