import asyncio import httpx from app import video class _FakeResponse: def __init__(self, body): self.body = body def raise_for_status(self): return None def json(self): return self.body class _FakeClient: responses = [] payloads = [] def __init__(self, **_): pass async def __aenter__(self): return self async def __aexit__(self, *_): return None async def post(self, _, json): self.payloads.append(json) return _FakeResponse(self.responses.pop(0)) def test_vision_request_retries_empty_content(monkeypatch): _FakeClient.responses = [ { "done_reason": "length", "eval_count": 1024, "message": {"content": "", "thinking": "reasoning"}, }, { "done_reason": "stop", "eval_count": 20, "message": {"content": '{"passed": true}', "thinking": ""}, }, ] _FakeClient.payloads = [] monkeypatch.setattr(httpx, "AsyncClient", _FakeClient) payload = { "think": False, "messages": [{"role": "user", "content": "Analyze"}], "options": {"num_predict": 1024}, } content, metadata = asyncio.run(video._request_vision_model(payload)) assert content == '{"passed": true}' assert metadata["doneReason"] == "stop" assert len(_FakeClient.payloads) == 2 assert _FakeClient.payloads[0]["think"] is False assert _FakeClient.payloads[1]["options"]["num_predict"] == 1536 def test_fast_result_quality_controls_accurate_fallback(): complete = { "passed": True, "conclusion": "通过", "summary": "状态正常", "events": [], "warnings": [], } assert video._fallback_reason(complete, True) is None assert video._fallback_reason({**complete, "passed": None}, True) == "FAST_RESULT_INSUFFICIENT" assert video._fallback_reason({**complete, "passed": None}, False) is None assert video._fallback_reason({"summary": "缺少字段"}, True).startswith( "FAST_RESULT_MISSING_FIELDS:" ) def test_auto_mode_falls_back_to_accurate_result(monkeypatch, tmp_path): frame_dir = tmp_path / "frames" frame_dir.mkdir() frame = frame_dir / "frame-001.jpg" frame.write_bytes(b"frame") calls = [] monkeypatch.setattr( video, "_extract_frames", lambda *_: ([frame], 2.0, frame_dir), ) async def fake_analyze(_, mode, *args): calls.append(mode) if mode == "FAST": return ( { "passed": None, "conclusion": "证据不足", "summary": "快速分析无法判断", "events": [], "warnings": [], }, {"mode": "FAST", "model": "fast", "sampledFrameCount": 1, "maximumWidth": 896}, ) return ( { "passed": True, "conclusion": "通过", "summary": "精确分析确认通过", "events": [], "warnings": [], }, {"mode": "ACCURATE", "model": "accurate", "sampledFrameCount": 1, "maximumWidth": 1280}, ) monkeypatch.setattr(video, "_analyze_frames", fake_analyze) profile_dir = tmp_path / "profile" profile_dir.mkdir() (profile_dir / "prompt.txt").write_text("Analyze", encoding="utf-8") profile = video.Profile( "test.video.v1", "VIDEO_ANALYSIS", profile_dir, {"fallbackWhenPassedUnknown": True}, ) result, _, metadata = asyncio.run(video.analyze_video(profile, tmp_path / "video.mp4")) assert calls == ["FAST", "ACCURATE"] assert result["passed"] is True assert metadata["fallback"] is True assert metadata["fallbackReason"] == "FAST_RESULT_INSUFFICIENT"