Registry (Forgejo) + memory-api clients under m2_market_indexer.client, with memory-api paths/creds as env-overridable constants (config.py). reindex.py drops market:catalog and rebuilds from published listings on main, skipping schema-invalid ones. watch.py polls the registry tip commit against a cached state.json and upserts changed/new published listings, deletes delisted ones.
145 lines
4.9 KiB
Python
145 lines
4.9 KiB
Python
"""Inline httpx MockTransport test for the reindex flow (T015 verify step).
|
|
|
|
Registry fixture: one `published` + one `delisted` listing. Asserts the
|
|
resulting memory-api calls are exactly: drop partition, then upsert the
|
|
published listing only (the delisted one is silently omitted by a
|
|
drop-then-upsert-published rebuild — contracts/catalog-index.md "reindex").
|
|
|
|
Run directly: `python indexer/tests/test_reindex_flow.py`
|
|
Or via pytest: `pytest indexer/tests/test_reindex_flow.py`
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import base64
|
|
import json
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import httpx
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "src"))
|
|
|
|
from jsonschema import Draft202012Validator # noqa: E402
|
|
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"
|
|
|
|
LISTING_PUBLISHED = {
|
|
"schema_version": "m2.listing.v1",
|
|
"listing_id": "lst_pdf-export",
|
|
"solution_id": "sol_pdf-export",
|
|
"solution_version": "1.2.0",
|
|
"inventory_type": "solution",
|
|
"name": "PDF Export",
|
|
"summary": "Branded PDF report generation",
|
|
"category": "reporting",
|
|
"keywords": ["pdf", "report", "branding"],
|
|
"price": {"amount": 50, "currency": "m2cr", "model": "one_time"},
|
|
"seller": "op_nasr",
|
|
"evidence_summary": "12 installs, 4.6 avg rating",
|
|
"tenant_visibility": ["*"],
|
|
"stats": {"installs": 12, "proposals_shown": 20, "proposals_accepted": 12},
|
|
"status": "published",
|
|
"install_ref": "lst_pdf-export-v1.2.0",
|
|
}
|
|
|
|
LISTING_DELISTED = {
|
|
**LISTING_PUBLISHED,
|
|
"listing_id": "lst_old-tool",
|
|
"name": "Old Tool",
|
|
"status": "delisted",
|
|
"install_ref": "lst_old-tool-v0.9.0",
|
|
}
|
|
|
|
SOLUTION_PUBLISHED = {"intent": "generate a branded PDF report from a data export"}
|
|
|
|
|
|
def _b64_contents(payload: dict) -> dict:
|
|
encoded = base64.b64encode(json.dumps(payload).encode("utf-8")).decode("ascii")
|
|
return {"content": encoded}
|
|
|
|
|
|
def _make_handlers():
|
|
calls: list[tuple[str, str]] = []
|
|
|
|
def registry_handler(request: httpx.Request) -> httpx.Response:
|
|
calls.append(("registry", f"{request.method} {request.url.path}"))
|
|
path = request.url.path
|
|
|
|
if path == "/api/v1/repos/m2/market-registry/contents/listings":
|
|
return httpx.Response(
|
|
200,
|
|
json=[
|
|
{"name": "lst_pdf-export", "type": "dir"},
|
|
{"name": "lst_old-tool", "type": "dir"},
|
|
],
|
|
)
|
|
listings_base = "/api/v1/repos/m2/market-registry/contents/listings"
|
|
if path == f"{listings_base}/lst_pdf-export/listing.json":
|
|
return httpx.Response(200, json=_b64_contents(LISTING_PUBLISHED))
|
|
if path == f"{listings_base}/lst_pdf-export/solution.json":
|
|
return httpx.Response(200, json=_b64_contents(SOLUTION_PUBLISHED))
|
|
if path == f"{listings_base}/lst_old-tool/listing.json":
|
|
return httpx.Response(200, json=_b64_contents(LISTING_DELISTED))
|
|
return httpx.Response(404, json={"message": "not found"})
|
|
|
|
def memory_handler(request: httpx.Request) -> httpx.Response:
|
|
calls.append(("memory", f"{request.method} {request.url.path}"))
|
|
return httpx.Response(200, json={"ok": True})
|
|
|
|
return calls, registry_handler, memory_handler
|
|
|
|
|
|
def _exercise_reindex_flow():
|
|
calls, registry_handler, memory_handler = _make_handlers()
|
|
|
|
registry = RegistryClient(
|
|
"https://git.machinemachine.ai",
|
|
"frg_test",
|
|
"m2/market-registry",
|
|
"main",
|
|
transport=httpx.MockTransport(registry_handler),
|
|
)
|
|
memory = MemoryClient(
|
|
"https://memory.machinemachine.ai",
|
|
"mem_test",
|
|
"market:catalog",
|
|
partition_path="/partitions/{partition}",
|
|
document_path="/partitions/{partition}/documents/{doc_id}",
|
|
transport=httpx.MockTransport(memory_handler),
|
|
)
|
|
with SCHEMA_PATH.open() as f:
|
|
validator = Draft202012Validator(json.load(f))
|
|
|
|
counts = run(registry, memory, validator)
|
|
|
|
assert counts == {
|
|
"scanned": 2,
|
|
"invalid": 0,
|
|
"not_published": 1,
|
|
"upserted": 1,
|
|
}, counts
|
|
|
|
memory_calls = [c for kind, c in calls if kind == "memory"]
|
|
assert memory_calls == [
|
|
"DELETE /partitions/market:catalog",
|
|
"PUT /partitions/market:catalog/documents/lst_pdf-export",
|
|
], memory_calls
|
|
|
|
registry_calls = [c for kind, c in calls if kind == "registry"]
|
|
assert any("lst_pdf-export/solution.json" in c for c in registry_calls)
|
|
assert not any("lst_old-tool/solution.json" in c for c in registry_calls)
|
|
|
|
return counts, memory_calls
|
|
|
|
|
|
def test_reindex_flow_upserts_published_only():
|
|
_exercise_reindex_flow()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
counts, memory_calls = _exercise_reindex_flow()
|
|
print("OK: reindex counts ->", counts)
|
|
print("OK: memory-api calls ->", memory_calls)
|