"""Hopper failures must not become successful negative lookups.""" import asyncio from importlib import import_module from types import SimpleNamespace from unittest.mock import AsyncMock, Mock import pytest from artemis.agents.hopper.hopper import HopperError, HopperOutput module = import_module("artemis.agents.hopper.hopper") @pytest.mark.asyncio @pytest.mark.parametrize("invalid", [None, {}, {"found": True, "output": " ", "reason": "Empty"}]) async def test_invalid_extraction_switches_to_fallback(monkeypatch, invalid): good = HopperOutput(found=True, output="com.example.app", reason="Matched") primary = SimpleNamespace(ainvoke=AsyncMock(return_value=invalid)) secondary = SimpleNamespace(ainvoke=AsyncMock(return_value=good)) factory = Mock( side_effect=[ SimpleNamespace(with_structured_output=lambda _: primary), SimpleNamespace(with_structured_output=lambda _: secondary), ] ) monkeypatch.setattr(module, "get_llm", factory) assert ( await module.hopper(SimpleNamespace(data_engine=None), "Find app", "com.example.app") == good ) assert primary.ainvoke.await_count == secondary.ainvoke.await_count == 1 @pytest.mark.asyncio @pytest.mark.parametrize("use_fallback", [False, True]) async def test_exhausted_extraction_raises_a_distinct_error(monkeypatch, use_fallback): model = SimpleNamespace(ainvoke=AsyncMock(return_value=None)) monkeypatch.setattr( module, "get_llm", lambda **_: SimpleNamespace(with_structured_output=lambda _: model) ) with pytest.raises(HopperError, match="模型未能完成数据提取"): await module.hopper( SimpleNamespace(data_engine=None), "Find app", "packages", use_fallback=use_fallback ) assert model.ainvoke.await_count == (2 if use_fallback else 1) @pytest.mark.asyncio async def test_cancelled_extraction_never_uses_fallback(monkeypatch): model = SimpleNamespace(ainvoke=AsyncMock(side_effect=asyncio.CancelledError)) monkeypatch.setattr( module, "get_llm", lambda **_: SimpleNamespace(with_structured_output=lambda _: model) ) with pytest.raises(asyncio.CancelledError): await module.hopper(SimpleNamespace(data_engine=None), "Find app", "packages") assert model.ainvoke.await_count == 1 @pytest.mark.asyncio async def test_model_setup_failure_is_not_reported_as_missing_data(monkeypatch): monkeypatch.setattr(module, "get_llm", Mock(side_effect=ValueError("invalid model settings"))) with pytest.raises(HopperError, match="模型未能完成数据提取"): await module.hopper(SimpleNamespace(data_engine=None), "Find app", "packages")