Files
DumiFlex 39529a669b feat(engine): migrate library v1->v2 (multi-tag) + bump schema version
engine/migrations/v1_to_v2.py: option sub_category->sub_categories[]; sub-category name slugify (whitespace/grammar chars->_, reserved suffixed, de-duped) cascaded to options + tag_groups; instance category_filter list->boolean-expr string + exclude_null flag; nested @{uuid:a,b,null} refs->@{uuid:a or b!null}. Idempotent. Two entry points share one core: envelope migrate_v1_to_v2 (import/export, registered in _CHAIN) + per-row migrate_module/bundle_v1_to_v2 (lazy/bulk DB path).

Register _CHAIN[1] + bump engine CURRENT_SCHEMA_VERSION 1->2. New SQL migration 016 marches stale DB rows 1->2 at boot via run_bulk_lazy + the per-row migrators (014's documented bump pattern). TS mirror (migrations.ts) + client CURRENT bump follow next (no cross-language version assertion couples them). Update version/entity-count assertions in payload-migration, migration-runner, and export-build tests.
2026-06-07 01:40:53 +03:00

147 lines
5.2 KiB
Python

"""Tests for /wp/api/export/build — Task 10 / importer-exporter v2.
Endpoint wraps `engine.exporter.build_export_payload` and returns the
full 8-bucket payload directly. UUID sets are partitioned by bucket;
mis-typed UUIDs are silently dropped by the exporter (cross-bucket
isolation guarantee).
"""
import pytest
pytestmark = pytest.mark.asyncio
async def _create_wildcard(wp_client, name: str = "colors") -> dict:
resp = await wp_client.post("/wp/api/modules", json={
"type": "wildcard", "name": name, "description": "",
"category_id": None, "tags": [], "payload": {"options": []},
})
assert resp.status == 201
return await resp.json()
async def _create_bundle(wp_client, *, name: str, children=None) -> dict:
resp = await wp_client.post("/wp/api/bundles", json={
"name": name,
"children": children or [],
})
assert resp.status == 201
return await resp.json()
async def _create_template(wp_client, *, name: str = "tpl") -> dict:
resp = await wp_client.post("/wp/api/templates", json={
"name": name,
"template_string": "$subject, $style",
})
assert resp.status == 201
return await resp.json()
# ── Happy path ────────────────────────────────────────────────────────────
async def test_build_returns_seven_bucket_payload_with_wildcard(wp_client):
wc = await _create_wildcard(wp_client)
resp = await wp_client.post("/wp/api/export/build", json={
"wildcard_uuids": [wc["id"]],
})
assert resp.status == 200
body = await resp.json()
assert body["schema_version"] == 2
assert isinstance(body["exported_at"], str)
assert body["exported_at"] != ""
# All 8 buckets present.
for key in (
"bundles", "wildcards", "fixed_values", "combines",
"derivations", "constraints", "categories", "templates",
):
assert key in body, f"missing bucket: {key}"
# Wildcard appears in wildcards bucket with non-empty fingerprint.
assert len(body["wildcards"]) == 1
assert body["wildcards"][0]["id"] == wc["id"]
assert body["wildcards"][0].get("snapshot_fingerprint")
async def test_build_with_empty_body_returns_empty_buckets(wp_client):
resp = await wp_client.post("/wp/api/export/build", json={})
assert resp.status == 200
body = await resp.json()
assert body["schema_version"] == 2
for key in (
"bundles", "wildcards", "fixed_values", "combines",
"derivations", "constraints", "categories", "templates",
):
assert body[key] == [], f"expected empty bucket: {key}"
async def test_build_transitive_bundle_walk(wp_client):
"""Bundle A references bundle B via children[type==bundle]; passing
A's id should pull B in too."""
b = await _create_bundle(wp_client, name="inner")
a = await _create_bundle(
wp_client, name="outer",
children=[{"id": b["id"], "type": "bundle", "name": "inner"}],
)
resp = await wp_client.post("/wp/api/export/build", json={
"bundle_uuids": [a["id"]],
})
assert resp.status == 200
body = await resp.json()
ids = {row["id"] for row in body["bundles"]}
assert ids == {a["id"], b["id"]}
async def test_build_accepts_template_uuids_and_returns_templates(wp_client):
tpl = await _create_template(wp_client, name="Hero shot")
resp = await wp_client.post("/wp/api/export/build", json={
"template_uuids": [tpl["id"]],
})
assert resp.status == 200
body = await resp.json()
assert len(body["templates"]) == 1
assert body["templates"][0]["id"] == tpl["id"]
assert body["templates"][0]["name"] == "Hero shot"
assert body["templates"][0]["template_string"] == "$subject, $style"
# ── Validation ────────────────────────────────────────────────────────────
async def test_build_invalid_json_returns_400(wp_client):
resp = await wp_client.post(
"/wp/api/export/build",
data="not json",
headers={"content-type": "application/json"},
)
assert resp.status == 400
async def test_build_non_object_body_returns_400(wp_client):
resp = await wp_client.post("/wp/api/export/build", json=["x"])
assert resp.status == 400
async def test_build_non_array_uuid_list_returns_400(wp_client):
resp = await wp_client.post("/wp/api/export/build", json={
"bundle_uuids": "not a list",
})
assert resp.status == 400
# ── Cross-bucket isolation (inherited from exporter) ──────────────────────
async def test_build_wrong_type_uuid_silently_filtered(wp_client):
"""Passing a wildcard's id under combine_uuids should yield empty
combines AND empty wildcards — the exporter filters cross-bucket misses."""
wc = await _create_wildcard(wp_client)
resp = await wp_client.post("/wp/api/export/build", json={
"combine_uuids": [wc["id"]],
})
assert resp.status == 200
body = await resp.json()
assert body["combines"] == []
assert body["wildcards"] == []