SLICE-6: export & the round-trip lock — canonical serializer + streamed export (SD-0002 §7.2) #29
@@ -102,3 +102,71 @@ def test_more_images_than_variants_emits_image_only_rows():
|
||||
assert rows[0]["Variant SKU"] == "WG-MUG"
|
||||
assert rows[1]["Variant SKU"] == "" and rows[1]["Handle"] == "moon-mug"
|
||||
assert [r["Image Src"] for r in rows] == ["https://x/a.jpg", "https://x/b.jpg", "https://x/c.jpg"]
|
||||
|
||||
|
||||
import random
|
||||
|
||||
from app.domains.products import codec, diff, validate
|
||||
|
||||
|
||||
def _gen_catalog(seed: int) -> dict:
|
||||
"""A deterministic random text-field catalog, in load_catalog()'s shape."""
|
||||
rng = random.Random(seed)
|
||||
words = ["moon", "star", "river", "cloud", "ember", "fern", "slate", "wave"]
|
||||
catalog: dict[str, CatalogProduct] = {}
|
||||
n_products = rng.randint(1, 6)
|
||||
for pid in range(1, n_products + 1):
|
||||
handle = "-".join(rng.sample(words, rng.randint(1, 3))) + f"-{pid}"
|
||||
if handle in catalog:
|
||||
continue
|
||||
has_opts = rng.random() < 0.6
|
||||
option_names = ("Size", "Color", None) if has_opts else (None, None, None)
|
||||
tags = rng.sample(["apparel", "kitchen", "sale", "new"], rng.randint(0, 3))
|
||||
fields = {
|
||||
"title": f"{handle.title()} Thing",
|
||||
"description_html": rng.choice([None, "<p>hi</p>"]),
|
||||
"vendor": rng.choice([None, "Acme", "Wiggle Goods"]),
|
||||
"product_type": "standalone",
|
||||
"google_product_category": rng.choice([None, "Home & Garden"]),
|
||||
"tags": tags,
|
||||
"status": rng.choice(["draft", "active", "archived"]),
|
||||
"published": rng.choice([True, False]),
|
||||
}
|
||||
variants = []
|
||||
if has_opts:
|
||||
sizes = rng.sample(["S", "M", "L", "XL"], rng.randint(1, 4))
|
||||
for vi, size in enumerate(sizes, start=1):
|
||||
variants.append(CatalogVariant(
|
||||
id=pid * 100 + vi, options=(size, "Indigo", None), position=vi,
|
||||
fields={"sku": f"SKU-{pid}-{vi}", "variant_image": None}))
|
||||
else:
|
||||
variants.append(CatalogVariant(
|
||||
id=pid * 100 + 1, options=(None, None, None), position=1,
|
||||
fields={"sku": f"SKU-{pid}", "variant_image": None}))
|
||||
images = []
|
||||
for ii in range(rng.randint(0, 3)):
|
||||
images.append(CatalogImage(
|
||||
id=pid * 10 + ii, source_url=f"https://img/{handle}-{ii}.jpg",
|
||||
position=ii + 1, alt_text=rng.choice([None, f"alt {ii}"])))
|
||||
catalog[handle] = CatalogProduct(
|
||||
id=pid, handle=handle, title=fields["title"], option_names=option_names,
|
||||
fields=fields, variants=variants, images=images)
|
||||
return catalog
|
||||
|
||||
|
||||
def _roundtrip_diff(catalog: dict) -> diff.DiffResult:
|
||||
"""export → bytes → import pipeline → diff against the same catalog."""
|
||||
text = "".join(serialize.catalog_to_csv(catalog.values()))
|
||||
parsed = codec.parse_csv(text.encode("utf-8"))
|
||||
products = validate.build_products(parsed)
|
||||
return diff.compute_diff(catalog, products)
|
||||
|
||||
|
||||
def test_inv12_roundtrip_is_noop_over_generated_catalogs():
|
||||
for seed in range(200):
|
||||
catalog = _gen_catalog(seed)
|
||||
result = _roundtrip_diff(catalog)
|
||||
assert result.summary["adds"] == 0, f"seed {seed}: {result.summary}"
|
||||
assert result.summary["updates"] == 0, f"seed {seed}: {result.summary}"
|
||||
assert result.summary["errors"] == 0, f"seed {seed}: {result.summary}"
|
||||
assert result.summary["unchanged"] == len(catalog), f"seed {seed}"
|
||||
|
||||
Reference in New Issue
Block a user