Files
AIRegulation-DocAnalysis/backend/tests/perception/test_regulation_differ.py
T
2026-08-06 11:08:46 +08:00

158 lines
6.1 KiB
Python

"""Tests for the deterministic regulation differ.
These tests pin the behaviour that the previous cosine-similarity implementation
could not deliver: real regulatory edits (numeric limits, deontic modals) must be
detected, and inserting a paragraph must not report unrelated paragraphs as
changed. Everything here runs offline — no LLM, no network, no embeddings.
"""
from __future__ import annotations
from app.infrastructure.perception.regulation_differ import RegulationDiffer
def _differ() -> RegulationDiffer:
"""Build a differ with an explicit ratio so tests never depend on .env."""
return RegulationDiffer(min_change_ratio=0.02)
def test_identical_documents_report_no_changes():
"""An unchanged regulation must produce an empty change list."""
text = "第一条 车辆制动系统应在时速50公里条件下于30米内完全停止。\n第二条 驾驶员座椅面料的阻燃性能应符合附录B的规定。"
assert _differ().diff(text, text) == []
def test_numeric_tightening_is_detected():
"""A changed numeric limit is the case cosine similarity scored 0.9153 and missed."""
old = "第一条 车辆制动系统应在时速50公里条件下于30米内完全停止。"
new = "第一条 车辆制动系统应在时速50公里条件下于20米内完全停止。"
changes = _differ().diff(old, new)
assert len(changes) == 1
change = changes[0]
assert change.change_type == "modified"
assert change.numeric_changed is True
assert change.needs_llm is True
def test_deontic_relaxation_is_detected():
"""Weakening 应当 to 宜 changes the legal force and must be flagged."""
old = "第三条 生产企业应当每年开展一次安全评估。"
new = "第三条 生产企业宜每年开展一次安全评估。"
changes = _differ().diff(old, new)
assert len(changes) == 1
assert changes[0].deontic_changed is True
assert changes[0].needs_llm is True
def test_prohibition_removal_is_detected():
"""Dropping 不得 flips a prohibition into a permission."""
old = "第四条 车辆不得使用未经认证的电池组。"
new = "第四条 车辆可以使用经备案的电池组。"
changes = _differ().diff(old, new)
assert len(changes) == 1
assert changes[0].deontic_changed is True
def test_inserted_paragraph_does_not_shift_the_rest():
"""Regression test for positional alignment.
The previous implementation compared old[i] to new[i], so inserting one
paragraph at the top reported every following paragraph as changed. With
sequence alignment only the inserted paragraph is new.
"""
old = "\n".join([
"第一条 本标准规定了车辆制动系统的技术要求。",
"第二条 车辆制动系统应在时速50公里条件下于30米内完全停止。",
"第三条 驾驶员座椅面料的阻燃性能应符合附录B的规定。",
])
new = "\n".join([
"第零条 本标准适用于所有M1类车辆。",
"第一条 本标准规定了车辆制动系统的技术要求。",
"第二条 车辆制动系统应在时速50公里条件下于30米内完全停止。",
"第三条 驾驶员座椅面料的阻燃性能应符合附录B的规定。",
])
changes = _differ().diff(old, new)
assert len(changes) == 1, f"expected only the inserted paragraph, got {changes}"
assert changes[0].change_type == "added"
assert "第零条" in changes[0].new_text
assert changes[0].old_text == ""
def test_deleted_paragraph_is_reported_once():
"""Removing a provision yields exactly one 'removed' record."""
old = "\n".join([
"第一条 本标准规定了车辆制动系统的技术要求。",
"第二条 车辆制动系统应在时速50公里条件下于30米内完全停止。",
"第三条 驾驶员座椅面料的阻燃性能应符合附录B的规定。",
])
new = "\n".join([
"第一条 本标准规定了车辆制动系统的技术要求。",
"第三条 驾驶员座椅面料的阻燃性能应符合附录B的规定。",
])
changes = _differ().diff(old, new)
assert len(changes) == 1
assert changes[0].change_type == "removed"
assert "第二条" in changes[0].old_text
assert changes[0].new_text == ""
assert changes[0].needs_llm is True
def test_trivial_edit_is_not_sent_to_the_llm():
"""A cosmetic edit with no number or modal change must not cost an LLM call."""
old = "第五条 本标准由全国汽车标准化技术委员会归口管理。"
new = "第五条 本标准由全国汽车标准化技术委员会归口管理"
changes = _differ().diff(old, new)
for change in changes:
assert change.numeric_changed is False
assert change.deontic_changed is False
assert change.needs_llm is False, f"trivial edit was gated to the LLM: {change}"
def test_large_rewrite_is_sent_to_the_llm():
"""A substantial rewrite clears the change-ratio gate even without numbers or modals."""
old = "第六条 本标准参考了国际同类标准的相关内容。"
new = "第六条 本条款描述了完全不同的主题内容,涉及整车认证流程与型式试验的组织安排。"
changes = _differ().diff(old, new)
assert len(changes) == 1
assert changes[0].change_ratio >= 0.02
assert changes[0].needs_llm is True
def test_empty_old_text_yields_no_changes():
"""A first crawl has no baseline, so there is nothing to diff."""
assert _differ().diff("", "第一条 任意内容。") == []
def test_unchanged_paragraphs_are_never_returned():
"""Only changed paragraphs appear; equal ones are dropped."""
old = "\n".join([
"第一条 保持不变的条款。",
"第二条 车辆制动距离不得超过30米。",
"第三条 另一条保持不变的条款。",
])
new = "\n".join([
"第一条 保持不变的条款。",
"第二条 车辆制动距离不得超过20米。",
"第三条 另一条保持不变的条款。",
])
changes = _differ().diff(old, new)
assert len(changes) == 1
assert changes[0].numeric_changed is True
assert "第二条" in changes[0].old_text