Phase 5.5.4 (SPA library picker for ContextWidget) plus QA-driven polish. * New ModulePickerModal with search, kind tabs, single + multi-pick, and compact filter UX (favorites/hide-added inline; category + tag chips behind popover with active-count badge). * ModuleEditModal kind-aware body — wildcard option editor with enable/disable, weight overrides, mode picker (random/subset/pinned), sub-category multi-select. * Lock + internal per-instance overrides ported and adapted: locked_seed derives a per-wildcard rng via sha256(seed:binding); internal hides bindings from the public PIPELINE_CONTEXT payload. * Card indicators: modified dot (option-pool overrides), missing-from- library dot (polled hash check), in-card lock + internal toggles. * RNG-faithful assembler preview via new /wp/api/preview/resolve; header shows "PREVIEW · 42". UNRESOLVED chips clickable to strip $var. * Bulk collapse/expand toggle, focus toast w/ pulsing border overlay that tracks node bounds, friendly @name substitution in modal. * Backend: new /wp/api/modules/import-from-workflow endpoint preserves uuid; engine emits per-module effective seeds via WP_Context UI payload for authoritative lock-toggle defaults.
215 lines
7.6 KiB
Python
215 lines
7.6 KiB
Python
"""Tests for /wp/api/modules CRUD endpoints."""
|
|
import pytest
|
|
|
|
pytestmark = pytest.mark.asyncio
|
|
|
|
|
|
async def test_list_empty(wp_client):
|
|
resp = await wp_client.get("/wp/api/modules")
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body == {"items": [], "total": 0}
|
|
|
|
|
|
async def test_create_returns_201_and_id(wp_client):
|
|
resp = await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "colors", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
assert resp.status == 201
|
|
body = await resp.json()
|
|
# Post migration 004 every module's id is an 8-hex short uuid.
|
|
assert len(body["id"]) == 8
|
|
assert all(c in "0123456789abcdef" for c in body["id"])
|
|
assert body["version"] == 1
|
|
assert body["payload"] == {"options": []}
|
|
|
|
|
|
async def test_create_missing_type_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/modules", json={
|
|
"name": "x", "payload": {"values": []},
|
|
})
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_import_from_workflow_creates_at_supplied_id(wp_client):
|
|
"""ContextWidget's `Save to library` action posts here with the
|
|
workflow's existing 8-hex uuid so existing `@{uuid}` refs stay
|
|
valid. The row must land at THAT id, not a freshly generated one."""
|
|
resp = await wp_client.post("/wp/api/modules/import-from-workflow", json={
|
|
"id": "deadbeef",
|
|
"type": "wildcard",
|
|
"name": "imported",
|
|
"payload": {"options": []},
|
|
})
|
|
assert resp.status == 201
|
|
body = await resp.json()
|
|
assert body["id"] == "deadbeef"
|
|
|
|
# Subsequent GET returns the saved row.
|
|
g = await wp_client.get("/wp/api/modules/deadbeef")
|
|
assert g.status == 200
|
|
|
|
|
|
async def test_import_from_workflow_rejects_existing_id(wp_client):
|
|
"""Rare race: row already exists. Endpoint returns 409."""
|
|
await wp_client.post("/wp/api/modules/import-from-workflow", json={
|
|
"id": "ababab12",
|
|
"type": "wildcard",
|
|
"name": "first",
|
|
"payload": {"options": []},
|
|
})
|
|
resp = await wp_client.post("/wp/api/modules/import-from-workflow", json={
|
|
"id": "ababab12",
|
|
"type": "wildcard",
|
|
"name": "second",
|
|
"payload": {"options": []},
|
|
})
|
|
assert resp.status == 409
|
|
|
|
|
|
async def test_import_from_workflow_rejects_malformed_id(wp_client):
|
|
"""Caller-supplied id must be 8-char lowercase hex — the same
|
|
shape `_gen_id()` produces. Anything else (uppercase, wrong
|
|
length, non-hex chars) → 400."""
|
|
for bad_id in ("DEADBEEF", "deadbee", "deadbeefff", "wc_color"):
|
|
resp = await wp_client.post("/wp/api/modules/import-from-workflow", json={
|
|
"id": bad_id,
|
|
"type": "wildcard",
|
|
"name": "x",
|
|
"payload": {"options": []},
|
|
})
|
|
assert resp.status == 400, f"id={bad_id} should be rejected"
|
|
|
|
|
|
async def test_import_from_workflow_missing_id_returns_400(wp_client):
|
|
resp = await wp_client.post("/wp/api/modules/import-from-workflow", json={
|
|
"type": "wildcard", "name": "x", "payload": {"options": []},
|
|
})
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_create_invalid_json_returns_400(wp_client):
|
|
resp = await wp_client.post(
|
|
"/wp/api/modules",
|
|
data="not json",
|
|
headers={"content-type": "application/json"},
|
|
)
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_get_by_id(wp_client):
|
|
create = await wp_client.post("/wp/api/modules", json={
|
|
"type": "fixed_values", "name": "lens", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"values": []},
|
|
})
|
|
mid = (await create.json())["id"]
|
|
resp = await wp_client.get(f"/wp/api/modules/{mid}")
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["id"] == mid
|
|
assert body["type"] == "fixed_values"
|
|
|
|
|
|
async def test_get_missing_404(wp_client):
|
|
resp = await wp_client.get("/wp/api/modules/ghost")
|
|
assert resp.status == 404
|
|
|
|
|
|
async def test_put_updates_only_provided_fields(wp_client):
|
|
create = await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "x", "description": "hello",
|
|
"category_id": None, "tags": ["a"], "payload": {"options": []},
|
|
})
|
|
mid = (await create.json())["id"]
|
|
resp = await wp_client.put(f"/wp/api/modules/{mid}", json={"name": "y"})
|
|
assert resp.status == 200
|
|
body = await resp.json()
|
|
assert body["name"] == "y"
|
|
assert body["description"] == "hello" # unchanged
|
|
assert body["tags"] == ["a"] # unchanged
|
|
assert body["version"] == 2
|
|
|
|
|
|
async def test_put_missing_404(wp_client):
|
|
resp = await wp_client.put("/wp/api/modules/ghost", json={"name": "x"})
|
|
assert resp.status == 404
|
|
|
|
|
|
async def test_delete_returns_204(wp_client):
|
|
create = await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "x", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
mid = (await create.json())["id"]
|
|
resp = await wp_client.delete(f"/wp/api/modules/{mid}")
|
|
assert resp.status == 204
|
|
follow = await wp_client.get(f"/wp/api/modules/{mid}")
|
|
assert follow.status == 404
|
|
|
|
|
|
async def test_delete_missing_404(wp_client):
|
|
resp = await wp_client.delete("/wp/api/modules/ghost")
|
|
assert resp.status == 404
|
|
|
|
|
|
async def test_list_filter_by_type(wp_client):
|
|
await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "a", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
await wp_client.post("/wp/api/modules", json={
|
|
"type": "fixed_values", "name": "b", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"values": []},
|
|
})
|
|
resp = await wp_client.get("/wp/api/modules?type=wildcard")
|
|
body = await resp.json()
|
|
assert body["total"] == 1
|
|
assert body["items"][0]["type"] == "wildcard"
|
|
|
|
|
|
async def test_list_search_filter(wp_client):
|
|
await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "ColorPalette", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": "lighting", "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
resp = await wp_client.get("/wp/api/modules?q=color")
|
|
body = await resp.json()
|
|
assert body["total"] == 1
|
|
assert body["items"][0]["name"] == "ColorPalette"
|
|
|
|
|
|
async def test_list_total_ignores_limit(wp_client):
|
|
"""Regression: Dashboard count cards used to read 1 for every kind
|
|
because list_modules returned `total = len(items)` AFTER the LIMIT
|
|
was applied. `total` must reflect the unpaginated row count so the
|
|
SPA can poll cheaply with limit=1 yet still display the real total."""
|
|
for n in ("a", "b", "c"):
|
|
await wp_client.post("/wp/api/modules", json={
|
|
"type": "wildcard", "name": n, "description": "",
|
|
"category_id": None, "tags": [], "payload": {"options": []},
|
|
})
|
|
resp = await wp_client.get("/wp/api/modules?type=wildcard&limit=1")
|
|
body = await resp.json()
|
|
assert len(body["items"]) == 1 # paginated
|
|
assert body["total"] == 3 # but total reflects ALL rows
|
|
|
|
|
|
async def test_list_invalid_limit_400(wp_client):
|
|
resp = await wp_client.get("/wp/api/modules?limit=abc")
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_list_negative_limit_400(wp_client):
|
|
resp = await wp_client.get("/wp/api/modules?limit=-1")
|
|
assert resp.status == 400
|
|
|
|
|
|
async def test_list_negative_offset_400(wp_client):
|
|
resp = await wp_client.get("/wp/api/modules?offset=-1")
|
|
assert resp.status == 400
|