test(machine): unit-cover operator-fees parser + state-store apply
24 tests for the load-bearing logic introduced by the previous commit: `src/services/__tests__/operator-fees.test.ts` (10): - canonical v1 payload with components parses cleanly - absent schema_version treated as v1 (back-compat with cassette config doc that shipped without one) - unknown top-level keys silently ignored (v2 forward-compat) - absent `components` → WARN + zero breakdown (graceful degrade, producer-mandatory at v1 but consumer-safe) - components present but sums disagree with totals → WARN + still parses (totals authoritative per coord log §`14:25Z`) - tiny float drift (well under 1e-6) does NOT trip the consistency assert - required fields missing → throws - non-numeric component → throws with the offending key in the message - FEE_CAP_PER_DIRECTION exposed at 0.15 `electron/__tests__/state-store-fees.test.ts` (14): - null pre-apply (`getFeeConfig` + watermark) - round-trip via getFeeConfig after applyFeeConfig - upsert on subsequent newer event (singleton id=1) - watermark dedup: rejects equal AND older event.created_at - persisted row unchanged when stale event is rejected - 15% per-direction cap: rejects above-cap on either direction - accepts at the cap boundary exactly - rejects negative + non-finite fractions - schema_version < 1 rejected - non-integer event_created_at rejected - watermark does NOT advance when payload validation fails (atomicity) Uses in-memory SQLite (`:memory:`) — fresh DB per test, no on-disk artifacts, no parallel-test interference. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
20b146363f
commit
bd6270cbd6
2 changed files with 352 additions and 0 deletions
182
apps/machine/src/services/__tests__/operator-fees.test.ts
Normal file
182
apps/machine/src/services/__tests__/operator-fees.test.ts
Normal file
|
|
@ -0,0 +1,182 @@
|
|||
/**
|
||||
* Tests for the operator-fees parser (aiolabs/lamassu-next#57).
|
||||
*
|
||||
* Covers the consumer-side wire-format invariants locked in the coord
|
||||
* log §`14:25Z`:
|
||||
* - schema_version absent → treated as v1
|
||||
* - components absent → WARN + zero breakdown (graceful degrade)
|
||||
* - components present → consistency assert: sums must match totals
|
||||
* within 1e-6; on drift WARN + still apply (sums are authoritative)
|
||||
* - 15% per-direction cap enforced
|
||||
* - Unknown top-level keys ignored (v2 forward-compat)
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { __testing } from '../operator-fees.js'
|
||||
|
||||
const { parseV1Payload, FEE_CAP_PER_DIRECTION } = __testing
|
||||
|
||||
describe('parseV1Payload', () => {
|
||||
let warnSpy: ReturnType<typeof vi.spyOn>
|
||||
|
||||
beforeEach(() => {
|
||||
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
|
||||
})
|
||||
afterEach(() => {
|
||||
warnSpy.mockRestore()
|
||||
})
|
||||
|
||||
it('parses a canonical v1 payload with components', () => {
|
||||
const parsed = parseV1Payload({
|
||||
schema_version: 1,
|
||||
cash_in_fee_fraction: 0.0633,
|
||||
cash_out_fee_fraction: 0.1077,
|
||||
components: {
|
||||
super_cash_in: 0.03,
|
||||
super_cash_out: 0.03,
|
||||
operator_cash_in: 0.0333,
|
||||
operator_cash_out: 0.0777,
|
||||
},
|
||||
})
|
||||
|
||||
expect(parsed).toEqual({
|
||||
cashInFeeFraction: 0.0633,
|
||||
cashOutFeeFraction: 0.1077,
|
||||
superCashInFraction: 0.03,
|
||||
superCashOutFraction: 0.03,
|
||||
operatorCashInFraction: 0.0333,
|
||||
operatorCashOutFraction: 0.0777,
|
||||
schemaVersion: 1,
|
||||
})
|
||||
expect(warnSpy).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('treats absent schema_version as v1 (back-compat with cassette config)', () => {
|
||||
const parsed = parseV1Payload({
|
||||
cash_in_fee_fraction: 0.05,
|
||||
cash_out_fee_fraction: 0.05,
|
||||
components: {
|
||||
super_cash_in: 0.025,
|
||||
super_cash_out: 0.025,
|
||||
operator_cash_in: 0.025,
|
||||
operator_cash_out: 0.025,
|
||||
},
|
||||
})
|
||||
expect(parsed.schemaVersion).toBe(1)
|
||||
})
|
||||
|
||||
it('ignores unknown top-level keys (v2 forward-compat)', () => {
|
||||
// Simulated v2 payload — adds a `discounts` field. v1 consumer
|
||||
// should parse the known fields and silently drop the rest.
|
||||
const parsed = parseV1Payload({
|
||||
schema_version: 2,
|
||||
cash_in_fee_fraction: 0.05,
|
||||
cash_out_fee_fraction: 0.05,
|
||||
components: {
|
||||
super_cash_in: 0.025,
|
||||
super_cash_out: 0.025,
|
||||
operator_cash_in: 0.025,
|
||||
operator_cash_out: 0.025,
|
||||
},
|
||||
discounts: [{ npub: 'abc', applies_to: 'super', fraction: 0.01 }],
|
||||
future_field: { whatever: true },
|
||||
})
|
||||
expect(parsed.cashInFeeFraction).toBe(0.05)
|
||||
expect(parsed.schemaVersion).toBe(2)
|
||||
// No throws — extra fields silently ignored.
|
||||
})
|
||||
|
||||
it('falls through to zero components + WARN when components absent', () => {
|
||||
const parsed = parseV1Payload({
|
||||
schema_version: 1,
|
||||
cash_in_fee_fraction: 0.05,
|
||||
cash_out_fee_fraction: 0.05,
|
||||
})
|
||||
expect(parsed.superCashInFraction).toBe(0)
|
||||
expect(parsed.superCashOutFraction).toBe(0)
|
||||
expect(parsed.operatorCashInFraction).toBe(0)
|
||||
expect(parsed.operatorCashOutFraction).toBe(0)
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('payload missing `components`')
|
||||
)
|
||||
})
|
||||
|
||||
it('WARNs + still parses when components sum disagrees with totals', () => {
|
||||
// Producer-bug shape: totals say 0.10 but components sum to 0.05.
|
||||
// Per coord log §`07:33Z`+§`14:25Z` consumer logs WARN + applies
|
||||
// sums (totals are authoritative); does NOT throw.
|
||||
const parsed = parseV1Payload({
|
||||
schema_version: 1,
|
||||
cash_in_fee_fraction: 0.1,
|
||||
cash_out_fee_fraction: 0.1,
|
||||
components: {
|
||||
super_cash_in: 0.025,
|
||||
super_cash_out: 0.025,
|
||||
operator_cash_in: 0.025,
|
||||
operator_cash_out: 0.025,
|
||||
},
|
||||
})
|
||||
|
||||
expect(parsed.cashInFeeFraction).toBe(0.1)
|
||||
expect(parsed.cashOutFeeFraction).toBe(0.1)
|
||||
expect(warnSpy).toHaveBeenCalledWith(
|
||||
expect.stringContaining('payload internally inconsistent')
|
||||
)
|
||||
})
|
||||
|
||||
it('does NOT warn on tiny float drift well under the 1e-6 threshold', () => {
|
||||
// 0.0333 + 0.0300 == 0.0633 modulo IEEE-754; this should not trip
|
||||
// the consistency assert.
|
||||
parseV1Payload({
|
||||
schema_version: 1,
|
||||
cash_in_fee_fraction: 0.0333 + 0.03,
|
||||
cash_out_fee_fraction: 0.0777 + 0.03,
|
||||
components: {
|
||||
super_cash_in: 0.03,
|
||||
super_cash_out: 0.03,
|
||||
operator_cash_in: 0.0333,
|
||||
operator_cash_out: 0.0777,
|
||||
},
|
||||
})
|
||||
// No consistency-inconsistency warnings.
|
||||
const inconsistent = warnSpy.mock.calls.find((c) =>
|
||||
String(c[0]).includes('internally inconsistent')
|
||||
)
|
||||
expect(inconsistent).toBeUndefined()
|
||||
})
|
||||
|
||||
it('throws when cash_in_fee_fraction is missing', () => {
|
||||
expect(() =>
|
||||
parseV1Payload({
|
||||
cash_out_fee_fraction: 0.05,
|
||||
})
|
||||
).toThrow(/cash_in_fee_fraction/)
|
||||
})
|
||||
|
||||
it('throws when cash_out_fee_fraction is missing', () => {
|
||||
expect(() =>
|
||||
parseV1Payload({
|
||||
cash_in_fee_fraction: 0.05,
|
||||
})
|
||||
).toThrow(/cash_out_fee_fraction/)
|
||||
})
|
||||
|
||||
it('throws when a component is non-numeric', () => {
|
||||
expect(() =>
|
||||
parseV1Payload({
|
||||
cash_in_fee_fraction: 0.05,
|
||||
cash_out_fee_fraction: 0.05,
|
||||
components: {
|
||||
super_cash_in: 'oops',
|
||||
super_cash_out: 0.025,
|
||||
operator_cash_in: 0.025,
|
||||
operator_cash_out: 0.025,
|
||||
},
|
||||
})
|
||||
).toThrow(/components\.super_cash_in/)
|
||||
})
|
||||
|
||||
it('exposes the 15% cap constant', () => {
|
||||
expect(FEE_CAP_PER_DIRECTION).toBe(0.15)
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue