From c1565a01c2df36bd4b7d1dca53718dd2cb75b341 Mon Sep 17 00:00:00 2001 From: iomgaa Date: Tue, 14 Jul 2026 16:33:09 -0400 Subject: [PATCH] test: assert stage/verdict/pair_id and Q-refusal flip_skipped path --- tests/unit/test_adversarial_flip_gate.py | 90 ++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_adversarial_flip_gate.py b/tests/unit/test_adversarial_flip_gate.py index bb8baaa..8877855 100644 --- a/tests/unit/test_adversarial_flip_gate.py +++ b/tests/unit/test_adversarial_flip_gate.py @@ -131,10 +131,20 @@ async def test_flip_gate_answer_flips_passed(tmp_path): "SELECT verdict FROM adversarial_verdicts WHERE question_id='hard' AND stage='cheat'" ).fetchone()[0] assert cheat == "passed" - mrow = store._conn.execute( - "SELECT verdict, pair_id FROM adversarial_verdicts WHERE stage='flip_mirror'" - ).fetchone() - assert mrow[0] == "passed" and mrow[1] # 镜像独立行 + pair_id 非空 + # flip_original + flip_mirror 两条独立 stage 行,共用同一 pair_id(非空),verdict=passed + by_stage = { + r[0]: (r[1], r[2]) + for r in store._conn.execute( + "SELECT stage, verdict, pair_id FROM adversarial_verdicts " + "WHERE question_id='hard' AND stage IN ('flip_original', 'flip_mirror')" + ) + } + assert set(by_stage) == {"flip_original", "flip_mirror"} + assert by_stage["flip_original"][0] == "passed" + assert by_stage["flip_mirror"][0] == "passed" + assert by_stage["flip_original"][1] # pair_id 非空 + assert by_stage["flip_original"][1] == by_stage["flip_mirror"][1] # 共用同一 pair_id + assert "hard" in store.final_passed_question_ids({"hard": question_hash(q)}, _fp()) store.close() @@ -172,6 +182,23 @@ async def test_flip_gate_same_answer_filtered(tmp_path): "SELECT verdict FROM adversarial_verdicts WHERE question_id='stick' AND stage='cheat'" ).fetchone()[0] assert cheat == "filtered_no_flip" # cheat 行被改写 → final 不含它 + # flip_original + flip_mirror 两条独立 stage 行,共用同一 pair_id(非空),verdict 一致 + by_stage = { + r[0]: (r[1], r[2]) + for r in store._conn.execute( + "SELECT stage, verdict, pair_id FROM adversarial_verdicts " + "WHERE question_id='stick' AND stage IN ('flip_original', 'flip_mirror')" + ) + } + assert set(by_stage) == {"flip_original", "flip_mirror"} + assert by_stage["flip_original"][0] == "filtered_no_flip" + assert by_stage["flip_mirror"][0] == "filtered_no_flip" + assert by_stage["flip_original"][1] # pair_id 非空 + assert by_stage["flip_original"][1] == by_stage["flip_mirror"][1] # 共用同一 pair_id + # 改写后的 cheat 行不再计入终判 passed(双保险:cheat 改判 + filtered_no_flip 排除) + assert "stick" not in store.final_passed_question_ids( + {"stick": question_hash(q)}, _fp() + ) store.close() @@ -202,6 +229,61 @@ async def test_flip_gate_invalid_mirror_skipped_but_kept(tmp_path): store.close() +@pytest.mark.asyncio +async def test_flip_gate_mirror_ok_but_agent_refuses_skipped(tmp_path): + """镜像生成成功但 agent 对镜像 Q 拒答(None) → flip_skipped,survivor 保留不误杀。 + + 与 invalid_mirror 用例互补:此处镜像题**造得出来**(走到 agent),只因 agent 无 + 有效预测→canonical(Q)=None→保守 flip_skipped,绝不据此误杀原题。 + """ + store = QuestionGenStore(str(tmp_path / "q.db")) + q = _q("refuse") + _preset_cheat(store, q, pred="A") # P canonical="蒸" + agent = _FakeAgent({}) # 镜像题无预测 → predict 返回 None(拒答) + vlm = _FakeVLM( + json.dumps( + { + "mirror": { + "question": "X 之后?", + "options": ["A. 炒", "B. 蒸", "C. 煮", "D. 炸"], + "answer": "A", + } + }, + ensure_ascii=False, + ) + ) + kept = await run_flip_gate( + [q], + agent=agent, + vlm=vlm, + store=store, + trees={"v1": object()}, + config=AdversarialFilterConfig(), + round_no=0, + run_id="r0", + session_id="s", + ) + assert {x.question_id for x in kept} == {"refuse"} # 保守保留,不误杀 + assert agent.calls == ["refuse_mirror"] # 镜像被跑一次,C2:原题 P 未被重跑 + by_stage = { + r[0]: r[1] + for r in store._conn.execute( + "SELECT stage, verdict FROM adversarial_verdicts " + "WHERE question_id='refuse' AND stage IN ('flip_original', 'flip_mirror')" + ) + } + assert by_stage == {"flip_original": "flip_skipped", "flip_mirror": "flip_skipped"} + cheat = store._conn.execute( + "SELECT verdict FROM adversarial_verdicts WHERE question_id='refuse' AND stage='cheat'" + ).fetchone()[0] + assert cheat == "passed" # cheat 行保持 passed(保留只经作弊门) + # flip_skipped 不算 filtered_no_flip → 仍计入终判 passed(保留题最终可入库) + assert "refuse" in store.final_passed_question_ids( + {"refuse": question_hash(q)}, _fp() + ) + store.close() + + @pytest.mark.asyncio async def test_flip_gate_mirror_excluded_and_unsupported_passes(tmp_path): store = QuestionGenStore(str(tmp_path / "q.db"))