spirekeeper/tests
Padreug 27449e1d11 fix(cassettes): order state events by created_at, not by one remembered id
The gate on the ATM-state consumer compared the incoming event id against
the id stored on a single arbitrary row (SELECT ... LIMIT 1, no ORDER BY).
That is a one-event memory, not a watermark: a re-delivered A, B, A applied
three times. Worse, created_at was parsed, written to state_at and then
never compared, so an event arriving late overwrote newer state — nothing
in the path ever looked at the clock.

Events are now applied only when strictly newer than the OLDEST state stamp
on file. Strict '>' subsumes replay dedup, since a replay carries the same
stamp. Oldest rather than newest is deliberate: every execute in this data
layer commits on its own, so a multi-row apply cannot be made atomic here,
and gating on the oldest means a crash mid-apply is re-applied on the next
event instead of being mistaken for a complete one. The ATM republishes on
a heartbeat, so it converges.

Stamps are compared as unix floats because SQLite returns integers,
Postgres returns timestamps and the incoming value is tz-aware; comparing
raw would either raise or quietly mislead. An unparseable incoming stamp
fails closed.

Also renames apply_bootstrap_state to apply_reported_state and corrects the
module comments. There has never been a once-per-machine guard, so calling
it a one-shot bootstrap consumer described something the code did not do.

Refs #43

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-09-22 22:23:19 +02:00
..
__init__.py feat: code quality 2024-08-13 08:20:45 +02:00
conftest.py refactor: rename extension identity to spirekeeper 2026-06-13 22:30:05 +02:00
test_calculations.py refactor(v2): canonical sat-amount vocabulary + delete Lamassu-era reverse-derivation 2026-05-26 20:08:30 +02:00
test_cassette_configs.py fix(cassettes): order state events by created_at, not by one remembered id 2026-09-22 22:23:19 +02:00
test_cassette_state_consumer.py fix(cassettes): order state events by created_at, not by one remembered id 2026-09-22 22:23:19 +02:00
test_collision_guard.py refactor: rename extension identity to spirekeeper 2026-06-13 22:30:05 +02:00
test_deposit_currency.py feat(v2): lock deposit currency to machine.fiat_code (closes #26) 2026-05-16 18:03:34 +02:00
test_fee_cap_validation.py feat(v2): CRUD + per-direction fee cap validation (#38 2/5) 2026-06-01 10:42:03 +02:00
test_fee_mismatch_recording.py refactor: rename extension identity to spirekeeper 2026-06-13 22:30:05 +02:00
test_fee_publish_triggers.py feat(v2): wire fee-config publish into machine + super-config triggers (#39 3/3) 2026-06-01 20:07:56 +02:00
test_fee_transport.py feat(v2): fee_transport — kind-30078 publisher for operator fee config (#39 2/3) 2026-06-01 20:00:29 +02:00
test_machine_config_transport.py feat(transport): get_machine_config RPC — server-delivered machine config (#41) 2026-07-02 21:54:40 +02:00
test_nip44_v2.py chore(v2): lint pass — black + ruff auto-fix + mypy regressions (#29 v1.1) 2026-05-31 15:50:14 +02:00
test_nostr_attribution.py refactor(v2): canonical sat-amount vocabulary + delete Lamassu-era reverse-derivation 2026-05-26 20:08:30 +02:00
test_operator_split_legs.py feat(v2): principal-based fee split — fixes super under-payment (#38 3/5) 2026-06-01 11:24:09 +02:00
test_pair_endpoint.py fix(pairing): retry all bunker admin RPCs past transient timeouts (#38) 2026-07-02 15:30:55 +02:00
test_pairing.py fix(pairing): retry all bunker admin RPCs past transient timeouts (#38) 2026-07-02 15:30:55 +02:00
test_principal_based_fees.py feat(v2): principal-based fee split — fixes super under-payment (#38 3/5) 2026-06-01 11:24:09 +02:00
test_roster_resolver.py refactor: rename extension identity to spirekeeper 2026-06-13 22:30:05 +02:00
test_unpaired_machine_guards.py fix: complete the unpaired-machine sweep + regression test 2026-06-22 16:55:33 +02:00