feat(api): add /modules/hashes endpoint for drift detection

This commit is contained in:
DumiFlex
2026-04-28 13:55:09 +03:00
parent d1d8d025a9
commit 27f3cc9890
2 changed files with 40 additions and 0 deletions
+26
View File
@@ -88,3 +88,29 @@ async def test_embed_bundle_rejects_non_list_uuids(aiohttp_client, app_with_db):
"uuids": "not-a-list",
})
assert resp.status == 400
async def test_hashes_endpoint_returns_uuid_hash_map_for_wildcards_only(
aiohttp_client, app_with_db,
):
"""Drift-detection primitive. Returns {hashes: {uuid: hash}} keyed
by 8hex uuid. Wildcards-only because catalog only contains wildcards
(spec §2.7) — no point shipping hashes for kinds that never drift
embedded snapshots."""
app, conn = app_with_db
repo = ModuleRepository(conn)
wc = repo.create(
type="wildcard", name="x", description="",
category_id=None, tags=[], payload={"options": []},
)
cb = repo.create(
type="combine", name="y", description="",
category_id=None, tags=[], payload={"template": "$a"},
)
client = await aiohttp_client(app)
resp = await client.get("/wp/api/modules/hashes")
assert resp.status == 200
body = await resp.json()
assert wc["uuid"] in body["hashes"]
assert cb["uuid"] not in body["hashes"] # combines excluded
assert body["hashes"][wc["uuid"]] == wc["payload_hash"]
+14
View File
@@ -162,6 +162,19 @@ async def toggle_favorite(request: web.Request) -> web.Response:
return json_ok(updated)
async def list_hashes(request: web.Request) -> web.Response:
"""Lightweight bulk hash fetch for drift detection. Spec §4.2.
Returns wildcards-only because catalog never contains other kinds
(spec §2.7). Lightweight (no payload, no metadata) so the SPA can
poll on every workflow load without measurable cost."""
with db_session(request) as conn:
rows = ModuleRepository(conn).list(type="wildcard")
return json_ok({
"hashes": {row["uuid"]: row["payload_hash"] for row in rows},
})
async def embed_bundle(request: web.Request) -> web.Response:
"""Lazy walk for SPA library picker. Spec §4.2.
@@ -220,6 +233,7 @@ async def embed_bundle(request: web.Request) -> web.Response:
def register(router) -> None:
router.add_get("/wp/api/modules", list_modules)
router.add_post("/wp/api/modules", create_module)
router.add_get("/wp/api/modules/hashes", list_hashes)
router.add_post("/wp/api/modules/match", match_module)
router.add_post("/wp/api/modules/embed-bundle", embed_bundle)
router.add_get("/wp/api/modules/{id}", get_module)