- 新建 pytest.ini 统一 test discovery(tests/ + skills/ir_generation_skill/tests/) - test_step1~3 转换为 pytest 兼容格式,无输出文件时自动 skip - 新增 tests/test_detect_conflicts.py(18 个纯函数单测) - 新增 tests/test_config.py(7 个配置模块单测) - CI 改为 pytest -v 使用 pytest.ini testpaths - DEV_AGENT.md 新增 PR 提交规范 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -365,6 +365,97 @@ def run_all_tests():
|
||||
return total_failures == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# pytest discovery support — skips gracefully when output files are absent
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
|
||||
def _load_si_and_doc():
|
||||
"""Try to load semantic_index.json and the input document. Returns (si, doc) or (None, None)."""
|
||||
try:
|
||||
si = config.load_json(config.SEMANTIC_INDEX_JSON)
|
||||
doc = config.load_input_document()
|
||||
return si, doc
|
||||
except FileNotFoundError:
|
||||
return None, None
|
||||
|
||||
|
||||
def test_step1_unit_ids():
|
||||
"""pytest: verify all function_units have valid unit_id and name."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found — run step1_semantic_index.py first")
|
||||
units = si.get("function_units", [])
|
||||
errors = check_unit_ids(units)
|
||||
assert not errors, f"unit_id/name errors: {errors}"
|
||||
|
||||
|
||||
def test_step1_path_fields():
|
||||
"""pytest: verify all function_units have non-empty path arrays."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
units = si.get("function_units", [])
|
||||
errors = check_unit_paths(units)
|
||||
assert not errors, f"path field errors: {errors}"
|
||||
|
||||
|
||||
def test_step1_concept_parents():
|
||||
"""pytest: verify concept parent references are valid."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
concepts = si.get("concepts", [])
|
||||
errors = check_concept_parents(concepts)
|
||||
assert not errors, f"concept parent errors: {errors}"
|
||||
|
||||
|
||||
def test_step1_sources_exist():
|
||||
"""pytest: verify all source references point to real content."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
units = si.get("function_units", [])
|
||||
image_index = build_image_index(doc)
|
||||
node_index = build_logic_tree_node_index(doc)
|
||||
errors = check_sources_exist(units, image_index, node_index)
|
||||
assert not errors, f"source reference errors: {errors}"
|
||||
|
||||
|
||||
def test_step1_logic_tree_coverage():
|
||||
"""pytest: verify decision/action nodes in logic trees are covered (warnings only)."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
units = si.get("function_units", [])
|
||||
node_index = build_logic_tree_node_index(doc)
|
||||
warnings = check_logic_tree_coverage(units, node_index)
|
||||
# Warnings are informational, not failures — but report them
|
||||
if warnings:
|
||||
print(f"\n[WARN] Logic tree coverage warnings: {warnings}")
|
||||
|
||||
|
||||
def test_step1_ensemble_confidence():
|
||||
"""pytest: verify function_units have confidence/ensemble_support/source_versions."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
units = si.get("function_units", [])
|
||||
errors = check_ensemble_confidence(units)
|
||||
assert not errors, f"ensemble confidence errors: {errors}"
|
||||
|
||||
|
||||
def test_step1_confidence_summary():
|
||||
"""pytest: verify confidence_summary counts match actual unit/concept counts."""
|
||||
si, doc = _load_si_and_doc()
|
||||
if si is None:
|
||||
pytest.skip("semantic_index.json not found")
|
||||
errors = check_confidence_summary(si)
|
||||
assert not errors, f"confidence_summary errors: {errors}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
@@ -317,6 +317,93 @@ def run_all_tests():
|
||||
return total_failures == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# pytest discovery support
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
|
||||
def _load_fragments_or_skip():
|
||||
"""Load ir_fragments.json or return None."""
|
||||
try:
|
||||
return config.load_json(config.IR_FRAGMENTS_JSON)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def test_step2_non_empty_rules():
|
||||
"""pytest: every fragment must have at least one rule."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found — run step2_ir_extraction.py first")
|
||||
errors = check_non_empty_rules(fragments)
|
||||
assert not errors, f"non-empty rule errors: {errors}"
|
||||
|
||||
|
||||
def test_step2_rule_paths():
|
||||
"""pytest: every rule must have a non-empty path array."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_rule_paths(fragments)
|
||||
assert not errors, f"rule path errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step2_precondition_fields():
|
||||
"""pytest: every rule must have precondition with geographic_scope and screen_type."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_precondition_fields(fragments)
|
||||
assert not errors, f"precondition errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step2_user_interaction_content():
|
||||
"""pytest: user_interaction actions must have non-empty, non-placeholder content."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_user_interaction_content(fragments)
|
||||
assert not errors, f"user_interaction content errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step2_sources_have_refs():
|
||||
"""pytest: every rule should reference at least one source."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_sources_have_logic_tree_nodes(fragments)
|
||||
assert not errors, f"source reference errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step2_trigger_conditions():
|
||||
"""pytest: every trigger condition must have signal, operator, value."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_trigger_conditions(fragments)
|
||||
assert not errors, f"trigger condition errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step2_duplicate_rule_ids():
|
||||
"""pytest: no duplicate rule_ids across all fragments."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_duplicate_rule_ids(fragments)
|
||||
assert not errors, f"duplicate rule_id errors: {errors}"
|
||||
|
||||
|
||||
def test_step2_action_types():
|
||||
"""pytest: all actions must have valid types."""
|
||||
fragments = _load_fragments_or_skip()
|
||||
if fragments is None:
|
||||
pytest.skip("ir_fragments.json not found")
|
||||
errors = check_action_types(fragments)
|
||||
assert not errors, f"action type errors: {errors[:5]}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
@@ -147,6 +147,32 @@ def run_all_tests():
|
||||
return total_failures == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# pytest discovery support
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
|
||||
def test_step2_5_path_enumeration():
|
||||
"""pytest: path_enumeration.json should have valid structure."""
|
||||
try:
|
||||
path_data = config.load_json(config.PATH_ENUM_JSON)
|
||||
except FileNotFoundError:
|
||||
pytest.skip("path_enumeration.json not found — run step2_5_branch_coverage.py first")
|
||||
errors = check_path_enumeration(path_data)
|
||||
assert not errors, f"path enumeration errors: {errors}"
|
||||
|
||||
|
||||
def test_step2_5_autocomplete_fragments():
|
||||
"""pytest: auto-complete fragments must have valid structure if present."""
|
||||
fragments = load_autocomplete_fragments()
|
||||
errors = check_autocomplete_fragments(fragments)
|
||||
# If fragments is None (no autocomplete needed), check_autocomplete_fragments returns a warning
|
||||
actual_errors = [e for e in errors if "未生成" not in e]
|
||||
assert not actual_errors, f"autocomplete fragment errors: {actual_errors}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
@@ -227,6 +227,77 @@ def run_all_tests():
|
||||
return total_failures == 0
|
||||
|
||||
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
# pytest discovery support
|
||||
# ═══════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
import pytest # noqa: E402
|
||||
|
||||
|
||||
def _load_ir_final_or_skip():
|
||||
"""Load ir_final.json or return None."""
|
||||
try:
|
||||
return config.load_json(config.IR_FINAL_JSON)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def _load_audit_report_or_skip():
|
||||
"""Load ir_audit_report.md or return None."""
|
||||
try:
|
||||
with open(config.IR_AUDIT_REPORT_MD, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def test_step3_top_level_structure():
|
||||
"""pytest: ir_final must have required top-level fields."""
|
||||
ir = _load_ir_final_or_skip()
|
||||
if ir is None:
|
||||
pytest.skip("ir_final.json not found — run step3_merge_and_audit.py first")
|
||||
errors = check_top_level_structure(ir)
|
||||
assert not errors, f"top-level structure errors: {errors}"
|
||||
|
||||
|
||||
def test_step3_rule_ids():
|
||||
"""pytest: rule_ids must be unique and follow naming convention."""
|
||||
ir = _load_ir_final_or_skip()
|
||||
if ir is None:
|
||||
pytest.skip("ir_final.json not found")
|
||||
errors = check_rule_ids(ir)
|
||||
assert not errors, f"rule_id errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step3_rule_paths():
|
||||
"""pytest: every rule must have a non-empty path array."""
|
||||
ir = _load_ir_final_or_skip()
|
||||
if ir is None:
|
||||
pytest.skip("ir_final.json not found")
|
||||
rules = ir.get("rules", [])
|
||||
errors = check_rule_paths(rules)
|
||||
assert not errors, f"rule path errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step3_rule_completeness():
|
||||
"""pytest: each rule must have all required fields."""
|
||||
ir = _load_ir_final_or_skip()
|
||||
if ir is None:
|
||||
pytest.skip("ir_final.json not found")
|
||||
rules = ir.get("rules", [])
|
||||
errors = check_rule_completeness(rules)
|
||||
assert not errors, f"rule completeness errors: {errors[:5]}"
|
||||
|
||||
|
||||
def test_step3_audit_report():
|
||||
"""pytest: audit report must have all required sections."""
|
||||
report = _load_audit_report_or_skip()
|
||||
if report is None:
|
||||
pytest.skip("ir_audit_report.md not found — run step3_merge_and_audit.py first")
|
||||
errors = check_audit_report(report)
|
||||
assert not errors, f"audit report errors: {errors[:5]}"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
success = run_all_tests()
|
||||
sys.exit(0 if success else 1)
|
||||
|
||||
Reference in New Issue
Block a user