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
170
apps/machine/electron/__tests__/state-store-fees.test.ts
Normal file
170
apps/machine/electron/__tests__/state-store-fees.test.ts
Normal file
|
|
@ -0,0 +1,170 @@
|
|||
/**
|
||||
* Tests for fee-config persistence in state-store (aiolabs/lamassu-next#57).
|
||||
*
|
||||
* Validates the v9→v10 migration shape, the 15% cap enforced at the IPC
|
||||
* boundary (defense in depth — renderer is the primary check), watermark
|
||||
* dedup, and atomic apply semantics.
|
||||
*
|
||||
* Uses an in-memory SQLite database — fresh per test, no on-disk
|
||||
* artifacts, no parallel-test interference.
|
||||
*/
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
applyFeeConfig,
|
||||
closeDatabase,
|
||||
getFeeConfig,
|
||||
getLastKnownFeeConfigCreatedAt,
|
||||
initDatabase,
|
||||
} from '../state-store.js'
|
||||
|
||||
const VALID_PAYLOAD = {
|
||||
cashInFeeFraction: 0.0633,
|
||||
cashOutFeeFraction: 0.1077,
|
||||
schemaVersion: 1,
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
initDatabase(':memory:')
|
||||
})
|
||||
afterEach(() => {
|
||||
closeDatabase()
|
||||
})
|
||||
|
||||
describe('state-store: fee_config', () => {
|
||||
it('returns null before any event has been applied', () => {
|
||||
expect(getFeeConfig()).toBeNull()
|
||||
expect(getLastKnownFeeConfigCreatedAt()).toBe(0)
|
||||
})
|
||||
|
||||
it('applies a valid event and round-trips via getFeeConfig', () => {
|
||||
const result = applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
expect(result).toEqual({ applied: true })
|
||||
|
||||
const row = getFeeConfig()
|
||||
expect(row).not.toBeNull()
|
||||
expect(row!.cashInFeeFraction).toBe(0.0633)
|
||||
expect(row!.cashOutFeeFraction).toBe(0.1077)
|
||||
expect(row!.schemaVersion).toBe(1)
|
||||
expect(row!.eventCreatedAt).toBe(1_780_256_000)
|
||||
expect(row!.appliedAt).toBeGreaterThan(0)
|
||||
|
||||
expect(getLastKnownFeeConfigCreatedAt()).toBe(1_780_256_000)
|
||||
})
|
||||
|
||||
it('upserts subsequent newer events (replaces the singleton row)', () => {
|
||||
applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.05, cashOutFeeFraction: 0.08, schemaVersion: 1 },
|
||||
1_780_256_100
|
||||
)
|
||||
expect(result).toEqual({ applied: true })
|
||||
|
||||
const row = getFeeConfig()!
|
||||
expect(row.cashInFeeFraction).toBe(0.05)
|
||||
expect(row.cashOutFeeFraction).toBe(0.08)
|
||||
expect(row.eventCreatedAt).toBe(1_780_256_100)
|
||||
})
|
||||
|
||||
describe('watermark dedup', () => {
|
||||
it('rejects an event whose created_at equals the watermark', () => {
|
||||
applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
const result = applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
expect(result.applied).toBe(false)
|
||||
if (result.applied === false) {
|
||||
expect(result.reason).toMatch(/event\.created_at .* <= lastKnownFeeConfigCreatedAt/)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects an event whose created_at is older than the watermark', () => {
|
||||
applyFeeConfig(VALID_PAYLOAD, 1_780_256_500)
|
||||
const result = applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
|
||||
it('leaves the persisted row unchanged when a stale event is rejected', () => {
|
||||
applyFeeConfig(VALID_PAYLOAD, 1_780_256_500)
|
||||
const before = getFeeConfig()!
|
||||
applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.01, cashOutFeeFraction: 0.02, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
const after = getFeeConfig()!
|
||||
expect(after.cashInFeeFraction).toBe(before.cashInFeeFraction)
|
||||
expect(after.cashOutFeeFraction).toBe(before.cashOutFeeFraction)
|
||||
expect(after.eventCreatedAt).toBe(before.eventCreatedAt)
|
||||
})
|
||||
})
|
||||
|
||||
describe('per-direction 15% cap', () => {
|
||||
it('rejects when cash_in_fee_fraction exceeds 0.15', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.151, cashOutFeeFraction: 0.05, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(false)
|
||||
if (result.applied === false) {
|
||||
expect(result.reason).toMatch(/cash_in_fee_fraction out of range/)
|
||||
}
|
||||
})
|
||||
|
||||
it('rejects when cash_out_fee_fraction exceeds 0.15', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.05, cashOutFeeFraction: 0.16, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
|
||||
it('accepts when both fractions are exactly at the 15% cap', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.15, cashOutFeeFraction: 0.15, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(true)
|
||||
})
|
||||
|
||||
it('rejects negative fractions', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: -0.01, cashOutFeeFraction: 0.05, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-finite fractions', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: NaN, cashOutFeeFraction: 0.05, schemaVersion: 1 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('schema validation', () => {
|
||||
it('rejects schema_version less than 1', () => {
|
||||
const result = applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.05, cashOutFeeFraction: 0.05, schemaVersion: 0 },
|
||||
1_780_256_000
|
||||
)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
|
||||
it('rejects non-integer event_created_at', () => {
|
||||
const result = applyFeeConfig(VALID_PAYLOAD, 1_780_256_000.5)
|
||||
expect(result.applied).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('atomicity', () => {
|
||||
it('does not advance watermark when payload validation fails', () => {
|
||||
applyFeeConfig(VALID_PAYLOAD, 1_780_256_000)
|
||||
applyFeeConfig(
|
||||
{ cashInFeeFraction: 0.99, cashOutFeeFraction: 0.05, schemaVersion: 1 },
|
||||
1_780_256_500
|
||||
)
|
||||
// Cap rejection — watermark should still be at the first apply.
|
||||
expect(getLastKnownFeeConfigCreatedAt()).toBe(1_780_256_000)
|
||||
})
|
||||
})
|
||||
})
|
||||
Loading…
Add table
Add a link
Reference in a new issue