Spaces:
Running
Running
File size: 7,723 Bytes
bb59211 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 | """「数据不对 → 卡在人工审核」的行为锁。
盯住三件事,每一件坏掉都是**静默**的(不报错、流程照常走完):
1. 判 FAIL/ERROR 的批次被推回 ``to_review``,且清掉 ``approved_at``
2. WARN 只记录、不拦 —— 否则警告阈值一响,整批数据全卡住
3. **自动批准路径主动让路** —— AI 标注 / 视频门禁各看各的报告,不看质检
结论;不查那个粘性标记的话,它们会在质检推回 to_review 之后又把状态
置成 reviewed,坏数据照样被放行
全部在临时存储根上跑(monkeypatch ``localstore`` 的路径常量),不碰真实数据。
pytest tests/test_cleaning_gate.py
python3 tests/test_cleaning_gate.py
"""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parents[1])) # noqa: E402
import pytest # noqa: E402
from app import localstore # noqa: E402
from app.processing.cleaning import store # noqa: E402
from app.processing.cleaning import contract as C # noqa: E402
@pytest.fixture
def sandbox(tmp_path, monkeypatch):
"""把 episode state 落到临时目录 —— 别碰 ~/.cache/egodata-data。"""
states = tmp_path / "state" / "episode_states"
states.mkdir(parents=True, exist_ok=True)
monkeypatch.setattr(localstore, "STATE_ROOT", tmp_path / "state")
monkeypatch.setattr(localstore, "EPISODE_STATES_DIR", states)
localstore._episode_state_cache.clear()
yield states
localstore._episode_state_cache.clear()
def _report(status, *, ranges=0):
"""造一份最小报告 —— 只关心 ``episode.status``,那是拦截判据。"""
return {
"schema_version": 2,
"episode_id": "E1",
"computed_at": "2026-09-24T00:00:00+00:00",
"passed": status == C.PASS,
"streams": [],
"ranges": [{}] * ranges,
"episode": {
"status": status,
"status_label": C.SEVERITY_LABELS.get(status, status),
"failed_streams": [], "warn_streams": [], "pending_streams": [],
"summary": "test",
},
"findings": [],
}
def _seed(states, status="to_review", approved_at=None):
import json
eid = "E1"
(states / f"{eid}.json").write_text(json.dumps({
"id": eid, "name": eid, "project": "P", "status": status,
"approved_at": approved_at,
}, ensure_ascii=False), encoding="utf-8")
localstore._episode_state_cache.pop(eid, None)
return eid
# ── 拦截 ────────────────────────────────────────────────────
@pytest.mark.parametrize("status", [C.FAIL, C.ERROR])
def test_failure_pushes_back_to_review(sandbox, status):
eid = _seed(sandbox, status="reviewed", approved_at="2026-01-01T00:00:00Z")
assert store.save_report(eid, _report(status)) is True
state = localstore.read_episode_state(eid)
assert state["status"] == "to_review" # 自动批准被撤销
assert state["approved_at"] is None # 批准时间一起清掉
assert store.is_approval_blocked(state) is True
@pytest.mark.parametrize("status", [C.WARN, C.PASS, C.PENDING])
def test_non_failure_does_not_block(sandbox, status):
"""WARN/PENDING 只是警告 —— 拦的话整批数据都会卡住。"""
eid = _seed(sandbox, status="reviewed")
store.save_report(eid, _report(status))
state = localstore.read_episode_state(eid)
assert state["status"] == "reviewed" # 状态没被动过
assert store.is_approval_blocked(state) is False
def test_does_not_resurrect_deleted_episode(sandbox):
"""质检不能把用户删掉/否决过的批次复活。"""
for status in ("deleted", "rejected"):
eid = _seed(sandbox, status=status)
store.save_report(eid, _report(C.FAIL))
assert localstore.read_episode_state(eid)["status"] == status
def test_report_still_written_when_not_blocking(sandbox):
"""不拦也得把报告写进去 —— 拦截不是写报告的前提。"""
eid = _seed(sandbox)
store.save_report(eid, _report(C.WARN, ranges=3))
summary = store.load_summary(eid)
assert summary["status"] == C.WARN and summary["ranges"] == 3
# ── 自动批准让路 ────────────────────────────────────────────
def _quality_gate(state):
"""转调 ai_annotation 的真实写入路径,避免测试复制一份逻辑。"""
from app.ai_annotation import _set_ai_quality_state
return _set_ai_quality_state
def test_ai_gate_refuses_to_auto_approve_when_blocked(sandbox):
"""★ 核心用例:质检判 FAIL 后,AI 标注门禁即使自己通过也不得自动批准。"""
eid = _seed(sandbox, status="to_review")
store.save_report(eid, _report(C.FAIL))
_quality_gate(None)(eid, {"passed": True}, auto_approve=True)
state = localstore.read_episode_state(eid)
assert state["status"] == "to_review", "坏数据被 AI 门禁放行了"
assert state["ai_quality_status"] == "passed" # 它自己的结论照常记录
def test_ai_gate_still_auto_approves_when_not_blocked(sandbox):
"""别把拦截修成"自动批准彻底失效" —— 干净数据该自动过。"""
eid = _seed(sandbox, status="to_review")
store.save_report(eid, _report(C.PASS))
_quality_gate(None)(eid, {"passed": True}, auto_approve=True)
assert localstore.read_episode_state(eid)["status"] == "reviewed"
def test_video_gate_refuses_to_auto_approve_when_blocked(sandbox):
"""视频干净 ≠ 数据干净 —— 视频门禁同样要让路。"""
from app.video_quality import _apply_video_quality_result
eid = _seed(sandbox, status="to_review")
store.save_report(eid, _report(C.FAIL))
_apply_video_quality_result(eid, {"passed": True})
state = localstore.read_episode_state(eid)
assert state["status"] == "to_review", "坏数据被视频门禁放行了"
assert state["video_quality_status"] == "passed"
# ── 标记的粘性 ──────────────────────────────────────────────
def test_block_clears_once_data_is_fixed(sandbox):
"""数据修好后重跑要能自动批准 —— 否则卡住就再也出不来了。
注意 ``save_report`` **只拦不批**:它解封后不会自己把状态改回 reviewed,
批准是 AI/视频门禁或人工的事。所以这里走一遍真实的"修好 → 重跑"顺序:
worker 先置 to_review,质检复核(清除标记),随后门禁自动批准。
"""
eid = _seed(sandbox, status="to_review")
store.save_report(eid, _report(C.FAIL))
assert store.is_approval_blocked(localstore.read_episode_state(eid)) is True
# —— 重跑 ——
localstore.set_episode_status(eid, "to_review") # worker 完成回调做的
store.save_report(eid, _report(C.PASS)) # 质检复核:数据已修好
state = localstore.read_episode_state(eid)
assert store.is_approval_blocked(state) is False
_quality_gate(None)(eid, {"passed": True}, auto_approve=True)
assert localstore.read_episode_state(eid)["status"] == "reviewed"
def test_save_report_never_approves(sandbox):
"""质检**只拦不批** —— 它不该越过门禁自己把批次标成通过。"""
eid = _seed(sandbox, status="to_review")
store.save_report(eid, _report(C.PASS))
assert localstore.read_episode_state(eid)["status"] == "to_review"
if __name__ == "__main__":
raise SystemExit(pytest.main([__file__, "-q"]))
|