From 83cda680d25d64d00dfba26cececf6c4f09803a8 Mon Sep 17 00:00:00 2001 From: "m2 (AI Agent)" Date: Thu, 2 Jul 2026 03:34:21 +0200 Subject: [PATCH] wedge: T026 integration tests (real ledger via uvicorn, real bundle, temp HOME) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Written inline after two headless workers died on API errors. Tests caught a real cross-slice bug: ledger grants stored the full install ref as listing_id, breaking GET /license/{op}/{listing} — fixed by parsing the listing prefix per the ref convention in contracts/ledger-api.md. Also pinned the indexer test's schema path (venv-installed package resolved outside the repo) — its earlier pass was vacuous without jsonschema installed. 27 tests green. Co-Authored-By: Claude Fable 5 --- ..._install_flow.cpython-312-pytest-9.1.1.pyc | Bin 0 -> 24752 bytes cli/tests/test_install_flow.py | 248 ++++++++++++++++++ ..._reindex_flow.cpython-312-pytest-9.1.1.pyc | Bin 9662 -> 9771 bytes indexer/tests/test_reindex_flow.py | 5 + .../__pycache__/models.cpython-312.pyc | Bin 12902 -> 12983 bytes ledger/src/m2_ledger/models.py | 5 +- .../test_schemas.cpython-312-pytest-9.1.1.pyc | Bin 12566 -> 12566 bytes specs/001-market-first-wedge/tasks.md | 2 +- 8 files changed, 258 insertions(+), 2 deletions(-) create mode 100644 cli/tests/__pycache__/test_install_flow.cpython-312-pytest-9.1.1.pyc create mode 100644 cli/tests/test_install_flow.py diff --git a/cli/tests/__pycache__/test_install_flow.cpython-312-pytest-9.1.1.pyc b/cli/tests/__pycache__/test_install_flow.cpython-312-pytest-9.1.1.pyc new file mode 100644 index 0000000000000000000000000000000000000000..ff369e07bea43d9efeb6d25db09bdd1769a25040 GIT binary patch literal 24752 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 LTE_tgo0 dict: + r = httpx.get(url + path, headers={"X-API-Key": key}) + r.raise_for_status() + return r.json() + + +def _grant(url: str, operator: str, amount: int = 100) -> None: + httpx.post( + url + "/grant/starter", + json={"operator_id": operator, "amount": amount}, + headers={"X-API-Key": ADMIN_KEY}, + ).raise_for_status() + + +def _tx_count(url: str) -> int: + return len(_ledger_get(url, "/tx")["transactions"]) + + +# --- temp bundle + fixtures --------------------------------------------------------- + + +def _build_bundle(root: Path, failing_check: bool = False) -> tuple[Path, str]: + """Real bundle tarball; returns (tar_path, content_hash).""" + src = root / "bundle-src" + (src / "payload").mkdir(parents=True) + (src / "payload" / "hello.txt").write_text("hello from m2-market test bundle\n") + check_cmd = "false" if failing_check else "test -f ~/m2-market-test/hello.txt" + (src / "recipe.yaml").write_text( + "targets:\n" + " - src: hello.txt\n" + " dest: ~/m2-market-test/hello.txt\n" + "checks:\n" + f" - cmd: {check_cmd}\n" + " expect_exit: 0\n" + "entrypoint: cat ~/m2-market-test/hello.txt\n" + ) + (src / "solution.json").write_text(json.dumps({"solution_id": "sol_test"})) + tar_path = root / "bundle.tar.gz" + with tarfile.open(tar_path, "w:gz") as tar: + tar.add(src, arcname="bundle") + digest = hashlib.sha256(tar_path.read_bytes()).hexdigest() + return tar_path, f"sha256:{digest}" + + +@pytest.fixture() +def env(tmp_path, monkeypatch, ledger_url): + """Isolated HOME/state/config + fixture catalog/registry pointed at the real ledger.""" + home = tmp_path / "home" + home.mkdir() + monkeypatch.setenv("HOME", str(home)) + monkeypatch.setenv("M2_MARKET_STATE", str(tmp_path / "state.json")) + + bundle_path, content_hash = _build_bundle(tmp_path) + + config = tmp_path / "config.toml" + config.write_text( + f'operator_id = "m2bd"\n' + f'tenant = "m2-core"\n' + f'ledger_url = "{ledger_url}"\n' + f'ledger_api_key = "{SERVICE_KEY}"\n' + f'memory_api_url = "http://catalog.invalid"\n' + f'memory_api_key = "k"\n' + f'forgejo_url = "http://registry.invalid"\n' + f'forgejo_token = "t"\n' + f'apply_adapter = "local"\n' + ) + monkeypatch.setenv("M2_MARKET_CONFIG", str(config)) + + listing = { + "listing_id": LISTING_ID, + "name": "MM PDF Report", + "summary": "branded pdf reports", + "price": {"amount": PRICE, "currency": "m2cr", "model": "fixed"}, + "seller": "sdjs-operator", + "solution_version": "1.0.0", + "install_ref": f"{LISTING_ID}-v1.0.0", + } + solution = { + "solution_id": "sol_test", + "content_hash": content_hash, + "permissions": ["write ~/m2-market-test"], + "deployment": {"entrypoint": "cat ~/m2-market-test/hello.txt"}, + } + + from m2_market import cli as cli_mod # noqa: PLC0415 + + monkeypatch.setattr(cli_mod.CatalogClient, "get", lambda self, lid: dict(listing) if lid == LISTING_ID else None) + monkeypatch.setattr(cli_mod.RegistryClient, "get_solution", lambda self, lid: dict(solution)) + monkeypatch.setattr( + cli_mod.RegistryClient, + "download_bundle", + lambda self, ref, dest: (Path(dest).mkdir(parents=True, exist_ok=True), shutil.copy(bundle_path, Path(dest) / "bundle.tar.gz"))[1] and Path(dest) / "bundle.tar.gz", + ) + + return { + "home": home, + "ledger_url": ledger_url, + "bundle_path": bundle_path, + "listing": listing, + "solution": solution, + "tmp": tmp_path, + "monkeypatch": monkeypatch, + "cli_mod": cli_mod, + } + + +def _run(args, **kwargs): + from m2_market.cli import main # noqa: PLC0415 + + return CliRunner().invoke(main, args, catch_exceptions=False, **kwargs) + + +# --- scenarios ---------------------------------------------------------------------- + + +def test_funded_install_end_to_end(env): + _grant(env["ledger_url"], "m2bd", 100) + + result = _run(["install", LISTING_ID, "--yes"]) + assert result.exit_code == 0, result.output + + # ledger rows exact: net + cut + assert _ledger_get(env["ledger_url"], "/balance/m2bd")["balance"] == 100 - PRICE + assert _ledger_get(env["ledger_url"], "/balance/sdjs-operator")["balance"] == 36 + assert _ledger_get(env["ledger_url"], "/balance/platform")["balance"] == 4 + # grant exists + lic = httpx.get( + env["ledger_url"] + f"/license/m2bd/{LISTING_ID}", + headers={"X-API-Key": SERVICE_KEY}, + ) + assert lic.status_code == 200 + # file placed into the temp HOME by the local adapter + assert (env["home"] / "m2-market-test" / "hello.txt").exists() + # state.json updated + state = json.loads(Path(os.environ["M2_MARKET_STATE"]).read_text()) + assert state["installs"][LISTING_ID]["status"] == "applied" + # entrypoint hint printed + assert "cat ~/m2-market-test/hello.txt" in result.output + + +def test_insufficient_funds_exit_3_nothing_written(env): + # no grant: wallet is empty + before = _tx_count(env["ledger_url"]) + result = _run(["install", LISTING_ID, "--yes"]) + assert result.exit_code == 3 + assert _tx_count(env["ledger_url"]) == before + assert not (env["home"] / "m2-market-test").exists() + + +def test_idempotent_rerun_no_second_debit(env): + _grant(env["ledger_url"], "m2bd", 100) + assert _run(["install", LISTING_ID, "--yes"]).exit_code == 0 + txs_after_first = _tx_count(env["ledger_url"]) + + rerun = _run(["install", LISTING_ID, "--yes"]) + assert rerun.exit_code == 0 + assert "no-op" in rerun.output + assert _tx_count(env["ledger_url"]) == txs_after_first + assert _ledger_get(env["ledger_url"], "/balance/m2bd")["balance"] == 100 - PRICE + + +def test_apply_failure_refunds_and_rolls_back(env): + _grant(env["ledger_url"], "m2bd", 100) + # swap in a bundle whose post-install check fails + bad_bundle, bad_hash = _build_bundle(env["tmp"] / "bad", failing_check=True) + env["solution"]["content_hash"] = bad_hash + env["monkeypatch"].setattr( + env["cli_mod"].RegistryClient, + "download_bundle", + lambda self, ref, dest: (Path(dest).mkdir(parents=True, exist_ok=True), shutil.copy(bad_bundle, Path(dest) / "b.tar.gz"))[1] and Path(dest) / "b.tar.gz", + ) + + before = _ledger_get(env["ledger_url"], "/balance/m2bd")["balance"] + result = _run(["install", LISTING_ID, "--yes"]) + assert result.exit_code == 4 + assert "refund" in result.output.lower() + # refund compensates exactly + assert _ledger_get(env["ledger_url"], "/balance/m2bd")["balance"] == before + refunds = _ledger_get(env["ledger_url"], "/tx?reason=refund")["transactions"] + assert refunds, "compensating refund rows expected" + # rollback removed placed files + assert not (env["home"] / "m2-market-test" / "hello.txt").exists() diff --git a/indexer/tests/__pycache__/test_reindex_flow.cpython-312-pytest-9.1.1.pyc b/indexer/tests/__pycache__/test_reindex_flow.cpython-312-pytest-9.1.1.pyc index 072a837eb18f06cc5f17db99bfc99137abef622d..46a786ca4db9a6a6a19a6a940d1232f9e5ce79e3 100644 GIT binary patch delta 1263 zcmZ{kT}&KR6vyYx3}qMgt9&f5?6S-5Qdo+7lvXJPx~#!&D-$w;2cJ(Z|JtR)m)~BmXRI)-R7eUI35K%VI%6ANnO=0FM$Iu#wn=&BP$K5L>a0 z7{Yd97$d}v>k3Bi07WtE#5g7}xu?;n3y%@Iv1f}ODeS$@mgCrmPf+i7ECV=5+7OQAnq77b8nYgiB|)$!SSWpm9G zCrG{vVpKMGK7dL2i6;xQZ+R1tmPzkf$X3zo0m#UgefP4@@WSVK8Fh7KL0q6Hbwo_F zI>Wfgm}Sf{E)i(2%N##XDDxR(+afJ@{AXZMe&L^SW!R&O%d$BzFm#2TR~e37V|y$u z()6@_xnh3B%-zVB&@x3?w!MVRPO{ncEOUo(smmR`Ab^~rsQmBDfj|e?y9!h zKWM|!!_8x~`B$OS@VZpn7ojL$Y+nhnO$frj78ceC^83(nctaiyM`1$_g-7anQ{iUJ z$x3(-M&*9^{ef3VNHsb&i$_P8Vw*v*;U(L>k+%w%udE7T$@R#@kqT$m~sfurhD*mCfzvXQT-=to45w?{d_0V^ESY%`@o(@isWb0U1s)$7j z#XE#luiS`t!A-drAMV>?b%ZfWpto1Hwen`!u3gJlZdiqDLg$?G(q`p@_^qv1*;Z?o zHVR_3B%b5!0%M-R-;{Iz!l{%ivt7AdDPJ^mIbm^jjkTIJQ7BdZdlFLraE%pLaosW} z#78vb|4I3fc&Guuebo*6??FZWmhgdF{+TER@4J+)o)7E4O`hCVZ`a+d`?@~%jnY{y KBrk!^*Yg()mmop_ delta 1164 zcmaKs&u<$=6vyZ7ZW5bhZ8uJw#6M!kc4C{B{2)LRlEyZ)(9$*~ZB&{i#d7nO)bZL5 zW2YiQkO*8MaZ3mFLaPF5g(4pEW|pV+wT-}4N@#l6%^e$DEs=!Omq0aeRmc+76GInbwUjEE(# zzaJE49-^t6y)BThGq#ZuANK~8jkGlE{TZL`uLO*Mvw_PRs0-=hK8bOO$#%2rBk0IcJl9UT18USup{-(x zXB5Y0YmMzqlTVSn4}4av^xTD<_`ByMEQ%NP1Y|{7&%*QVmfi!97xz5}Cr^-KP}ZW5@#hACC*8_OrXP-WqgrPmsg7I@~k-U&cP+oJ{mHRl>2W(jLq4EIn;cp;9cHg{#FXS|+da*GSBX2fibaZ~x-^+yz&}ouT((MWljN zI4*7m*8-Qxvai_6_@oVM4-b#g;oi_JG{vdVWw69&p>j~#_#1?OEmCL@L?m<=u8T|I zC~S#0!!zCTQ0|sEC%z0%!mRi;{K>>M5;A_LX8!03$E9tBV8iRS`&!W|VX;x;yeu9> z7CP&4wRU6pLZ@@PfQW_yo%{x=h4$9)ZPh13?vy55L}kb0;{zX3_yNtji?GXn(1PD+ zVV)Q3@m$~-Nw$WiwFY0Mkl!R^(&DT5DC~-#;?wDOq{ih zb?2)sisusV?z|=QC5a0X=OuCyP79pkWHGj@P^cCwW}(2#adA*%muzrsr)C@za##p0>JNz8?-+H3=%&x)@1ML>i4W$oKIHU-zJwplO6a6_Gu1l diff --git a/indexer/tests/test_reindex_flow.py b/indexer/tests/test_reindex_flow.py index afc9e8a..9d80172 100644 --- a/indexer/tests/test_reindex_flow.py +++ b/indexer/tests/test_reindex_flow.py @@ -13,6 +13,7 @@ from __future__ import annotations import base64 import json +import os import sys from pathlib import Path @@ -25,6 +26,10 @@ from m2_market_indexer.client import MemoryClient, RegistryClient # noqa: E402 from m2_market_indexer.reindex import run # noqa: E402 SCHEMA_PATH = Path(__file__).resolve().parents[2] / "schemas" / "listing.schema.json" +# Pin the schema for config.listing_schema_path(): when the package under test is the +# venv-installed copy (uv workspace), its parents[3]-based resolution lands outside the +# repo and loads the wrong file. The test's fixture contract is THIS repo's schema. +os.environ["LISTING_SCHEMA_PATH"] = str(SCHEMA_PATH) LISTING_PUBLISHED = { "schema_version": "m2.listing.v1", diff --git a/ledger/src/m2_ledger/__pycache__/models.cpython-312.pyc b/ledger/src/m2_ledger/__pycache__/models.cpython-312.pyc index 78ebe84fcec8eec69c79eb710533f6e889a9f4b1..caf107eb0b5130dfd0b5922e703f7e86d8aa1c1c 100644 GIT binary patch delta 748 zcmYk3Pe>F|9LMMF?u@%{cV^dg|IDtYo9n8%DyCvstyzk85WB=Z@u(jzV9>N-*?{p-g`fk zjf}JK_^V56P?_~UMf%)n zt|~?66q(QrMy`D#xof(QE>#P?;d*?Q(nb2{k3kCuQ8Ubk_d=GF3&^+Ai|IT1a^Giv zeEDj@-*gl2z+5DOv#Zh7u~$d7JxSPzG<2j;Gw-)9ATC(_&{#dQGFxzWZ#%mGjFxF|FFy<$@fbfz>tD^?_%no3u+Z4WXQ}bs{Lt8L6Ven(@b@D6>ybhr*FG%$>Qm337qJ&${0Brxfo%t aB4oK4c{nUuA|r_t@NX}p8)kp&A;&**!^b!P delta 742 zcmXw#O=uHA6vyXncGGr}-Ss1Bnr&){Nl2qjCA1R1qP7~Uhhnu_ELKYGrtOxpfytKU z5UB?}RY4d8ODz=IgHkY-J$UHJgZAJhz4);p)PsVz1wo-8I%9Q){qJx8@4eag=4Iil zuDq3HjM)0Jk)Nt0)|HU}tlDOv3W|=cAdm1h)kteGjSe&h>K4(Ga9gg>Y9xqtUq!A! zMbiEBSz}U_EAlw33I9qYR?^(7=)m`X7tkEGns+l0np4(bmv!ZdFo0qxgGw+f02)HT z_We-VGlXU#rP|%0D?k^UUP<401sx8b1cg2h_lCup8AEp@u3e62vQTzJ-59e{D*YYK zL5}9MmT>(988xjDme;ULq&Qnr`8jvA1ny&SlRc$}w5o-mjc#f?uuVTUx8t$p%<}Y$ zo|->P)kw0l589-ldlgWqWz~9mab<4Hm#vA}pV$8Shy`~_eso8NjH7*UXga!LS>o0)QbUu0l2I-sV5$QU!*X^C?f^eXlq2n!-t%9jL;#AQv zNt9WnpG%&u$Ad6LH{%ziQD$t=K;o0%!P7>`GRz4!WD%lq!sm*nSs1g7 zqDkUB`7D=1T)LPz()e_7)FS-XgkLJ*qY^%tBl7kL@0aiv$Rw519WYE!rL%B}TIo34 zr3>k5-*Gth0q=c}BU?D)df3;tPjMyocD+d6