* feat(mcp): accept customer_number on gnubok_create_customer Parity with gnubok_update_customer: a customer number no longer needs a create-then-update two-step with two approvals. The staged params carry the trimmed number, commitCreateCustomer inserts it, and the payload-size ceiling is bumped 59.7K to 59.75K with a documented entry (the property has no description; name + maxLength are the whole contract). Requested by a user on Discord 2026-08-16. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * feat(ui): mark webshop integrations and orders tab as Beta WooCommerce and Shopify rows on the import page get a quiet Beta chip next to the title, and the webshop /orders sidebar item sets the existing betaBadge flag. Chip recipe matches the nav beta badge so Beta reads identically everywhere. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(mcp): enforce customer_number invariants and show it on the approval card Consolidated resolution pass for PR #1677: - skeptic (correctness): maxLength 32 was advertisement-only on the create path; now enforced with a runtime guard in gnubok_create_customer execute (clean errors for non-string and >32) and a 400 guard in commitCreateCustomer, matching the web/v1 routes and commitUpdateCustomer. - skeptic (correctness): CustomerPreview never rendered the staged customer_number, leaving the approver blind to the new field; added a conditional Kundnr row. - CodeRabbit: reset the event bus in create-customer.test.ts beforeEach. - Tests cover both new guards at the tool and executor layers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
97 lines
3.2 KiB
TypeScript
97 lines
3.2 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createQueuedMockSupabase } from '@/tests/helpers'
|
|
import { eventBus } from '@/lib/events/bus'
|
|
import { tools } from '../server'
|
|
|
|
const tool = () => tools.find((candidate) => candidate.name === 'gnubok_create_customer')!
|
|
|
|
describe('gnubok_create_customer: customer_number input', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
eventBus.clear()
|
|
})
|
|
|
|
it('exposes customer_number in the strict input schema', () => {
|
|
const properties = tool().inputSchema.properties as Record<string, Record<string, unknown>>
|
|
expect(tool().inputSchema.additionalProperties).toBe(false)
|
|
expect(properties.customer_number).toMatchObject({ type: 'string', maxLength: 32 })
|
|
})
|
|
|
|
it('stages the trimmed customer_number in params and preview', async () => {
|
|
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: 'op-create-customer-1' } })
|
|
|
|
const result = (await tool().execute(
|
|
{
|
|
name: 'Kund AB',
|
|
customer_type: 'swedish_business',
|
|
customer_number: ' K-1001 ',
|
|
email: 'faktura@example.test',
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; operation_id?: string; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(true)
|
|
expect(result.operation_id).toBe('op-create-customer-1')
|
|
expect(result.preview).toMatchObject({
|
|
customer_number: 'K-1001',
|
|
email: 'faktura@example.test',
|
|
})
|
|
|
|
const inserted = findCall('pending_operations', 'insert')?.[0] as {
|
|
params: Record<string, unknown>
|
|
}
|
|
expect(inserted.params).toMatchObject({ customer_number: 'K-1001' })
|
|
})
|
|
|
|
it('rejects a customer_number longer than 32 characters before staging', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
tool().execute(
|
|
{ name: 'Kund AB', customer_type: 'swedish_business', customer_number: 'X'.repeat(33) },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/32/)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a non-string customer_number before staging', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
tool().execute(
|
|
{ name: 'Kund AB', customer_type: 'swedish_business', customer_number: 1001 },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/string/i)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('stages customer_number as null when omitted', async () => {
|
|
const { supabase, enqueue, findCall } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: 'op-create-customer-2' } })
|
|
|
|
const result = (await tool().execute(
|
|
{ name: 'Kund AB', customer_type: 'swedish_business' },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(true)
|
|
expect(result.preview).toMatchObject({ customer_number: null })
|
|
|
|
const inserted = findCall('pending_operations', 'insert')?.[0] as {
|
|
params: Record<string, unknown>
|
|
}
|
|
expect(inserted.params).toMatchObject({ customer_number: null })
|
|
})
|
|
})
|