Spaces:
Running on Zero
Running on Zero
| """Tests for hosted Space VLM validation tooling.""" | |
| from __future__ import annotations | |
| import tempfile | |
| import unittest | |
| from datetime import datetime, timezone | |
| from pathlib import Path | |
| from scripts.check_space_vlm import ( | |
| DEFAULT_HARDWARE, | |
| TEST_ASSETS, | |
| ValidationResult, | |
| extract_trace_record, | |
| parse_space_repo_id, | |
| render_report, | |
| space_client_url, | |
| update_failure_notes, | |
| validate_prediction, | |
| write_json_results, | |
| write_trace_record, | |
| ) | |
| from src.models.schema import DiaryEntry, ObjectInfo, ObjectUnderstanding, Persona, PersonaEnvelope, TraceRecord | |
| from src.utils.zero_gpu import zero_gpu | |
| class SpaceVlmToolingTest(unittest.TestCase): | |
| def test_asset_manifest_covers_three_validation_objects(self) -> None: | |
| keys = {asset.key for asset in TEST_ASSETS} | |
| self.assertEqual(keys, {"mug", "keyboard", "shoe"}) | |
| self.assertTrue(all(asset.source_page.startswith("https://commons.wikimedia.org/") for asset in TEST_ASSETS)) | |
| self.assertTrue(all(asset.download_url.startswith("https://commons.wikimedia.org/") for asset in TEST_ASSETS)) | |
| def test_parse_space_repo_id_from_space_url(self) -> None: | |
| repo_id = parse_space_repo_id("https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary") | |
| self.assertEqual(repo_id, "build-small-hackathon/ObjectverseDiary") | |
| def test_space_client_url_uses_direct_hf_space_host(self) -> None: | |
| client_url = space_client_url("https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary") | |
| self.assertEqual(client_url, "https://build-small-hackathon-objectversediary.hf.space") | |
| def test_default_hardware_targets_zero_gpu_live_space(self) -> None: | |
| self.assertEqual(DEFAULT_HARDWARE, "zero-a10g") | |
| def test_zero_gpu_decorator_is_noop_without_spaces_package(self) -> None: | |
| def sample(value: int) -> int: | |
| return value + 1 | |
| decorated = zero_gpu(duration=10)(sample) | |
| self.assertEqual(decorated(2), 3) | |
| def test_validate_prediction_accepts_minicpm_runtime(self) -> None: | |
| asset = TEST_ASSETS[0] | |
| trace = _trace_record( | |
| object_name="striped coffee mug", | |
| visible_features=["ceramic cup", "handle", "striped surface"], | |
| runtime_vision="minicpm-v object understanding", | |
| fallbacks=["mock-text-runtime"], | |
| ) | |
| response = [None, {}, {}, "", "", "", trace.model_dump(mode="json")] | |
| result = validate_prediction(asset, Path("/tmp/mug.jpg"), response) | |
| self.assertTrue(result.passed) | |
| self.assertEqual(result.object_name, "striped coffee mug") | |
| self.assertEqual(result.runtime_text, "mock persona and diary generation") | |
| def test_validate_prediction_rejects_vision_fallback(self) -> None: | |
| asset = TEST_ASSETS[1] | |
| trace = _trace_record( | |
| object_name="computer keyboard", | |
| visible_features=["black keys"], | |
| runtime_vision="minicpm-v object understanding", | |
| fallbacks=["vision-fallback-to-mock", "mock-text-runtime"], | |
| ) | |
| response = [None, {}, {}, "", "", "", trace.model_dump(mode="json")] | |
| result = validate_prediction(asset, Path("/tmp/keyboard.jpg"), response) | |
| self.assertFalse(result.passed) | |
| self.assertIn("vision fallback marker", result.error) | |
| def test_extract_trace_record_accepts_gradio_response(self) -> None: | |
| trace = _trace_record( | |
| object_name="running shoe", | |
| visible_features=["laces", "rubber sole"], | |
| runtime_vision="minicpm-v object understanding", | |
| fallbacks=["mock-text-runtime"], | |
| ) | |
| response = [None, {}, {}, "", "", "", trace.model_dump(mode="json")] | |
| extracted = extract_trace_record(response) | |
| self.assertEqual(extracted.object_understanding.object.name, "running shoe") | |
| self.assertEqual(extracted.model_runtime["vision"], "minicpm-v object understanding") | |
| def test_write_trace_record_writes_valid_public_json(self) -> None: | |
| trace = _trace_record( | |
| object_name="striped coffee mug", | |
| visible_features=["ceramic cup", "handle"], | |
| runtime_vision="minicpm-v object understanding", | |
| fallbacks=["mock-text-runtime"], | |
| ) | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| output_path = write_trace_record(trace, Path(tmp_dir) / "mug.json") | |
| payload = output_path.read_text(encoding="utf-8") | |
| parsed = TraceRecord.model_validate_json(payload) | |
| self.assertEqual(parsed.trace_id, trace.trace_id) | |
| self.assertNotIn("HUGGINGFACE_TOKEN", payload) | |
| self.assertNotIn("HF_TOKEN", payload) | |
| self.assertNotIn("hf_", payload) | |
| def test_write_trace_record_rejects_sensitive_token_markers(self) -> None: | |
| trace = _trace_record( | |
| object_name="computer keyboard", | |
| visible_features=["black keys"], | |
| runtime_vision="minicpm-v object understanding", | |
| fallbacks=["mock-text-runtime"], | |
| ) | |
| trace.model_runtime["runtime"] = "vision model id: openbmb/MiniCPM-V-2_6; token hf_forbidden" | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| output_path = Path(tmp_dir) / "keyboard.json" | |
| with self.assertRaises(ValueError): | |
| write_trace_record(trace, output_path) | |
| self.assertFalse(output_path.exists()) | |
| def test_render_report_includes_results_and_safe_config(self) -> None: | |
| result = ValidationResult( | |
| key="shoe", | |
| label="Running shoe", | |
| source_page="https://commons.wikimedia.org/wiki/File:Running_shoes.jpg", | |
| image_path="/tmp/shoe.jpg", | |
| passed=True, | |
| object_name="running shoe", | |
| visible_features=["laces", "athletic sole"], | |
| likely_context="sports gear", | |
| confidence=0.86, | |
| runtime_vision="minicpm-v object understanding", | |
| runtime_text="mock persona and diary generation", | |
| fallbacks=["mock-text-runtime"], | |
| ) | |
| report = render_report( | |
| space_url="https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary", | |
| repo_id="build-small-hackathon/ObjectverseDiary", | |
| results=[result], | |
| probe_result=_probe_result(minicpm_load_ok=True), | |
| configured={"hardware": "zero-a10g", "OBJECTVERSE_VISION_BACKEND": "minicpm-v"}, | |
| ) | |
| self.assertIn("Overall status: PASS", report) | |
| self.assertIn("Vision Runtime Probe", report) | |
| self.assertIn("minicpm_load_ok", report) | |
| self.assertIn("Running shoe", report) | |
| self.assertIn("OBJECTVERSE_VISION_BACKEND", report) | |
| self.assertIn("live MiniCPM-V configuration remains active", report) | |
| self.assertNotIn("hf_", report.lower()) | |
| self.assertNotIn("HUGGINGFACE_TOKEN", report) | |
| def test_render_report_includes_configuration_error(self) -> None: | |
| report = render_report( | |
| space_url="https://huggingface.co/spaces/build-small-hackathon/ObjectverseDiary", | |
| repo_id="build-small-hackathon/ObjectverseDiary", | |
| results=[], | |
| rollback={"hardware": "cpu-basic", "OBJECTVERSE_VISION_BACKEND": "mock"}, | |
| configuration_error="HfHubHTTPError: 402 Payment Required", | |
| ) | |
| self.assertIn("Overall status: FAIL", report) | |
| self.assertIn("Configuration Error", report) | |
| self.assertIn("402 Payment Required", report) | |
| def test_write_json_results_includes_probe_when_present(self) -> None: | |
| result = ValidationResult( | |
| key="mug", | |
| label="Coffee mug", | |
| source_page="https://commons.wikimedia.org/wiki/File:Striped_coffee_mug.jpg", | |
| image_path="/tmp/mug.jpg", | |
| passed=False, | |
| object_name="coffee mug", | |
| visible_features=["uploaded photo provided"], | |
| likely_context="everyday human environment", | |
| confidence=0.42, | |
| runtime_vision="minicpm-v object understanding", | |
| runtime_text="mock persona and diary generation", | |
| fallbacks=["vision-fallback-to-mock", "mock-text-runtime"], | |
| error="vision fallback marker was present", | |
| ) | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| output_path = write_json_results( | |
| [result], | |
| Path(tmp_dir) / "report.json", | |
| probe_result=_probe_result(minicpm_load_ok=False), | |
| ) | |
| payload = output_path.read_text(encoding="utf-8") | |
| parsed = output_path.read_text(encoding="utf-8") | |
| self.assertIn('"probe"', payload) | |
| self.assertIn('"results"', payload) | |
| self.assertNotIn("hf_", parsed) | |
| self.assertNotIn("HF_TOKEN", parsed) | |
| def test_update_failure_notes_replaces_latest_failure_section(self) -> None: | |
| failed = ValidationResult( | |
| key="keyboard", | |
| label="Computer keyboard", | |
| source_page="https://commons.wikimedia.org/wiki/File:Computer_keyboard.jpg", | |
| image_path="/tmp/keyboard.jpg", | |
| passed=False, | |
| object_name="keyboard", | |
| visible_features=["uploaded photo provided"], | |
| likely_context="everyday human environment", | |
| confidence=0.42, | |
| runtime_vision="minicpm-v object understanding", | |
| runtime_text="mock persona and diary generation", | |
| fallbacks=["vision-fallback-to-mock", "mock-text-runtime"], | |
| error="vision fallback marker was present", | |
| ) | |
| with tempfile.TemporaryDirectory() as tmp_dir: | |
| notes_path = Path(tmp_dir) / "FAILURES.md" | |
| notes_path.write_text("# Failure Notes\n\n## Current Status\n\nStable.\n", encoding="utf-8") | |
| update_failure_notes(results=[failed], probe_result=_probe_result(False), output_path=notes_path) | |
| update_failure_notes(results=[failed], probe_result=_probe_result(False), output_path=notes_path) | |
| content = notes_path.read_text(encoding="utf-8") | |
| self.assertEqual(content.count("## Latest Space VLM Validation Failure"), 1) | |
| self.assertIn("keyboard: vision fallback marker was present", content) | |
| self.assertNotIn("hf_", content) | |
| def _trace_record( | |
| *, | |
| object_name: str, | |
| visible_features: list[str], | |
| runtime_vision: str, | |
| fallbacks: list[str], | |
| ) -> TraceRecord: | |
| persona = PersonaEnvelope( | |
| persona=Persona( | |
| object_name=object_name, | |
| character_name="Test Object", | |
| mood="watchful", | |
| secret_fear="being ignored", | |
| core_memory="It remembers the test bench.", | |
| complaint="I am more than a fixture.", | |
| tags=["test", "object", "archive"], | |
| ) | |
| ) | |
| return TraceRecord( | |
| trace_id="space-vlm-test", | |
| created_at=datetime.now(timezone.utc), | |
| mode="Cynical", | |
| input={"has_image": True, "image_filename": "asset.jpg", "description": "public test asset"}, | |
| object_understanding=ObjectUnderstanding( | |
| object=ObjectInfo( | |
| name=object_name, | |
| visible_features=visible_features, | |
| likely_context="test environment", | |
| confidence=0.9, | |
| ) | |
| ), | |
| persona=persona, | |
| diary=DiaryEntry( | |
| title="Secret Diary - Day 1", | |
| english="I was tested today.", | |
| chinese="今天我被测试了。", | |
| ), | |
| model_runtime={ | |
| "vision": runtime_vision, | |
| "text": "mock persona and diary generation", | |
| "runtime": "vision model id: openbmb/MiniCPM-V-2_6; no llama.cpp model connected yet", | |
| }, | |
| fallbacks=fallbacks, | |
| ) | |
| def _probe_result(minicpm_load_ok: bool) -> dict[str, object]: | |
| return { | |
| "backend": "minicpm-v", | |
| "vision_model_id": "openbmb/MiniCPM-V-2_6", | |
| "torch_import": True, | |
| "transformers_import": True, | |
| "cuda_available": True, | |
| "device_count": 1, | |
| "device_name": "NVIDIA test device", | |
| "mps_available": False, | |
| "minicpm_load_attempted": True, | |
| "minicpm_load_ok": minicpm_load_ok, | |
| "errors": [] if minicpm_load_ok else [{"stage": "minicpm_load", "type": "RuntimeError", "summary": "test failure"}], | |
| } | |
| if __name__ == "__main__": | |
| unittest.main() | |