ObjectverseDiary / tests /test_mock_mvp.py
qqyule's picture
Deploy Hub GGUF downloader runtime
c45600f verified
"""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()