feat(cassettes): swap the count-publish endpoint for operation endpoints
The operator can no longer write a count. POST .../cassettes/ops records one operation — refill, empty, recount, set_denomination — and publishes the machine's recent window; GET .../cassettes/ops lists them newest first with acked_at, so the dashboard can tell a delivered operation from one merely sent. POST .../cassettes/publish is gone, along with update_cassette_config and UpsertCassetteConfigData. Nothing in the operator can now set a count, which is the point: a value with one writer cannot be clobbered. Under the old endpoint a dashboard form loaded before a dispense silently discarded that dispense on publish, and neither side could detect it — addressable events order by created_at at second granularity and a relay returns OK for an event it then drops, so the losing writer is never told. The op is recorded before the publish and is deliberately not rolled back when the publish fails. It records something that physically happened; notes went into a bay whether or not a relay was reachable. The window carries recent operations rather than just the newest, so an op that missed its own publish rides out with the next one. Validation rejects an unpaired machine and a position the machine has not reported. Bay count stays hardware-determined.
This commit is contained in:
parent
3d8368bcc4
commit
2ac3e2064e
5 changed files with 89 additions and 214 deletions
|
|
@ -2,9 +2,9 @@
|
|||
Tests for the v1.1 cassette-config layer (aiolabs/satmachineadmin#29).
|
||||
|
||||
Covers the pure pieces that don't need a live DB:
|
||||
- Pydantic validator behaviour on PublishCassettesPayload + the row /
|
||||
upsert models (position key coercion, integer ranges, multiple-same-
|
||||
denomination payloads, wire-format round-trip)
|
||||
- Pydantic validator behaviour on PublishCassettesPayload + the row model
|
||||
(position key coercion, integer ranges, multiple-same-denomination
|
||||
payloads, wire-format round-trip)
|
||||
- _should_apply_state_event ordering gate (extracted from
|
||||
apply_reported_state so the decision is testable without a database
|
||||
round-trip)
|
||||
|
|
@ -29,7 +29,6 @@ from ..crud import _as_unix, _should_apply_state_event
|
|||
from ..models import (
|
||||
CassettePayloadRow,
|
||||
PublishCassettesPayload,
|
||||
UpsertCassetteConfigData,
|
||||
)
|
||||
|
||||
# =============================================================================
|
||||
|
|
@ -149,44 +148,6 @@ class TestCassettePayloadRow:
|
|||
CassettePayloadRow(denomination=20, count=-1)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# UpsertCassetteConfigData — operator-edit form
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class TestUpsertCassetteConfigData:
|
||||
"""Operator-driven row edit. Both fields optional; same int constraints
|
||||
as the wire-format row but applied independently per-edit. Position is
|
||||
NOT editable — it's the row's identity (the hardware bay number)."""
|
||||
|
||||
def test_partial_update_count_only(self):
|
||||
d = UpsertCassetteConfigData(count=80)
|
||||
assert d.count == 80
|
||||
assert d.denomination is None
|
||||
|
||||
def test_partial_update_denomination_only(self):
|
||||
"""v1.1 operational case: operator records a cartridge swap at
|
||||
refill — slot 1 was $20, dispatcher replaced with $50."""
|
||||
d = UpsertCassetteConfigData(denomination=50)
|
||||
assert d.denomination == 50
|
||||
assert d.count is None
|
||||
|
||||
def test_empty_update_is_legal(self):
|
||||
"""An empty UpsertCassetteConfigData parses fine; the CRUD short-
|
||||
circuits a no-op on empty payload (no SQL emitted)."""
|
||||
d = UpsertCassetteConfigData()
|
||||
assert d.count is None
|
||||
assert d.denomination is None
|
||||
|
||||
def test_rejects_negative_count(self):
|
||||
with pytest.raises(ValueError):
|
||||
UpsertCassetteConfigData(count=-1)
|
||||
|
||||
def test_rejects_non_positive_denomination(self):
|
||||
with pytest.raises(ValueError):
|
||||
UpsertCassetteConfigData(denomination=0)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# _should_apply_state_event — ordering gate
|
||||
# =============================================================================
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue