Files
accounted/lib/pending-operations/__tests__/tool-name.test.ts
T
ce6efdb3dc refactor(pending): one pending-op-owned preview for chat, /pending and flow views (#1537)
* refactor(pending): one pending-op-owned preview for chat, /pending and flow views

A staged pending_operation was rendered three separate ways: the /pending
page's OperationPreview switch (8 specialized renderers keyed on
operation_type), ApprovalCard's own PreviewBlock (near-duplicate renderers
keyed on 4 hardcoded MCP tool names), and AgentChat's toolNameFor() hack
that mapped stored operation_types onto 'gnubok_'-prefixed tool names on
hydration. This is the weakest seam ahead of flow-run views (plan seam
8.3): every new operation type had to be taught to render in two places
and silently degraded in the third.

Now there is one owner:

- components/pending-operations/OperationPreview.tsx: the /pending
  renderers moved verbatim, dispatched on operation_type, consumed by
  /pending, ApprovalCard and future flow-run views.
- components/pending-operations/vocabulary.ts: operation labels,
  single-action warnings and the one canonical rejection-category list
  (ApprovalCard's copy was byte-identical and is deleted).
- lib/pending-operations/tool-name.ts: the single translation point
  between bare operation_types and 'gnubok_' tool names, with tests.

toolNameFor gotcha fixed on the way: ApprovalCard's old dispatch only
recognized 4 tool names, so a hydrated card for any other operation type
(attach_document_to_transaction, match_transaction_invoice, ...) silently
fell back to a raw generic preview. Hydration now passes the stored
operation_type straight through attachStagedOperations to the card, and
live streamed cards derive it from the event's tool name, so every
operation type keeps its specialized preview on resume.

Per-surface chrome (list row on /pending vs inline chat card) is
deliberately kept: only the preview + vocabulary were the duplicated seam.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: drop a stray hunt_title copy rename that rode along

'Kvittojakten' -> 'Leta efter underlag' in messages/sv.json was
uncommitted working-tree state from another session, swept into the
extraction commit by git add breadth. It is a product-naming call with
no en.json counterpart and does not belong in this refactor; preserved
in this branch's first commit if it turns out to be wanted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pending): carry params to chat previews; guard preview amounts

CodeRabbit round on #1537, both real. (1) AttachDocumentPreview renders
its DocumentViewButton from params.document_id, which neither chat path
carried: the staged_operation stream event now includes the tool-use
input (the same values the staging tool stored as
pending_operations.params) and hydration selects the params column, so
an attach-document card in chat shows its evidence button live and on
resume. (2) InvoicePreview and CreateTransactionPreview cast amounts
straight into formatCurrency; a payload without one rendered 'NaN kr'.
They now share the same show-the-gap guard the legacy summary already
had.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

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

68 lines
2.2 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
operationTypeFromToolName,
toolNameForOperationType,
} from '../tool-name'
/**
* pending_operations.operation_type is the bare action name; the MCP layer
* and the live chat stream carry 'gnubok_'-prefixed tool names. Getting the
* mapping wrong is invisible in mocks but silently drops every hydrated
* approval card to the generic raw preview (the toolNameFor gotcha this
* module replaces), so the round-trip is pinned here.
*/
describe('operationTypeFromToolName', () => {
it('strips the gnubok_ prefix from an MCP tool name', () => {
expect(operationTypeFromToolName('gnubok_categorize_transaction')).toBe(
'categorize_transaction',
)
expect(operationTypeFromToolName('gnubok_attach_document_to_transaction')).toBe(
'attach_document_to_transaction',
)
})
it('passes an already-bare operation_type through unchanged', () => {
expect(operationTypeFromToolName('categorize_transaction')).toBe(
'categorize_transaction',
)
})
it('passes a non-gnubok tool name through unchanged', () => {
expect(operationTypeFromToolName('some_vendor_tool')).toBe('some_vendor_tool')
expect(operationTypeFromToolName('remember_fact')).toBe('remember_fact')
})
})
describe('toolNameForOperationType', () => {
it('prefixes a bare operation_type', () => {
expect(toolNameForOperationType('create_voucher')).toBe('gnubok_create_voucher')
})
it('never double-prefixes an already-prefixed name', () => {
expect(toolNameForOperationType('gnubok_create_voucher')).toBe(
'gnubok_create_voucher',
)
})
})
describe('round-trip', () => {
// Every operation type with a specialized preview renderer must survive
// the trip in both directions: this is exactly the path a hydrated
// approval card's preview dispatch takes.
const opTypes = [
'categorize_transaction',
'create_customer',
'create_invoice',
'create_transaction',
'create_voucher',
'correct_entry',
'attach_document_to_transaction',
'match_transaction_invoice',
]
it.each(opTypes)('%s -> tool name -> back', (opType) => {
expect(operationTypeFromToolName(toolNameForOperationType(opType))).toBe(opType)
})
})