From aeba02f287c84dd2071c516519953b240aaf3596 Mon Sep 17 00:00:00 2001 From: "m2 (AI Agent)" Date: Thu, 2 Jul 2026 03:17:07 +0200 Subject: [PATCH] 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 --- .../__pycache__/models.cpython-312.pyc | Bin 12726 -> 12902 bytes ledger/src/m2_ledger/models.py | 17 ++++++++++--- .../conftest.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 2357 bytes ...st_properties.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 31266 bytes ledger/tests/test_properties.py | 23 ++++++++++++++++++ specs/001-market-first-wedge/tasks.md | 2 +- 6 files changed, 37 insertions(+), 5 deletions(-) create mode 100644 ledger/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc create mode 100644 ledger/tests/__pycache__/test_properties.cpython-312-pytest-9.1.1.pyc diff --git a/ledger/src/m2_ledger/__pycache__/models.cpython-312.pyc b/ledger/src/m2_ledger/__pycache__/models.cpython-312.pyc index 48525db42d5b85492a66f5d5e6c91236e1aa034f..78ebe84fcec8eec69c79eb710533f6e889a9f4b1 100644 GIT binary patch delta 889 zcmZuvUr19?7{6!Rz1!};Io;eJXHK2D`G>HCZT%rK7YL1{bZHPGUb9VEGG}dd$LMVp zq;YysDln25h%vcPhA->f8X!>zTfwq!+kpR zN#I{|98IA0am5!B84h9=mSF3?5NrQpZm9mXQzpvMI|LoN~D1!_UDJ0gxMIgx{h zDjQwA0Py}7P14b-UEL(VJB`4@sY;{P6jSySSAa_=Emj@`WCUwUnpg3Wd&_`I64rG4WLAJd@vpikH9&L zjntu-9cC>K+Kna`!V@@)B(|Av3Ywaz;73_q>E>E5C4H_i2=_opW_p=^CNo;%&UW(};f zXY-EMoWpx3zI-`5_&lCs;R|aUHJ0hfv2OU?+IYYZoTO3i0feb~KAP@&lw4)pIhFfc z?;oB(!5i;DblBt0#!9i@(dzprK@{93J`#jeIMC}h$U&$KBK6K97DK3 z4acH?9xik#7>?8;D$Xu9_fQ$ny-OVaI~crEW+8A$9^yHr6wVT1h-wCgERo6UMWm#%#9#sldN#vcrdpoK@nYiK zTs1sEt5{RGYq%yiim5X5)bLDRDWbcXIN+e;X)bp-pgs@Y1 zC!bdk<&>;oj$|lj)a2VNE^fuRIZQ%?k&$!O_CCfe3MU0+6dg@%u6kaFR6&n zOu5CGTv{?&UP^|A4`|F}2PqXUA0Q(Zh>Jrf=Shh(%1>^S>eGE9A~&OCLC^}(4QxAj z4^&rPF3h#V`GDjJ=gT4i9h{RJqz&0JHwazeF`2wwTHV}Qo>laN6C;C! z;sVVJVmez&c6e^CzsT)=fyMp1Gb2cn0Z9BR<^kH;?X2e_$b3YQ)kSKvpo}~-YY3noUOgB0~y!+`=THZM|~!DMlZEwP{=H815BTWUoKh-S&k&&(@Q1ga?V2N5nn zLX)#d8N^lv5r!ZlVsgHU596lIdsJRBGP+D&re?`_cJl+Zb&QPullwI+8E|iDbh{*|Naxn-?eBcH%c^J5OKk$N?d<<+{pZUSe Kw9PJhN-O}?53ZsB diff --git a/ledger/src/m2_ledger/models.py b/ledger/src/m2_ledger/models.py index 383d59b..d6b1079 100644 --- a/ledger/src/m2_ledger/models.py +++ b/ledger/src/m2_ledger/models.py @@ -121,8 +121,17 @@ def record_install( cut = int(amount * platform_pct) seller_amount = amount - cut - 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) + # 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) + 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}" conn.execute( @@ -131,14 +140,14 @@ def record_install( solution_version_major, tx_id, ts) VALUES (?, ?, ?, ?, ?, ?) """, - (grant_id, buyer, ref, 1, net_tx_id, ts), + (grant_id, buyer, ref, 1, paying_tx_id, ts), ) grant_row = conn.execute( "SELECT * FROM grant_ WHERE grant_id = ?", (grant_id,) ).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: diff --git a/ledger/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc b/ledger/tests/__pycache__/conftest.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..60d4cfe59d4a76a59b757c9b48d52a8f3b7c1721 GIT binary patch literal 2357 zcmcgt-)|H}9G}^p`~3s@gO-Yv^;#e&UN6-6ix{Dl3boJ>N(|h~X4&1^ZFld^*=@@; zC8=eRoNXE(m{jph*K-dk zmRqtl56s*TZObziy;LkJ2b8m*9kwmA=neeJk1(FaEZ!7mB}TMaZ;FMTl405|Xl1Qj zUf9l8&ABF>vve~zV^$W@LZTUtRph*moA{tG(0$N1=3p2@7~Wev_A=UAD#XO)UB|5!zNb|61ha?HFl2>+a;(HYar`>W?U z4sIS?COI`wt}w#=X&O9J(A|SD{DOX8LXXdWzIdbgy z$Qkw6QT6oD*%J%xe$uI0*$UE{b<}MpM$WuHIy|C|jhvrgZFS+$u~VbtLX?fOuxol| zagN3S9F}OYXR(GI<*{cewO;Eg)V9kKx~5N=s&1PaWeHVOz0gpeWt68tR;Ez4u&h%m zf)5muKE)EeB*2Z|H!e3-6ovBev#>9uyMXD0?tTY@sFHQEZIyH_ldfv_{#zZ_&sE<% zxtu$BFMDc*jIT9yuiTnhA%|K7FW(wnA+J9mQQv!&WR^*0h4c#vFRN9eEE8pgWFEvi zz8L;=PYvPF3tx_{rF-s1d!EX`Jle|nKFK_dfT3{x)7=B#eEDeC@BqHOGc=r$Z|@!o z0dpr48a@!Yvp)i~4knGpi;1JK!Lt+IEbKK@7M?&i;LeJj%MG0#&5fCrET$Zo8)$Lt&+Wzf82lBo$dw)*mMU>w3iQj1%Ka^ zAMs$<3@G+NZ`rk{n&_*NKH-z?Vi8p}O1i4D=!IF!_N=1Il1IuFwZZkM<%+m3H5bmr zui61>&Qo})Sgavpkw6v)XOT;ge_@|w?US18@j~E=dOC4*SUVlDaNQmht6QuLGFk9wa-GVb1xk+i+WsamojkKw+$9H`pPh8*L4 UbvaPAXp6jG$e&0U_Y3g<1{)>qi~s-t literal 0 HcmV?d00001 diff --git a/ledger/tests/__pycache__/test_properties.cpython-312-pytest-9.1.1.pyc b/ledger/tests/__pycache__/test_properties.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8bc7dee7cfc1c94cdc9da1e4a8fc850ace74aedf GIT binary patch literal 31266 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 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 diff --git a/specs/001-market-first-wedge/tasks.md b/specs/001-market-first-wedge/tasks.md index 88b32e5..e88022e 100644 --- a/specs/001-market-first-wedge/tasks.md +++ b/specs/001-market-first-wedge/tasks.md @@ -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] 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) -- [ ] 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 - [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)