158 lines
6.1 KiB
Python
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
|