From b40e4d30b509918634d2bff5d0ea9226615066b5 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Thu, 11 Jun 2026 21:58:47 -0700 Subject: [PATCH] =?UTF-8?q?test(products):=20INV-12=20property=20test=20?= =?UTF-8?q?=E2=80=94=20export=20round-trips=20to=20a=20no-op=20(SD-0002=20?= =?UTF-8?q?=C2=A76.8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/tests/test_products_serialize.py | 68 ++++++++++++++++++++++++ 1 file changed, 68 insertions(+) diff --git a/backend/tests/test_products_serialize.py b/backend/tests/test_products_serialize.py index 6c715e8..7b9dbea 100644 --- a/backend/tests/test_products_serialize.py +++ b/backend/tests/test_products_serialize.py @@ -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, "

hi

"]), + "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}"