import pytest from app.algorithm_ipc.schema import WhitelistSpec from app.algorithm_runner.actionformer_gated.runner import ActionFormerSegmentRecord from app.algorithm_runner.segment_policy import ( events_for_tear_record, pad_ranked_candidates_for_voice, rank_topk_for_candidates, ) from app.domain.vision_prediction import PredictionCandidate from app.services.voice_confirm import build_prompt_text def test_rank_preserves_model_order_within_whitelist() -> None: """清单顺序可能与模型置信度顺序不同;播报「第n个」应与模型 top1/top2 一致。""" topk = [ PredictionCandidate(label="缝线", confidence=0.9), PredictionCandidate(label="纱布", confidence=0.5), ] ordered = ["纱布", "缝线"] ranked = rank_topk_for_candidates(topk, ordered) assert [c.label for c in ranked] == ["缝线", "纱布"] def test_rank_without_candidates_keeps_model_order() -> None: topk = [ PredictionCandidate(label="a", confidence=0.9), PredictionCandidate(label="b", confidence=0.5), ] ranked = rank_topk_for_candidates(topk, []) assert [c.label for c in ranked] == ["a", "b"] def test_rank_drops_labels_not_on_surgery_whitelist() -> None: topk = [ PredictionCandidate(label="缝线", confidence=0.9), PredictionCandidate(label="陌生类", confidence=0.5), PredictionCandidate(label="纱布", confidence=0.1), ] ordered = ["纱布", "缝线"] ranked = rank_topk_for_candidates(topk, ordered) assert [c.label for c in ranked] == ["缝线", "纱布"] def test_pad_voice_options_fills_from_surgery_list() -> None: ranked = [PredictionCandidate(label="手套", confidence=0.88)] order = ["手套", "纱布", "缝线", "钳子"] out = pad_ranked_candidates_for_voice(ranked, order) assert [c.label for c in out] == ["手套", "纱布", "缝线"] assert out[0].confidence == pytest.approx(0.88) assert out[1].confidence == 0.0 and out[2].confidence == 0.0 def test_pad_voice_respects_max_options() -> None: out = pad_ranked_candidates_for_voice([], ["a", "b", "c"], max_options=2) assert [c.label for c in out] == ["a", "b"] def test_events_for_tear_record_voice_has_three_options(monkeypatch: pytest.MonkeyPatch) -> None: from app.baked import pipeline as p monkeypatch.setattr(p, "VOICE_CONFIRMATION_ENABLED", True, raising=False) monkeypatch.setattr(p, "VIDEO_AUTO_CONFIRM_CONFIDENCE", 0.99, raising=False) monkeypatch.setattr(p, "VIDEO_VOICE_CONFIRM_MIN_CONFIDENCE", 0.1, raising=False) wl = WhitelistSpec( candidate_consumables=["手套", "纱布", "缝线"], name_to_id={"手套": "g1", "纱布": "g2", "缝线": "g3"}, ) rec = ActionFormerSegmentRecord( segment_index=1, start_sec=0.0, end_sec=2.0, mid_stream_sec=1.0, item_id="g1", item_name="手套", top1_conf=0.55, top2_name="", top2_conf=0.0, top3_name="", top3_conf=0.0, majority_ref="", ) evs = events_for_tear_record( rec, wl=wl, camera_id="cam", wall_lo=100.0, wall_hi=102.0, cooldown_key="k", ) assert len(evs) == 1 assert evs[0]["type"] == "needs_voice_confirm" opts = evs[0]["options"] assert len(opts) == 3 assert [o["label"] for o in opts] == ["手套", "纱布", "缝线"] text = build_prompt_text([(str(o["label"]), float(o["confidence"])) for o in opts]) assert "第1个,手套。" in text assert "第2个,纱布。" in text assert "第3个,缝线。" in text