"""End-to-end test through the real app handlers against the REAL Modal backend. Requires the backend to be deployed: modal deploy modal_backend/shotcraft_inference.py Optionally point elsewhere: SHOTCRAFT_API_URL=https://... python test_app_e2e.py This spends real GPU time (1 MiniCPM call + 6 FLUX frames) — run it for Slice 4/5 validation, not in a tight loop. """ import json import zipfile from PIL import Image import app from model_runtime import health h = health() assert h.get("status") == "ok", f"Backend not reachable: {h}" print("backend OK —", h) photo = Image.open("examples/demo_product.png") # Stage 1 handler — real MiniCPM out = app.run_stage1(photo, "Handmade ceramic mugs from Krakow", "Home", "Minimal") pkg, analysis = out[0], out[1] fields = list(out[2:-1]) assert len(pkg.shots) == 5 and len(fields) == 40 print("run_stage1 OK —", analysis[:60], "...") # Edit prompt 3 (AC-2) + non-prompt field edit (FR-1.3) fields[2 * 8 + 7] = ("EDITED: lifestyle close-up of the speckled ceramic mug " "on a linen tablecloth, morning light") fields[1 * 8 + 0] = "Renamed Concept" # Stage 2 handler — real FLUX, 5 frames in one backend call class P: # stub gr.Progress def __call__(self, *a, **k): pass gallery, pkg2, regen_state = app.run_stage2(pkg, "Minimal", "1:1", *fields, progress=P()) assert len(gallery) == 5 assert all(img.size == (1024, 1024) for img, _ in gallery) assert pkg2.shots[1].concept_name == "Renamed Concept" assert pkg2.shots[2].image_prompt.startswith("EDITED:") print("run_stage2 OK — 5 real frames, edits persisted") # Regen one (AC-4) — only shot 2 changes import io before = io.BytesIO(); gallery[1][0].save(before, "PNG") gallery2, regen_state2 = app.regen_one(pkg2, "Minimal", "1:1", 2, gallery, regen_state) after = io.BytesIO(); gallery2[1][0].save(after, "PNG") assert regen_state2[2] == 1 assert before.getvalue() != after.getvalue(), "regen frame should differ" print("regen_one OK — shot 2 rerolled, counter:", regen_state2[2]) # Export (AC-5/AC-6) path = app.export_zip(pkg2, gallery2, [1, 4], regen_state2) zf = zipfile.ZipFile(path) names = sorted(zf.namelist()) m = json.loads(zf.read("selection_manifest.json")) assert "selection_manifest.json" in names assert len([n for n in names if n.endswith(".png")]) == 5 assert m["hero_frames"] == [1, 4] assert m["shots"][2]["image_prompt"].startswith("EDITED:") assert m["shots"][1]["regen_count"] == 1 print("export_zip OK —", names) print() print("REAL E2E PASSED")