Files
DumiFlex c13351d21c feat(context): type-aware drift/missing + id-clash state via classifyOne
Workflow drift/missing now runs through the shared classifyOne verdict: a same-id, DIFFERENT-kind library row surfaces as a distinct 'clash' state (violet badge) instead of a misleading drift/missing dot, and Refresh is suppressed + guarded in mergeRefresh so it can never clobber an unrelated item. Hybrid content-key: the workflow path compares payload_hash (drift semantics unchanged) while the import picker keeps fingerprint; classifyOne is content-key agnostic. /wp/api/modules/hashes now returns {type, payload_hash} per id; drift-store map + AppLayout fingerprint + setLibraryHash updated to the object shape.
2026-06-06 16:39:49 +03:00

179 lines
6.3 KiB
Python

"""Endpoint tests for embed-bundle + hashes + ModuleRow shape extension.
Spec §4.2."""
from __future__ import annotations
import sqlite3
import pytest
from aiohttp import web
from engine.db.migrations import migrate
from engine.db.repositories import ModuleRepository
from wp_api import modules as modules_api
@pytest.fixture
def app_with_db(tmp_path):
conn = sqlite3.connect(":memory:")
conn.row_factory = sqlite3.Row
migrate(conn)
app = web.Application()
app["wp_db"] = conn
modules_api.register(app.router)
return app, conn
async def test_embed_bundle_returns_picks_only_no_transitive_walk(
aiohttp_client, app_with_db,
):
"""Picking outfit (which `@{}`-refs color) returns ONLY outfit's
snapshot — no transitive walk. The user explicitly asked the
picker to embed only what they picked; nested wildcards are
looked up live at graph-run time inside `WP_Context.execute`.
Workflow JSON stops being self-contained, but the picker stops
pulling in surprise modules.
"""
app, conn = app_with_db
repo = ModuleRepository(conn)
color = repo.create(
type="wildcard", name="color", description="",
category_id=None, tags=[], payload={
"options": [{"value": "red", "weight": 1}],
},
)
outfit_payload = {
"options": [{"value": f"@{{{color['id']}}} dress", "weight": 1}],
}
outfit = repo.create(
type="wildcard", name="outfit", description="",
category_id=None, tags=[], payload=outfit_payload,
)
# ModuleRepository.create backfills 8-hex option ids; compare against
# the persisted payload (which carries the assigned ids) rather than
# the input dict.
expected_payload = outfit["payload"]
client = await aiohttp_client(app)
resp = await client.post("/wp/api/modules/embed-bundle", json={
"uuids": [outfit["id"]],
})
assert resp.status == 200
body = await resp.json()
assert body["pickOrder"] == [outfit["id"]]
assert len(body["modules"]) == 1
assert body["modules"][0] == expected_payload
# color is referenced by outfit but NOT embedded — runtime resolves
# it live from the library.
assert set(body["snapshots"].keys()) == {outfit["id"]}
assert color["id"] not in body["snapshots"]
assert body["walkOverflow"] == []
async def test_embed_bundle_walk_overflow_always_empty(
aiohttp_client, app_with_db,
):
"""Missing-target overflow used to surface here when the walker
couldn't resolve a `@{deadbeef}` ref. With the walker gone the
field is always `[]` — graph-side runtime is now responsible for
surfacing unresolved refs as resolver warnings."""
app, conn = app_with_db
repo = ModuleRepository(conn)
a = repo.create(
type="wildcard", name="a", description="",
category_id=None, tags=[], payload={
"options": [{"value": "@{deadbeef}", "weight": 1}],
},
)
client = await aiohttp_client(app)
resp = await client.post("/wp/api/modules/embed-bundle", json={
"uuids": [a["id"]],
})
assert resp.status == 200
body = await resp.json()
assert body["walkOverflow"] == []
# The pick itself is still returned.
assert a["id"] in body["snapshots"]
async def test_embed_bundle_rejects_non_list_uuids(aiohttp_client, app_with_db):
app, _ = app_with_db
client = await aiohttp_client(app)
resp = await client.post("/wp/api/modules/embed-bundle", json={
"uuids": "not-a-list",
})
assert resp.status == 400
async def test_hashes_endpoint_returns_type_and_payload_hash_for_every_kind(
aiohttp_client, app_with_db,
):
"""Drift/identity primitive. Returns `{hashes: {id: {type, payload_hash}}}`
for EVERY module kind. `type` lets consumers detect a cross-kind id
clash (the 8-hex id-space is shared across all 5 kinds); `payload_hash`
is the existing drift signal the in-graph WP_Context compares against
its embedded snapshot. Pre-5.5.6 the endpoint hard-coded
`type="wildcard"`; the in-graph WP_Context now embeds non-wildcard
kinds too."""
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 body["hashes"][wc["id"]]["type"] == "wildcard"
assert body["hashes"][cb["id"]]["type"] == "combine" # every kind included
assert body["hashes"][wc["id"]]["payload_hash"] == wc["payload_hash"]
assert body["hashes"][cb["id"]]["payload_hash"] == cb["payload_hash"]
# ── ModuleRow shape (spec §4.2) ──────────────────────────────────────
# Post-004 the API row no longer carries a separate `uuid` field — the
# `id` IS the canonical 8-hex uuid. `payload_hash` stays.
async def test_list_modules_response_includes_id_and_payload_hash(
aiohttp_client, app_with_db,
):
app, conn = app_with_db
repo = ModuleRepository(conn)
repo.create(
type="wildcard", name="x", description="",
category_id=None, tags=[], payload={"options": []},
)
client = await aiohttp_client(app)
resp = await client.get("/wp/api/modules")
assert resp.status == 200
items = (await resp.json())["items"]
assert len(items) == 1
assert "id" in items[0]
assert "uuid" not in items[0]
assert "payload_hash" in items[0]
assert len(items[0]["id"]) == 8
assert len(items[0]["payload_hash"]) == 64
async def test_get_module_response_includes_id_and_payload_hash(
aiohttp_client, app_with_db,
):
app, conn = app_with_db
repo = ModuleRepository(conn)
row = repo.create(
type="wildcard", name="x", description="",
category_id=None, tags=[], payload={"options": []},
)
client = await aiohttp_client(app)
resp = await client.get(f"/wp/api/modules/{row['id']}")
assert resp.status == 200
data = await resp.json()
assert data["id"] == row["id"]
assert "uuid" not in data
assert data["payload_hash"] == row["payload_hash"]