"""Smoke tests for the initial mock MVP.""" from __future__ import annotations import json import sys import tempfile import types import unittest from pathlib import Path from unittest.mock import patch import src.models.llama_cpp_runner as llama_cpp_runner from src.example_cache import load_sample_generation, sample_trace_path from src.examples import EXAMPLE_OBJECTS, gradio_examples from src.models.llama_cpp_runner import ( generate_diary, generate_persona, reply_as_object, reset_text_runtime_fallbacks, ) from src.models.vision_runner import understand_object, understand_object_with_metadata from src.models.vision_runner import probe_vision_runtime from src.pipeline import generate_object_diary from src.renderer.share_card import render_share_card from src.ui.layout import vision_runtime_probe from src.traces.anonymizer import anonymize_text from src.traces.logger import build_trace, save_trace from scripts.generate_sample_traces import generate_sample_traces from scripts.check_initial_stage import run_checks from src.config import get_runtime_settings, runtime_status class FakeMiniCpmModel: def __init__(self, response: str) -> None: self.response = response def chat(self, **_: object) -> str: return self.response class FakeLlamaModel: def __init__(self, responses: list[str]) -> None: self.responses = responses self.calls = 0 def create_chat_completion(self, **_: object) -> dict: self.calls += 1 response = self.responses.pop(0) return {"choices": [{"message": {"content": response}}]} class MockMvpTest(unittest.TestCase): def tearDown(self) -> None: reset_text_runtime_fallbacks() def test_runtime_defaults_to_mock(self) -> None: settings = get_runtime_settings({}) status = runtime_status(settings) self.assertEqual(settings.vision_backend, "mock") self.assertEqual(settings.text_backend, "mock") self.assertEqual(status["vision"], "mock object understanding") self.assertEqual(status["runtime"], "no llama.cpp model connected yet") def test_llama_cpp_runtime_status_does_not_expose_model_path(self) -> None: status = runtime_status( get_runtime_settings( { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_PATH": "/Users/leo/private/model.gguf", } ) ) self.assertEqual(status["text"], "llama-cpp text generation") self.assertIn("[configured external GGUF]", status["runtime"]) self.assertNotIn("/Users/leo", status["runtime"]) def test_llama_cpp_hub_runtime_status_uses_public_repo_summary(self) -> None: settings = get_runtime_settings( { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_REPO_ID": "qqyule/objectverse-diary-qwen15b-lora", "TEXT_MODEL_FILENAME": "objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf", } ) status = runtime_status(settings) self.assertEqual(settings.text_model_repo_id, "qqyule/objectverse-diary-qwen15b-lora") self.assertEqual(settings.text_model_filename, "objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf") self.assertIn("Hub GGUF", status["runtime"]) self.assertIn("qqyule/objectverse-diary-qwen15b-lora", status["runtime"]) self.assertNotIn("/home", status["runtime"]) self.assertNotIn("/Users", status["runtime"]) def test_llama_cpp_loads_model_from_hub_config_when_path_is_missing(self) -> None: previous_model = llama_cpp_runner._LLAMA_MODEL previous_path = llama_cpp_runner._LLAMA_MODEL_PATH llama_cpp_runner._LLAMA_MODEL = None llama_cpp_runner._LLAMA_MODEL_PATH = None loaded_paths: list[str] = [] class FakeLlama: def __init__(self, *, model_path: str, **_: object) -> None: loaded_paths.append(model_path) fake_module = types.ModuleType("llama_cpp") fake_module.Llama = FakeLlama try: with tempfile.TemporaryDirectory() as tmp_dir: model_path = Path(tmp_dir) / "model.gguf" model_path.write_bytes(b"GGUF") settings = get_runtime_settings( { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_REPO_ID": "qqyule/objectverse-diary-qwen15b-lora", "TEXT_MODEL_FILENAME": "objectverse-diary-qwen15b-lora-v2-q4_k_m.gguf", } ) with ( patch.dict(sys.modules, {"llama_cpp": fake_module}), patch("src.models.llama_cpp_runner._download_hf_gguf", return_value=str(model_path)), ): llama_cpp_runner._load_llama_model("", settings=settings) self.assertEqual(loaded_paths, [str(model_path)]) finally: llama_cpp_runner._LLAMA_MODEL = previous_model llama_cpp_runner._LLAMA_MODEL_PATH = previous_path def test_examples_cover_six_objects(self) -> None: self.assertEqual(len(EXAMPLE_OBJECTS), 6) self.assertEqual(len(gradio_examples()), 6) self.assertTrue(all(len(example) == 1 for example in gradio_examples())) def test_sample_generation_cache_loads_committed_example_trace(self) -> None: path = sample_trace_path(0) result = load_sample_generation(0) self.assertIsNotNone(path) self.assertIsNotNone(result) assert result is not None self.assertEqual(result.trace.trace_id, "sample-01") self.assertEqual(result.object_understanding.object.name, "coffee mug") self.assertEqual(result.trace_path, str(path)) def test_mock_generation_flow(self) -> None: object_understanding = understand_object( None, "old white coffee mug on my developer desk", ) persona = generate_persona(object_understanding, "Cynical") diary = generate_diary(persona, "Cynical") share_card = render_share_card(persona, diary) self.assertEqual(object_understanding.object.name, "coffee mug") self.assertEqual(len(persona.persona.tags), 3) self.assertIn("Secret Diary", diary.title) self.assertIn("今天", diary.chinese) self.assertIn("objectverse-card", share_card) def test_llama_cpp_persona_diary_and_chat_accept_valid_json(self) -> None: env = { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_PATH": "/tmp/objectverse-text-model.gguf", } fake_llama = FakeLlamaModel( [ """ {"persona":{"object_name":"coffee mug","character_name":"Mugworth","mood":"dry and suspicious","secret_fear":"being left empty forever","core_memory":"It remembers every late-night refill.","complaint":"I am treated like a ceramic fuel tank.","tags":["desk witness","warm archive","quiet judgment"]}} """, """ {"title":"Secret Diary - Day 418","english":"Today I held another bitter storm and called it service.","chinese":"今天我又装下一场苦涩风暴,并被称为有用。"} """, """ {"reply":"Mugworth: I have seen your deadlines dissolve into coffee rings."} """, ] ) with ( patch.dict("os.environ", env, clear=False), patch("src.models.llama_cpp_runner._load_llama_model", return_value=fake_llama), ): object_understanding = understand_object(None, "white coffee mug") persona = generate_persona(object_understanding, "Cynical") diary = generate_diary(persona, "Cynical") reply = reply_as_object(persona.model_dump(mode="json"), "What did you see?") self.assertEqual(persona.persona.character_name, "Mugworth") self.assertEqual(diary.title, "Secret Diary - Day 418") self.assertIn("Mugworth", reply) def test_llama_cpp_missing_model_path_falls_back_to_mock(self) -> None: env = {"OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_PATH": ""} with patch.dict("os.environ", env, clear=False): result = generate_object_diary(None, "dusty black keyboard", "Philosopher", save=False) self.assertEqual(result.persona.persona.object_name, "keyboard") self.assertIn("text-fallback-to-mock", result.trace.fallbacks) self.assertIn("mock-vision-runtime", result.trace.fallbacks) self.assertNotIn("mock-text-runtime", result.trace.fallbacks) def test_llama_cpp_import_failure_falls_back_to_mock(self) -> None: env = { "OBJECTVERSE_TEXT_BACKEND": "llama_cpp", "TEXT_MODEL_PATH": "/tmp/objectverse-text-model.gguf", } with ( patch.dict("os.environ", env, clear=False), patch("src.models.llama_cpp_runner._load_llama_model", side_effect=ImportError("no llama_cpp")), ): result = generate_object_diary(None, "old white coffee mug", "Cynical", save=False) self.assertEqual(result.persona.persona.object_name, "coffee mug") self.assertIn("text-fallback-to-mock", result.trace.fallbacks) def test_llama_cpp_invalid_json_falls_back_to_mock(self) -> None: env = { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_PATH": "/tmp/objectverse-text-model.gguf", } with ( patch.dict("os.environ", env, clear=False), patch("src.models.llama_cpp_runner._load_llama_model", return_value=FakeLlamaModel(["not json"])), ): result = generate_object_diary(None, "old white coffee mug", "Cynical", save=False) self.assertEqual(result.persona.persona.object_name, "coffee mug") self.assertIn("text-fallback-to-mock", result.trace.fallbacks) self.assertEqual(result.trace.model_runtime["text"], "llama-cpp text generation") def test_pipeline_uses_combined_llama_cpp_persona_and_diary(self) -> None: env = { "OBJECTVERSE_TEXT_BACKEND": "llama-cpp", "TEXT_MODEL_PATH": "/tmp/objectverse-text-model.gguf", } fake_llama = FakeLlamaModel( [ """ { "persona": { "object_name": "coffee mug", "character_name": "Mugworth", "mood": "dry and suspicious", "secret_fear": "being left empty forever", "core_memory": "It remembers every late-night refill.", "complaint": "I am treated like a ceramic fuel tank.", "tags": ["desk witness", "warm archive", "quiet judgment"] }, "diary": { "title": "Secret Diary - Day 418", "english": "Today I held another bitter storm and called it service.", "chinese": "今天我又装下一场苦涩风暴,并被称为有用。" } } """, ] ) with ( patch.dict("os.environ", env, clear=False), patch("src.models.llama_cpp_runner._load_llama_model", return_value=fake_llama), ): result = generate_object_diary(None, "old white coffee mug", "Cynical", save=False) self.assertEqual(result.persona.persona.character_name, "Mugworth") self.assertEqual(result.diary.title, "Secret Diary - Day 418") self.assertEqual(fake_llama.calls, 1) self.assertNotIn("text-fallback-to-mock", result.trace.fallbacks) def test_minicpm_vision_backend_accepts_valid_json(self) -> None: response = """ {"object":{"name":"coffee mug","visible_features":["white ceramic","round handle","desk shadow"],"likely_context":"work desk","confidence":0.88}} """ settings = get_runtime_settings( { "OBJECTVERSE_VISION_BACKEND": "minicpm-v", "VISION_MODEL_ID": "openbmb/MiniCPM-V-2_6", "OBJECTVERSE_TEXT_BACKEND": "mock", } ) with ( patch("src.models.vision_runner._load_rgb_image", return_value=object()), patch("src.models.vision_runner._load_minicpm_components", return_value=(FakeMiniCpmModel(response), object())), ): result = understand_object_with_metadata("/tmp/mug.png", "white mug", settings=settings) self.assertEqual(result.object_understanding.object.name, "coffee mug") self.assertEqual(result.object_understanding.object.confidence, 0.88) self.assertEqual(result.fallbacks, []) def test_minicpm_vision_backend_falls_back_on_invalid_json(self) -> None: settings = get_runtime_settings( { "OBJECTVERSE_VISION_BACKEND": "minicpm-v", "VISION_MODEL_ID": "openbmb/MiniCPM-V-2_6", "OBJECTVERSE_TEXT_BACKEND": "mock", } ) with ( patch("src.models.vision_runner._load_rgb_image", return_value=object()), patch("src.models.vision_runner._load_minicpm_components", return_value=(FakeMiniCpmModel("not json"), object())), ): result = understand_object_with_metadata("/tmp/keyboard.png", "dusty black keyboard", settings=settings) self.assertEqual(result.object_understanding.object.name, "keyboard") self.assertEqual(result.fallbacks, ["vision-fallback-to-mock"]) def test_vision_runtime_probe_redacts_sensitive_error_markers(self) -> None: settings = get_runtime_settings( { "OBJECTVERSE_VISION_BACKEND": "minicpm-v", "VISION_MODEL_ID": "openbmb/MiniCPM-V-2_6", } ) with patch( "src.models.vision_runner._load_minicpm_components", side_effect=RuntimeError("failed with token hf_forbidden in /Users/leo/.env"), ): probe = probe_vision_runtime(settings=settings, load_model=True) serialized = json.dumps(probe, ensure_ascii=False) self.assertTrue(probe["minicpm_load_attempted"]) self.assertFalse(probe["minicpm_load_ok"]) self.assertNotIn("hf_", serialized) self.assertNotIn("HF_TOKEN", serialized) self.assertNotIn("/Users/leo", serialized) self.assertNotIn(".env", serialized) def test_hidden_vision_runtime_probe_returns_safe_json(self) -> None: probe = vision_runtime_probe() serialized = json.dumps(probe, ensure_ascii=False) self.assertIn("backend", probe) self.assertIn("torch_import", probe) self.assertNotIn("hf_", serialized) self.assertNotIn("HF_TOKEN", serialized) def test_pipeline_saves_generation_result(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: result = generate_object_diary( None, "old white coffee mug on my developer desk", "Cynical", trace_dir=Path(tmp_dir), ) saved_path = Path(result.trace_path) self.assertTrue(saved_path.exists()) self.assertEqual(result.object_understanding.object.name, "coffee mug") self.assertEqual(saved_path.stem, result.trace.trace_id) def test_pipeline_records_minicpm_vision_runtime(self) -> None: response = """ {"object":{"name":"desk lamp","visible_features":["metal shade","thin neck","warm light"],"likely_context":"desk","confidence":0.91}} """ env = { "OBJECTVERSE_VISION_BACKEND": "minicpm-v", "VISION_MODEL_ID": "openbmb/MiniCPM-V-2_6", "OBJECTVERSE_TEXT_BACKEND": "mock", } with ( patch.dict("os.environ", env, clear=False), patch("src.models.vision_runner._load_rgb_image", return_value=object()), patch("src.models.vision_runner._load_minicpm_components", return_value=(FakeMiniCpmModel(response), object())), ): result = generate_object_diary("/tmp/lamp.png", "desk lamp", "Dramatic", save=False) self.assertEqual(result.object_understanding.object.name, "desk lamp") self.assertEqual(result.trace.model_runtime["vision"], "minicpm-v object understanding") self.assertIn("mock-text-runtime", result.trace.fallbacks) self.assertNotIn("mock-runtime", result.trace.fallbacks) def test_chat_uses_current_persona(self) -> None: object_understanding = understand_object(None, "dusty black mechanical keyboard") persona = generate_persona(object_understanding, "Philosopher") reply = reply_as_object(persona.model_dump(mode="json"), "What have you seen?") self.assertIn(persona.persona.character_name, reply) self.assertIn("keyboard", reply) def test_trace_save_and_anonymization(self) -> None: description = "old phone from user@example.com with serial 123456789" object_understanding = understand_object(None, description) persona = generate_persona(object_understanding, "Lonely") diary = generate_diary(persona, "Lonely") trace = build_trace(None, description, "Lonely", object_understanding, persona, diary) self.assertIn("[redacted-email]", anonymize_text(description)) self.assertIn("[redacted-number]", trace.input["description"]) self.assertIn("mock-runtime", trace.fallbacks) with tempfile.TemporaryDirectory() as tmp_dir: path = Path(save_trace(trace, Path(tmp_dir))) saved = json.loads(path.read_text(encoding="utf-8")) self.assertEqual(saved["trace_id"], trace.trace_id) self.assertEqual(saved["model_runtime"]["vision"], "mock object understanding") def test_generate_sample_traces_script(self) -> None: with tempfile.TemporaryDirectory() as tmp_dir: paths = generate_sample_traces(Path(tmp_dir)) saved = [json.loads(path.read_text(encoding="utf-8")) for path in paths] self.assertEqual(len(paths), 6) self.assertEqual(saved[0]["trace_id"], "sample-01") self.assertTrue(all(item["fallbacks"] == ["mock-runtime"] for item in saved)) def test_initial_stage_acceptance_checks(self) -> None: results = run_checks() self.assertIn("required files exist", results) self.assertIn("six sample traces validate", results) self.assertIn("Gradio Blocks app builds", results) if __name__ == "__main__": unittest.main()