""" tests/test_gallery.py — Zero-GPU gallery loader tests (Plan 02-04, UI-01). Verifies: (a) load_example returns a 7-element tuple matching render_result's output arity and does NOT raise. (b) load_example is NOT wrapped by @spaces.GPU and never calls the model — run_inference_multi is patched to raise; load_example must still succeed. (c) data/gallery/*-result.json fixtures round-trip into StructuredResult correctly (severity is int or None, all fields present). BUREAUCAT_NO_MODEL=1 is set BEFORE import to prevent model weight loading. """ import json import os import sys import types import unittest from pathlib import Path from unittest.mock import patch # ---- escape hatch: must be set BEFORE importing app ---- os.environ["BUREAUCAT_NO_MODEL"] = "1" # Ensure project root is on sys.path when run from tests/ or project root _REPO_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) if _REPO_ROOT not in sys.path: sys.path.insert(0, _REPO_ROOT) import app # noqa: E402 (after path setup) from app import EXAMPLE_LETTERS, StructuredResult, load_example # noqa: E402 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- _GALLERY_DIR = Path(_REPO_ROOT) / "data" / "gallery" _STRUCTURED_RESULT_FIELDS = frozenset( StructuredResult.__dataclass_fields__.keys() ) _RENDER_RESULT_ARITY = 7 # (panic_html, mascot_html, quip_md, tldr, why, actions, deadlines_html) def _make_select_event(index: int): """Return a SimpleNamespace that looks like gr.SelectData(index=N).""" return types.SimpleNamespace(index=index) # --------------------------------------------------------------------------- # Tests # --------------------------------------------------------------------------- class TestExampleLettersList(unittest.TestCase): """EXAMPLE_LETTERS has >= 4 entries with required keys.""" def test_length(self): self.assertGreaterEqual(len(EXAMPLE_LETTERS), 4, "need >= 4 gallery entries") def test_keys(self): for entry in EXAMPLE_LETTERS: for key in ("slug", "label", "image", "cached"): self.assertIn(key, entry, f"missing key '{key}' in entry {entry}") def test_image_paths_under_public(self): for entry in EXAMPLE_LETTERS: self.assertIn( "data/letters/public/", entry["image"], f"image path should be under data/letters/public/: {entry['image']}", ) def test_cached_paths_under_gallery(self): for entry in EXAMPLE_LETTERS: self.assertIn( "data/gallery/", entry["cached"], f"cached path should be under data/gallery/: {entry['cached']}", ) class TestLoadExampleArity(unittest.TestCase): """load_example returns correct arity without raising.""" def setUp(self): """Skip if gallery JSONs don't exist yet (generated by Task 1).""" json_path = Path(_REPO_ROOT) / EXAMPLE_LETTERS[0]["cached"] if not json_path.exists(): self.skipTest(f"Gallery JSON not yet generated: {json_path}") def test_returns_tuple_of_correct_arity(self): """load_example(index=0) returns a 7-element tuple.""" evt = _make_select_event(0) result_tuple = load_example(evt) self.assertIsInstance(result_tuple, tuple) self.assertEqual( len(result_tuple), _RENDER_RESULT_ARITY, f"expected {_RENDER_RESULT_ARITY}-element tuple, got {len(result_tuple)}", ) def test_all_indices(self): """load_example works for every valid index in EXAMPLE_LETTERS.""" for i, entry in enumerate(EXAMPLE_LETTERS): json_path = Path(_REPO_ROOT) / entry["cached"] if not json_path.exists(): continue with self.subTest(index=i, slug=entry["slug"]): evt = _make_select_event(i) tpl = load_example(evt) self.assertEqual(len(tpl), _RENDER_RESULT_ARITY) class TestLoadExampleNoModelCall(unittest.TestCase): """load_example never calls run_inference_multi — zero GPU guarantee.""" def setUp(self): json_path = Path(_REPO_ROOT) / EXAMPLE_LETTERS[0]["cached"] if not json_path.exists(): self.skipTest(f"Gallery JSON not yet generated: {json_path}") def test_no_model_call_when_inference_patched_to_raise(self): """ Even if run_inference_multi would raise, load_example still succeeds. This proves load_example reads only disk (never calls the model). """ def _boom(*args, **kwargs): raise RuntimeError("load_example must NOT call run_inference_multi") with patch.object(app, "run_inference_multi", side_effect=_boom): evt = _make_select_event(0) # Must NOT raise RuntimeError result_tuple = load_example(evt) self.assertEqual(len(result_tuple), _RENDER_RESULT_ARITY) def test_load_example_not_spaces_gpu_wrapped(self): """ load_example must not be decorated with @spaces.GPU. Presence of __wrapped__ or _spaces_fn_wrapped attributes indicates GPU decoration. """ self.assertFalse( hasattr(load_example, "__wrapped__"), "load_example should NOT be wrapped with @spaces.GPU (__wrapped__ found)", ) self.assertFalse( hasattr(load_example, "_spaces_fn_wrapped"), "load_example should NOT be wrapped with @spaces.GPU (_spaces_fn_wrapped found)", ) class TestGalleryJsonFixtures(unittest.TestCase): """data/gallery/*-result.json files round-trip into StructuredResult.""" def _iter_gallery_jsons(self): """Yield (stem, dict) for each existing gallery JSON.""" for entry in EXAMPLE_LETTERS: json_path = Path(_REPO_ROOT) / entry["cached"] if json_path.exists(): yield entry["slug"], json.loads(json_path.read_text(encoding="utf-8")) def test_all_fields_present(self): found_any = False for slug, data in self._iter_gallery_jsons(): found_any = True with self.subTest(slug=slug): missing = _STRUCTURED_RESULT_FIELDS - set(data.keys()) self.assertEqual( missing, set(), f"{slug}: missing StructuredResult fields: {missing}", ) if not found_any: self.skipTest("No gallery JSONs exist yet — run Task 1 first") def test_severity_type(self): found_any = False for slug, data in self._iter_gallery_jsons(): found_any = True with self.subTest(slug=slug): sev = data.get("severity") self.assertTrue( sev is None or isinstance(sev, int), f"{slug}: severity must be int or None, got {type(sev).__name__}", ) if not found_any: self.skipTest("No gallery JSONs exist yet — run Task 1 first") def test_no_quip_prefix(self): """quip field must NOT contain 'Bureaucat says:' prefix (prevents double-prefix).""" found_any = False for slug, data in self._iter_gallery_jsons(): found_any = True with self.subTest(slug=slug): quip = data.get("quip", "") self.assertNotIn( "Bureaucat says:", quip, f"{slug}: quip contains 'Bureaucat says:' prefix — would double-prefix in UI", ) if not found_any: self.skipTest("No gallery JSONs exist yet — run Task 1 first") def test_structured_result_roundtrip(self): """StructuredResult(**data) does not raise for any gallery JSON.""" found_any = False for slug, data in self._iter_gallery_jsons(): found_any = True with self.subTest(slug=slug): # Should not raise result = StructuredResult(**data) self.assertIsInstance(result, StructuredResult) self.assertIsNotNone(result.raw, f"{slug}: raw field is None") if not found_any: self.skipTest("No gallery JSONs exist yet — run Task 1 first") if __name__ == "__main__": unittest.main()