wedge: T013 property tests + T014 Dockerfile merged; fix zero-amount install rows

T013 worker found record_install violating CHECK(amount>0) when platform_pct
rounds cut or seller_amount to zero (unhandled IntegrityError -> HTTP 500).
Fixed: skip zero rows, grant references the paying tx; regression test added.
9 ledger tests green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
m2 (AI Agent) 2026-07-02 03:17:07 +02:00
parent bc650ea4ee
commit aeba02f287
6 changed files with 37 additions and 5 deletions

View file

@ -121,8 +121,17 @@ def record_install(
cut = int(amount * platform_pct) cut = int(amount * platform_pct)
seller_amount = amount - cut seller_amount = amount - cut
# The tx table CHECKs amount > 0: a pct that rounds either side to zero
# (0.0 -> cut == 0, 1.0 -> seller_amount == 0) must skip that row, not
# violate the constraint. amount >= 1 guarantees at least one row.
tx_ids: list[int] = []
net_tx_id = None
if seller_amount > 0:
net_tx_id = _insert_tx(conn, ts, buyer, seller, seller_amount, "install", ref) net_tx_id = _insert_tx(conn, ts, buyer, seller, seller_amount, "install", ref)
cut_tx_id = _insert_tx(conn, ts, buyer, "platform", cut, "install", ref) tx_ids.append(net_tx_id)
if cut > 0:
tx_ids.append(_insert_tx(conn, ts, buyer, "platform", cut, "install", ref))
paying_tx_id = net_tx_id if net_tx_id is not None else tx_ids[0]
grant_id = f"gr_{uuid.uuid4().hex}" grant_id = f"gr_{uuid.uuid4().hex}"
conn.execute( conn.execute(
@ -131,14 +140,14 @@ def record_install(
solution_version_major, tx_id, ts) solution_version_major, tx_id, ts)
VALUES (?, ?, ?, ?, ?, ?) VALUES (?, ?, ?, ?, ?, ?)
""", """,
(grant_id, buyer, ref, 1, net_tx_id, ts), (grant_id, buyer, ref, 1, paying_tx_id, ts),
) )
grant_row = conn.execute( grant_row = conn.execute(
"SELECT * FROM grant_ WHERE grant_id = ?", (grant_id,) "SELECT * FROM grant_ WHERE grant_id = ?", (grant_id,)
).fetchone() ).fetchone()
return {"tx_ids": [net_tx_id, cut_tx_id], "grant": dict(grant_row)} return {"tx_ids": tx_ids, "grant": dict(grant_row)}
def record_refund(conn: sqlite3.Connection, ref: str) -> dict: def record_refund(conn: sqlite3.Connection, ref: str) -> dict:

View file

@ -246,3 +246,26 @@ def test_api_install_402_leaves_tx_log_unchanged(client, service_headers, admin_
after = client.get("/tx", headers=service_headers).json()["transactions"] after = client.get("/tx", headers=service_headers).json()["transactions"]
assert after == before assert after == before
def test_edge_platform_pct_zero_and_one(tmp_path):
"""Regression: pct that rounds a row to zero must skip it, not 500.
(T013 worker finding: CHECK amount > 0 was violated by cut == 0 or
seller_amount == 0; fixed in models.record_install.)
"""
from m2_ledger import models as M
conn = M.init_db(str(tmp_path / "edge.db"))
M.record_grant(conn, "buyer", 100)
r0 = M.record_install(conn, "buyer", "seller", 10, 0.0, "ref:pct0")
assert M.balance(conn, "seller") == 10
assert M.balance(conn, "platform") == 0
assert len(r0["tx_ids"]) == 1
r1 = M.record_install(conn, "buyer", "seller", 10, 1.0, "ref:pct1")
assert M.balance(conn, "platform") == 10
assert len(r1["tx_ids"]) == 1
assert r1["grant"]["tx_id"] == r1["tx_ids"][0]
assert M.balance(conn, "buyer") == 80

View file

@ -51,7 +51,7 @@ published listing
- [x] T010 [P] [US1] Implement tx + grant models and derived-balance queries in `ledger/src/m2_ledger/models.py` (SQLite WAL, append-only invariants, data-model.md Transaction/LicenseGrant) - [x] T010 [P] [US1] Implement tx + grant models and derived-balance queries in `ledger/src/m2_ledger/models.py` (SQLite WAL, append-only invariants, data-model.md Transaction/LicenseGrant)
- [x] T011 [P] [US1] Implement X-API-Key auth (service/admin key classes) in `ledger/src/m2_ledger/auth.py` - [x] T011 [P] [US1] Implement X-API-Key auth (service/admin key classes) in `ledger/src/m2_ledger/auth.py`
- [x] T012 [US1] Implement API routes `/health`, `/balance/{op}`, `/tx/install`, `/tx/refund`, `/tx`, `/license/{op}/{listing}` in `ledger/src/m2_ledger/api.py` (402/409 semantics, all-or-nothing install tx, idempotent by ref) - [x] T012 [US1] Implement API routes `/health`, `/balance/{op}`, `/tx/install`, `/tx/refund`, `/tx`, `/license/{op}/{listing}` in `ledger/src/m2_ledger/api.py` (402/409 semantics, all-or-nothing install tx, idempotent by ref)
- [ ] T013 [US1] Ledger unit + property tests in `ledger/tests/test_properties.py`: balance==fold(log) under generated sequences; no grant without tx; install atomicity; duplicate-ref 409 - [x] T013 [US1] Ledger unit + property tests in `ledger/tests/test_properties.py`: balance==fold(log) under generated sequences; no grant without tx; install atomicity; duplicate-ref 409
- [ ] T014 [US1] Dockerfile + Coolify deployment for m2-ledger on the `coolify` network in `ledger/Dockerfile` + `ledger/README.md` deploy notes; verify `GET /health` from the network - [ ] T014 [US1] Dockerfile + Coolify deployment for m2-ledger on the `coolify` network in `ledger/Dockerfile` + `ledger/README.md` deploy notes; verify `GET /health` from the network
- [x] T015 [US1] Create `market:catalog` partition and implement indexer `indexer/src/m2_market_indexer/reindex.py` + `watch.py` per contracts/catalog-index.md (registry → memory-api upsert, tenant_visibility payload, delist removal) - [x] T015 [US1] Create `market:catalog` partition and implement indexer `indexer/src/m2_market_indexer/reindex.py` + `watch.py` per contracts/catalog-index.md (registry → memory-api upsert, tenant_visibility payload, delist removal)