Files
accounted/extensions/general/mcp-server/__tests__/sie-drop-widget.test.ts
T
0f7625535b feat(mcp): drag-and-drop SIE import card: exact bytes via tools/call, no model in the byte path (#1957)
E2E #6: the flow ordered SIE-first correctly, but the agent never
discovered gnubok_create_sie_upload, ran a local preflight, and sent the
user to the web wizard again; it also rendered a duplicate generic bank
card before the Swedbank-specific one.

1. New sie-drop widget (ui://sie-drop/app.html), rendered
   definition-level by gnubok_create_sie_upload: the user drags the
   .se/.sie file onto the card, the widget reads the EXACT bytes
   (FileReader), computes sha256 (WebCrypto), calls
   gnubok_sie_preflight via tools/call with file_content_base64 +
   sha256, shows the verdict, and on Importera stages
   gnubok_import_sie with the preflight's mappings. No network from
   the iframe, no model reproduction: byte path goes through the host
   bridge only, narrated into chat via ui/updateContext.

2. The inline size cap now applies only WITHOUT sha256: a hash-verified
   payload is byte-exact by proof, so the widget's 100 KB+ base64
   passes while unhashed model-retyped content stays refused.

3. Discovery + ordering fixes: create_sie_upload/preflight/import
   descriptions name the card path explicitly; create_company's
   history_note points at the card; connect_bank description says pass
   bank on the FIRST call when the user has named it (the duplicate
   generic card came from a bare call followed by the nudged retry).

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

47 lines
1.9 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('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' },
})
})
})