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