from __future__ import annotations import tempfile import unittest from pathlib import Path from core.deployment import DeploymentPolicy from datasets.field_notes import FieldNote, FieldNoteStore from plant.app import build_app, load_config from plant.plant_loader import ( FieldNotesPlantExporter, LocalFolderLoader, SpeciesIndexBuilder, demo_species, ) from plant.plant_service import DemoPlantVisionService, extract_json_object, parse_plant_response from plant.plant_tab import ( identify_plant_callback, plant_training_plan, render_result_card, save_field_note_callback, species_table, ) from plant.plant_tools import dataset_stats, search_species, set_services, training_plan from plant.training import build_plant_training_plan, write_llamafactory_dataset_info class PlantReferenceAppTest(unittest.TestCase): def test_plant_config_loads_domain_and_model(self) -> None: config = load_config("plant/models.yaml") self.assertEqual(config["domain"]["name"], "plant_discovery") self.assertEqual(config["models"]["plant_vlm"]["hf_id"], "openbmb/MiniCPM-V-4.6") def test_no_model_app_builds_gradio_blocks(self) -> None: demo = build_app(no_model=True) self.assertEqual(type(demo).__name__, "Blocks") def test_space_policy_rejects_no_model_mode(self) -> None: import plant.app as plant_app original = plant_app.current_policy plant_app.current_policy = lambda: DeploymentPolicy("space") # type: ignore[assignment] try: with self.assertRaises(ValueError): build_app(no_model=True) finally: plant_app.current_policy = original # type: ignore[assignment] def test_default_app_builds_openbmb_service_without_loading_weights(self) -> None: demo = build_app() self.assertEqual(type(demo).__name__, "Blocks") def test_demo_service_returns_structured_plant_result(self) -> None: result = DemoPlantVisionService().identify(object(), force_thinking=True) self.assertEqual(result.latin_name, "Bellis perennis") self.assertGreater(result.confidence, 0.8) self.assertEqual(result.to_dict()["family"], "Asteraceae") def test_demo_service_reports_no_llm_usage(self) -> None: status = DemoPlantVisionService().service_status() self.assertFalse(status["uses_llm"]) self.assertEqual(status["mode"], "demo") def test_extract_json_repairs_common_model_wrapping(self) -> None: parsed = extract_json_object('```json\n{"latin_name":"Rosa canina",}\n```') self.assertEqual(parsed["latin_name"], "Rosa canina") def test_parse_plant_response_builds_schema(self) -> None: result = parse_plant_response( '{"common_name":"Oak","latin_name":"Quercus robur","confidence":0.91}', model_used="demo", ) self.assertEqual(result.genus, "Quercus") self.assertEqual(result.model_used, "demo") def test_local_folder_loader_uses_species_folder_names(self) -> None: with tempfile.TemporaryDirectory() as tmp: species_dir = Path(tmp) / "Acer_palmatum" species_dir.mkdir() (species_dir / "leaf.jpg").write_bytes(b"not a real image but good metadata") loader = LocalFolderLoader(tmp) self.assertEqual(loader.species_list(), ["Acer palmatum"]) self.assertEqual(loader.iter_records()[0].latin_name, "Acer palmatum") def test_species_index_falls_back_to_demo_species(self) -> None: index = SpeciesIndexBuilder(root="missing-plant-root").build({"datasets": {}}) self.assertIn("Bellis perennis", index) def test_species_table_filters_by_query_and_family(self) -> None: rows = species_table(demo_species(), "rose", "Rosaceae") self.assertEqual(rows[0][0], "Rosa canina") def test_render_result_card_escapes_model_text(self) -> None: html = render_result_card( { "common_name": "", "latin_name": "Bellis perennis", "confidence": 0.5, } ) self.assertIn("<script>", html) self.assertNotIn("