951b33363b
Schedules and their template items now store {sie_dim_no: code} bags
(default_dimensions / dimensions), and the cron generator copies them
onto every spawned invoice + item, so recurring invoices book with the
same projekt/kostnadsstalle tags a manual invoice would. Wired through
the web CRUD routes, the staged-operation executors, and the MCP
create/update/list schedule tools (resolve-don't-select, resolutions
echoed in the preview).
Migration 20260728090000 adds the two jsonb columns (same shape+CHECK
as invoices/invoice_items, PR7 producer parity).
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
551 lines
20 KiB
TypeScript
551 lines
20 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import { createQueuedMockSupabase } from '@/tests/helpers'
|
|
import { TOOL_SCOPE_MAP } from '@/lib/auth/api-keys'
|
|
import { OPERATION_RISK_TIERS } from '@/lib/pending-operations/risk-tiers'
|
|
import { eventBus } from '@/lib/events/bus'
|
|
import { tools } from '../server'
|
|
|
|
const CUSTOMER_ID = '11111111-1111-4111-8111-111111111111'
|
|
const SCHEDULE_ID = '22222222-2222-4222-8222-222222222222'
|
|
|
|
const listTool = () => tools.find((t) => t.name === 'gnubok_list_recurring_schedules')!
|
|
const createTool = () => tools.find((t) => t.name === 'gnubok_create_recurring_schedule')!
|
|
const updateTool = () => tools.find((t) => t.name === 'gnubok_update_recurring_schedule')!
|
|
|
|
function currentSchedule(overrides: Record<string, unknown> = {}) {
|
|
return {
|
|
id: SCHEDULE_ID,
|
|
name: 'Månadsavgift',
|
|
status: 'active',
|
|
customer_id: CUSTOMER_ID,
|
|
day_of_month: 25,
|
|
send_hour: 8,
|
|
payment_terms_days: 30,
|
|
currency: 'SEK',
|
|
your_reference: null,
|
|
our_reference: null,
|
|
notes: null,
|
|
auto_send: false,
|
|
next_run_date: '2999-01-25',
|
|
customer: { name: 'Test Customer AB', email: 'billing@example.test' },
|
|
items: [
|
|
{ description: 'Support', quantity: 1, unit: 'st', unit_price: 5000, vat_rate: null, sort_order: 0 },
|
|
],
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks()
|
|
eventBus.clear()
|
|
})
|
|
|
|
describe('recurring schedule tools: registration', () => {
|
|
it('registers all three tools with strict input schemas', () => {
|
|
for (const tool of [listTool(), createTool(), updateTool()]) {
|
|
expect(tool).toBeDefined()
|
|
expect(tool.inputSchema.additionalProperties).toBe(false)
|
|
}
|
|
expect(listTool().annotations.readOnlyHint).toBe(true)
|
|
expect(createTool().annotations.readOnlyHint).toBe(false)
|
|
expect(updateTool().annotations.readOnlyHint).toBe(false)
|
|
})
|
|
|
|
it('maps scopes and risk tiers', () => {
|
|
expect(TOOL_SCOPE_MAP.gnubok_list_recurring_schedules).toBe('invoices:read')
|
|
expect(TOOL_SCOPE_MAP.gnubok_create_recurring_schedule).toBe('invoices:write')
|
|
expect(TOOL_SCOPE_MAP.gnubok_update_recurring_schedule).toBe('invoices:write')
|
|
expect(OPERATION_RISK_TIERS.create_recurring_schedule).toBe('medium')
|
|
expect(OPERATION_RISK_TIERS.update_recurring_schedule).toBe('medium')
|
|
})
|
|
|
|
it('both writes declare the staged-operation output contract', () => {
|
|
for (const tool of [createTool(), updateTool()]) {
|
|
const schema = tool.outputSchema as { properties?: Record<string, unknown>; required?: string[] }
|
|
expect(schema?.properties?.staged).toBeDefined()
|
|
expect(schema?.required).toContain('staged')
|
|
}
|
|
})
|
|
|
|
it('descriptions state day-of-month clamping and Stockholm send hour', () => {
|
|
for (const tool of [listTool(), createTool(), updateTool()]) {
|
|
expect(tool.description).toMatch(/clamps to the last day in shorter months/i)
|
|
expect(tool.description).toMatch(/Europe\/Stockholm/i)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('gnubok_list_recurring_schedules', () => {
|
|
it('returns qualified ids and sorted items', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({
|
|
data: [
|
|
{
|
|
id: SCHEDULE_ID,
|
|
name: 'Månadsavgift',
|
|
status: 'active',
|
|
customer_id: CUSTOMER_ID,
|
|
day_of_month: 31,
|
|
send_hour: 8,
|
|
payment_terms_days: 30,
|
|
currency: 'SEK',
|
|
auto_send: false,
|
|
next_run_date: '2026-08-31',
|
|
last_run_at: null,
|
|
last_invoice_id: null,
|
|
last_run_warning: null,
|
|
generated_count: 0,
|
|
customer: { name: 'Test Customer AB' },
|
|
items: [
|
|
{ description: 'Timmar', quantity: 1, unit: 'tim', unit_price: 50, vat_rate: 25, sort_order: 1 },
|
|
{ description: 'Support', quantity: 2, unit: 'st', unit_price: 100, vat_rate: null, sort_order: 0 },
|
|
],
|
|
},
|
|
],
|
|
count: 1,
|
|
})
|
|
|
|
const result = (await listTool().execute({}, 'company-1', 'user-1', supabase as never)) as {
|
|
schedules: Array<Record<string, unknown>>
|
|
count: number
|
|
total_count: number
|
|
}
|
|
|
|
expect(result.count).toBe(1)
|
|
expect(result.total_count).toBe(1)
|
|
const row = result.schedules[0]
|
|
expect(row.recurring_schedule_id).toBe(SCHEDULE_ID)
|
|
expect(row).not.toHaveProperty('id')
|
|
expect(row.customer_id).toBe(CUSTOMER_ID)
|
|
expect(row.customer_name).toBe('Test Customer AB')
|
|
expect(row.last_invoice_id).toBeNull()
|
|
expect(row.monthly_total_excl_vat).toBe(250)
|
|
expect((row.items as Array<{ description: string }>).map((i) => i.description)).toEqual([
|
|
'Support',
|
|
'Timmar',
|
|
])
|
|
})
|
|
})
|
|
|
|
describe('gnubok_create_recurring_schedule: validation and staging', () => {
|
|
const validArgs = {
|
|
customer_id: CUSTOMER_ID,
|
|
name: 'Månadsavgift',
|
|
day_of_month: 25,
|
|
items: [{ description: 'Support', quantity: 1, unit: 'st', unit_price: 5000 }],
|
|
}
|
|
|
|
it('rejects missing items before querying the database', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
createTool().execute(
|
|
{ customer_id: CUSTOMER_ID, name: 'X', day_of_month: 25 },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/items/i)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects a VAT rate outside the Swedish set before querying the database', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
createTool().execute(
|
|
{
|
|
...validArgs,
|
|
items: [{ description: 'Support', quantity: 1, unit: 'st', unit_price: 5000, vat_rate: 10 }],
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/vat_rate/i)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails when the customer is outside the selected company', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: null })
|
|
|
|
await expect(
|
|
createTool().execute({ ...validArgs, dry_run: true }, 'company-1', 'user-1', supabase as never),
|
|
).rejects.toThrow(/not found/i)
|
|
})
|
|
|
|
it('rejects auto_send when the customer has no email', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: null } })
|
|
|
|
await expect(
|
|
createTool().execute(
|
|
{ ...validArgs, auto_send: true, dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/email/i)
|
|
})
|
|
|
|
it('defaults auto_send to false and surfaces it explicitly in the dry-run preview', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: 'billing@example.test' } })
|
|
|
|
const result = (await createTool().execute(
|
|
{ ...validArgs, dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; dry_run?: boolean; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(false)
|
|
expect(result.dry_run).toBe(true)
|
|
expect(result.preview.auto_send).toBe(false)
|
|
expect(result.preview.monthly_total_excl_vat).toBe(5000)
|
|
expect(result.preview.projected_first_run_date).toMatch(/^\d{4}-\d{2}-\d{2}$/)
|
|
// Preview only: nothing staged.
|
|
expect(supabase.from).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('stages the schedule for approval at medium risk', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: 'billing@example.test' } })
|
|
enqueue({ data: { id: 'op-recurring-1' } })
|
|
|
|
const result = (await createTool().execute(validArgs, 'company-1', 'user-1', supabase as never)) as {
|
|
staged: boolean
|
|
operation_id?: string
|
|
risk_level: string
|
|
preview: Record<string, unknown>
|
|
}
|
|
|
|
expect(result).toMatchObject({
|
|
staged: true,
|
|
operation_id: 'op-recurring-1',
|
|
risk_level: 'medium',
|
|
})
|
|
expect(result.preview.auto_send).toBe(false)
|
|
expect(supabase.from).toHaveBeenNthCalledWith(2, 'pending_operations')
|
|
})
|
|
|
|
it('stages an explicit auto_send=true at HIGH risk with the flag visible in the preview', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: 'billing@example.test' } })
|
|
enqueue({ data: { id: 'op-recurring-2' } })
|
|
|
|
const result = (await createTool().execute(
|
|
{ ...validArgs, auto_send: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; risk_level: string; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(true)
|
|
expect(result.preview.auto_send).toBe(true)
|
|
// Param escalation (risk-tiers paramEscalatedRisk): an auto-sending
|
|
// schedule is a standing order for outbound customer email with no
|
|
// per-send approval, the same external side-effect that puts one-off
|
|
// send_invoice at 'high'. The static tier stays 'medium' (asserted
|
|
// above); the staged operation must carry the escalated level so
|
|
// auto-commit can never touch it and approval requires confirmed=true.
|
|
expect(result.risk_level).toBe('high')
|
|
})
|
|
})
|
|
|
|
describe('gnubok_update_recurring_schedule: validation and staging', () => {
|
|
it('requires at least one changed field', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
updateTool().execute({ schedule_id: SCHEDULE_ID, dry_run: true }, 'company-1', 'user-1', supabase as never),
|
|
).rejects.toThrow(/at least one/i)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('rejects an unknown status before querying the database', async () => {
|
|
const { supabase } = createQueuedMockSupabase()
|
|
|
|
await expect(
|
|
updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, status: 'stopped', dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/status/i)
|
|
expect(supabase.from).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('fails when the schedule is outside the selected company', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: null })
|
|
|
|
await expect(
|
|
updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, status: 'paused', dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/not found/i)
|
|
})
|
|
|
|
it('rejects enabling auto_send when the current customer has no email', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule({ customer: { name: 'Test Customer AB', email: null } }) })
|
|
|
|
await expect(
|
|
updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, auto_send: true, dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/email/i)
|
|
})
|
|
|
|
it('stages an update that enables auto_send at HIGH risk', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule() }) // current has auto_send: false + customer email
|
|
enqueue({ data: { id: 'op-recurring-4' } })
|
|
|
|
const result = (await updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, auto_send: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; risk_level: string }
|
|
|
|
expect(result.staged).toBe(true)
|
|
// Same escalation as the create tool: turning auto_send on converts the
|
|
// schedule into recurring outbound email, so the staged op is 'high'.
|
|
expect(result.risk_level).toBe('high')
|
|
})
|
|
|
|
it('stages a pause via the status field', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule() })
|
|
enqueue({ data: { id: 'op-recurring-3' } })
|
|
|
|
const result = (await updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, status: 'paused' },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as {
|
|
staged: boolean
|
|
operation_id?: string
|
|
preview: { current: Record<string, unknown>; proposed: Record<string, unknown> }
|
|
}
|
|
|
|
expect(result.staged).toBe(true)
|
|
expect(result.operation_id).toBe('op-recurring-3')
|
|
expect(result.preview.current.status).toBe('active')
|
|
expect(result.preview.proposed.status).toBe('paused')
|
|
expect(result.preview.current.recurring_schedule_id).toBe(SCHEDULE_ID)
|
|
})
|
|
|
|
it('previews an item replace against the current lines', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule() })
|
|
|
|
const result = (await updateTool().execute(
|
|
{
|
|
schedule_id: SCHEDULE_ID,
|
|
items: [{ description: 'Ny rad', quantity: 2, unit: 'tim', unit_price: 1200 }],
|
|
dry_run: true,
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as {
|
|
dry_run?: boolean
|
|
preview: {
|
|
current: { items: Array<{ description: string }> }
|
|
proposed: { items: Array<{ description: string }> }
|
|
}
|
|
}
|
|
|
|
expect(result.dry_run).toBe(true)
|
|
expect(result.preview.current.items.map((i) => i.description)).toEqual(['Support'])
|
|
expect(result.preview.proposed.items.map((i) => i.description)).toEqual(['Ny rad'])
|
|
})
|
|
|
|
it('verifies a new customer belongs to the company', async () => {
|
|
const otherCustomerId = '33333333-3333-4333-8333-333333333333'
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule() })
|
|
enqueue({ data: null })
|
|
|
|
await expect(
|
|
updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, customer_id: otherCustomerId, dry_run: true },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/not found/i)
|
|
expect(supabase.from).toHaveBeenNthCalledWith(2, 'customers')
|
|
})
|
|
})
|
|
|
|
describe('recurring schedule tools: dimension bags', () => {
|
|
const PROJEKT_DIM = {
|
|
id: 'dim-6',
|
|
sie_dim_no: 6,
|
|
name: 'Projekt',
|
|
resets_annually: false,
|
|
is_system: true,
|
|
is_active: true,
|
|
sort_order: 2,
|
|
}
|
|
const PROJEKT_VALUE = {
|
|
id: 'dv-1',
|
|
dimension_id: 'dim-6',
|
|
code: 'P001',
|
|
name: 'Villa Almgren',
|
|
is_active: true,
|
|
start_date: null,
|
|
end_date: null,
|
|
}
|
|
|
|
/**
|
|
* The queued mock's chain proxy discards call args by design, so capture
|
|
* .insert payloads per table with a thin wrapper around the original
|
|
* implementation. Lets the tests assert what actually lands in
|
|
* pending_operations.params.
|
|
*/
|
|
function captureInserts(supabase: ReturnType<typeof createQueuedMockSupabase>['supabase']) {
|
|
const inserted: Record<string, unknown[]> = {}
|
|
const originalFrom = supabase.from.getMockImplementation()!
|
|
supabase.from.mockImplementation((table: string) => {
|
|
const chain = originalFrom(table) as object
|
|
return new Proxy(chain, {
|
|
get(target, prop, receiver) {
|
|
if (prop === 'insert') {
|
|
return (rows: unknown) => {
|
|
;(inserted[table] ??= []).push(rows)
|
|
return (Reflect.get(target, prop, receiver) as (r: unknown) => unknown)(rows)
|
|
}
|
|
}
|
|
return Reflect.get(target, prop, receiver)
|
|
},
|
|
})
|
|
})
|
|
return inserted
|
|
}
|
|
|
|
it('create: stages schedule + item bags verbatim while dimensions are disabled (free-text passthrough)', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const inserted = captureInserts(supabase)
|
|
enqueue({ data: { dimensions_enabled: false } }) // company_settings (resolver)
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: 'billing@example.test' } })
|
|
enqueue({ data: { id: 'op-dims-1' } }) // pending_operations insert
|
|
|
|
const result = (await createTool().execute(
|
|
{
|
|
customer_id: CUSTOMER_ID,
|
|
name: 'Projekt-retainer',
|
|
day_of_month: 25,
|
|
default_dimensions: { '1': 'KS1' },
|
|
items: [
|
|
{ description: 'Support', quantity: 1, unit: 'st', unit_price: 5000, dimensions: { '6': 'P001' } },
|
|
{ description: 'Timmar', quantity: 2, unit: 'tim', unit_price: 1000 },
|
|
],
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(true)
|
|
const opRow = inserted['pending_operations'][0] as { params: Record<string, unknown> }
|
|
expect(opRow.params.default_dimensions).toEqual({ '1': 'KS1' })
|
|
const items = opRow.params.items as Array<Record<string, unknown>>
|
|
expect(items[0].dimensions).toEqual({ '6': 'P001' })
|
|
// Untagged template items stage without the key: the commit executor and
|
|
// the cron both treat a missing bag as {}.
|
|
expect(items[1]).not.toHaveProperty('dimensions')
|
|
expect(result.preview.default_dimensions).toEqual({ '1': 'KS1' })
|
|
})
|
|
|
|
it('create: resolves a value NAME to its registry code and echoes the resolution', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
const inserted = captureInserts(supabase)
|
|
enqueue({ data: { dimensions_enabled: true } }) // company_settings
|
|
enqueue({ data: null }) // ensure_company_dimensions rpc
|
|
enqueue({ data: [PROJEKT_DIM] }) // dimensions
|
|
enqueue({ data: [PROJEKT_VALUE] }) // dimension_values
|
|
enqueue({ data: { id: CUSTOMER_ID, name: 'Test Customer AB', email: 'billing@example.test' } })
|
|
enqueue({ data: { id: 'op-dims-2' } })
|
|
|
|
const result = (await createTool().execute(
|
|
{
|
|
customer_id: CUSTOMER_ID,
|
|
name: 'Villaprojektet',
|
|
day_of_month: 25,
|
|
default_dimensions: { '6': 'Villa Almgren' },
|
|
items: [{ description: 'Support', quantity: 1, unit: 'st', unit_price: 5000 }],
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as { staged: boolean; preview: Record<string, unknown> }
|
|
|
|
expect(result.staged).toBe(true)
|
|
const opRow = inserted['pending_operations'][0] as { params: Record<string, unknown> }
|
|
expect(opRow.params.default_dimensions).toEqual({ '6': 'P001' })
|
|
const resolutions = result.preview.dimension_resolutions as Array<Record<string, unknown>>
|
|
expect(resolutions.length).toBeGreaterThan(0)
|
|
})
|
|
|
|
it('create: rejects an unknown dimension value instead of auto-creating it', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: { dimensions_enabled: true } })
|
|
enqueue({ data: null })
|
|
enqueue({ data: [PROJEKT_DIM] })
|
|
enqueue({ data: [PROJEKT_VALUE] })
|
|
|
|
await expect(
|
|
createTool().execute(
|
|
{
|
|
customer_id: CUSTOMER_ID,
|
|
name: 'X',
|
|
day_of_month: 25,
|
|
default_dimensions: { '6': 'Helt Okänt Projekt' },
|
|
items: [{ description: 'S', quantity: 1, unit: 'st', unit_price: 100 }],
|
|
},
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
),
|
|
).rejects.toThrow(/matcha|Kandidater|gnubok_create_dimension_value/i)
|
|
})
|
|
|
|
it('update: stages {} as the clear-all-tags bag replace', async () => {
|
|
const { supabase, enqueue } = createQueuedMockSupabase()
|
|
enqueue({ data: currentSchedule() })
|
|
enqueue({ data: { id: 'op-dims-3' } })
|
|
|
|
const result = (await updateTool().execute(
|
|
{ schedule_id: SCHEDULE_ID, default_dimensions: {} },
|
|
'company-1',
|
|
'user-1',
|
|
supabase as never,
|
|
)) as {
|
|
staged: boolean
|
|
preview: {
|
|
current: Record<string, unknown>
|
|
changes: Record<string, unknown>
|
|
proposed: Record<string, unknown>
|
|
}
|
|
}
|
|
|
|
expect(result.staged).toBe(true)
|
|
expect(result.preview.changes.default_dimensions).toEqual({})
|
|
expect(result.preview.proposed.default_dimensions).toEqual({})
|
|
})
|
|
})
|