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>
56 lines
2.3 KiB
TypeScript
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' },
|
|
})
|
|
})
|
|
})
|