import os import unittest from unittest.mock import Mock, patch from PIL import Image import bookscope class EnvMixin: def setUp(self): self._env_patcher = patch.dict( os.environ, { key: value for key, value in os.environ.items() if not key.startswith("BOOKSCOPE_") and key != "HF_TOKEN" }, clear=True, ) self._env_patcher.start() def tearDown(self): self._env_patcher.stop() class ScanFlowTests(EnvMixin, unittest.TestCase): def test_default_live_failure_returns_empty_table_not_demo_rows(self): image = Image.new("RGB", (20, 20), "white") with patch.object(bookscope, "_call_hf_gradio_space", side_effect=RuntimeError("boom")): frame, status = bookscope.scan_shelf_image(image) self.assertTrue(frame.empty) self.assertIn("Live scan failed", status) def test_demo_mode_returns_demo_rows_only_when_enabled(self): os.environ["BOOKSCOPE_DEMO_MODE"] = "true" image = Image.new("RGB", (20, 20), "white") frame, status = bookscope.scan_shelf_image(image) self.assertFalse(frame.empty) self.assertEqual(set(frame["source"]), {"demo"}) self.assertIn("Demo mode is active", status) def test_default_provider_uses_minicpm_gradio_space(self): image = Image.new("RGB", (20, 20), "white") with patch.object(bookscope, "_call_hf_gradio_space", return_value='{"books": []}') as gradio: bookscope.scan_shelf_image(image) gradio.assert_called_once() def test_empty_gradio_space_env_still_uses_default_space(self): os.environ["BOOKSCOPE_GRADIO_SPACE"] = "" self.assertEqual(bookscope._gradio_space_id(), bookscope.DEFAULT_MINICPM_SPACE) self.assertTrue(bookscope._should_use_gradio_space()) def test_explicit_hf_model_uses_inference_provider_path(self): os.environ["HF_TOKEN"] = "token" os.environ["BOOKSCOPE_HF_MODEL"] = "provider/model" self.assertFalse(bookscope._should_use_gradio_space()) class EnrichmentTests(EnvMixin, unittest.TestCase): def test_open_library_malformed_search_json_is_not_fatal(self): response = Mock() response.raise_for_status.return_value = None response.json.side_effect = ValueError("not json") with patch("bookscope.requests.get", return_value=response): self.assertIsNone(bookscope._open_library_match("Bad Body", "Unknown")) def test_open_library_malformed_editions_json_returns_no_isbn(self): response = Mock() response.raise_for_status.return_value = None response.json.side_effect = ValueError("not json") with patch("bookscope.requests.get", return_value=response): self.assertEqual(bookscope._isbn_from_editions("/works/OL1W"), "") if __name__ == "__main__": unittest.main()