solidprivacy-nl commited on
Commit
4476fe9
·
1 Parent(s): f0d3f85

Add WP24 residual risk report tests

Browse files
Files changed (1) hide show
  1. tests/test_residual_risk_report.py +160 -0
tests/test_residual_risk_report.py ADDED
@@ -0,0 +1,160 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import importlib.util
2
+ import json
3
+ import sys
4
+ from pathlib import Path
5
+
6
+
7
+ def load_residual_module():
8
+ module_path = Path(__file__).resolve().parents[1] / "benchmark" / "build_residual_risk_report.py"
9
+ spec = importlib.util.spec_from_file_location("build_residual_risk_report", module_path)
10
+ module = importlib.util.module_from_spec(spec)
11
+ assert spec.loader is not None
12
+ sys.modules[spec.name] = module
13
+ spec.loader.exec_module(module)
14
+ return module
15
+
16
+
17
+ def sample_scorecard():
18
+ return {
19
+ "schema_version": "wp23_entity_class_scorecard_v1",
20
+ "generated_at": "2026-06-12T10:00:00+00:00",
21
+ "synthetic_only": True,
22
+ "report_only": True,
23
+ "thresholds_applied": False,
24
+ "production_gate": False,
25
+ "safe_for_production_claim": False,
26
+ "warnings": [
27
+ "Synthetic benchmark only; results do not prove real-world safety.",
28
+ "benchmark/gold/examples/mini.gold.example.json: schema example only; metrics are diagnostic, not full corpus results",
29
+ ],
30
+ "overall": {
31
+ "gold_count": 3,
32
+ "prediction_count": 3,
33
+ "false_negative_count": 1,
34
+ "false_positive_count": 1,
35
+ "recall_normalized": 0.666667,
36
+ "precision_normalized": 0.666667,
37
+ },
38
+ "per_domain": {
39
+ "legal": {
40
+ "gold_count": 2,
41
+ "prediction_count": 2,
42
+ "false_negative_count": 1,
43
+ "false_positive_count": 1,
44
+ "recall_normalized": 0.5,
45
+ "precision_normalized": 0.5,
46
+ },
47
+ "zorg": {
48
+ "gold_count": 1,
49
+ "prediction_count": 1,
50
+ "false_negative_count": 0,
51
+ "false_positive_count": 0,
52
+ "recall_normalized": 1.0,
53
+ "precision_normalized": 1.0,
54
+ },
55
+ },
56
+ "per_entity_class": [
57
+ {
58
+ "entity_class": "PERSON",
59
+ "gold_count": 1,
60
+ "prediction_count": 1,
61
+ "false_negative_count": 0,
62
+ "false_positive_count": 0,
63
+ "recall_normalized": 1.0,
64
+ "precision_normalized": 1.0,
65
+ },
66
+ {
67
+ "entity_class": "PHONE",
68
+ "gold_count": 1,
69
+ "prediction_count": 1,
70
+ "false_negative_count": 1,
71
+ "false_positive_count": 1,
72
+ "recall_normalized": 0.0,
73
+ "precision_normalized": 0.0,
74
+ },
75
+ {
76
+ "entity_class": "BSN",
77
+ "gold_count": 0,
78
+ "prediction_count": 0,
79
+ "false_negative_count": 0,
80
+ "false_positive_count": 0,
81
+ "recall_normalized": None,
82
+ "precision_normalized": None,
83
+ },
84
+ ],
85
+ "failure_counts": {
86
+ "false_negative_count": 1,
87
+ "false_positive_count": 1,
88
+ "preserve_term_failure_count": 1,
89
+ "known_trap_failure_count": 1,
90
+ "partial_overlap_diagnostic_count": 1,
91
+ },
92
+ "details": {
93
+ "false_negatives": [{"entity_class": "PHONE", "expected_text": "06 1234 5678"}],
94
+ "false_positives": [{"entity_class": "BSN", "text": "13-03-2026"}],
95
+ "preserve_term_failures": [{"term": "cliënt"}],
96
+ "known_trap_failures": [{"trap_text": "13-03-2026"}],
97
+ "partial_overlaps": [{"policy": "diagnostic_only"}],
98
+ },
99
+ "gold_files": ["benchmark/gold/examples/mini.gold.example.json"],
100
+ "prediction_file": None,
101
+ }
102
+
103
+
104
+ def test_residual_report_contains_required_policy_and_limitations():
105
+ module = load_residual_module()
106
+
107
+ report = module.build_residual_risk_report(sample_scorecard(), generated_at="2026-06-12T12:30:00+00:00")
108
+
109
+ assert report["schema_version"] == "wp24_false_negative_residual_risk_report_v1"
110
+ assert report["synthetic_only"] is True
111
+ assert report["report_only"] is True
112
+ assert report["thresholds_applied"] is False
113
+ assert report["production_gate"] is False
114
+ assert report["safe_for_production_claim"] is False
115
+ assert report["policy"]["risk_visible"] is True
116
+ assert report["policy"]["no_production_safety_claim"] is True
117
+ assert report["policy"]["no_recall_precision_threshold"] is True
118
+ assert report["policy"]["ci_must_not_fail_on_low_scores"] is True
119
+ assert "schema examples only" in " ".join(report["coverage_status"]["known_limitations"])
120
+ assert "supplied prediction JSON only" in " ".join(report["coverage_status"]["known_limitations"])
121
+ assert report["overall_false_negative_risk_summary"]["residual_risk_level"] == "critical_unresolved_due_to_coverage_limits"
122
+
123
+
124
+ def test_residual_report_summarizes_domain_entity_preserve_trap_and_near_miss_risks():
125
+ module = load_residual_module()
126
+ report = module.build_residual_risk_report(sample_scorecard(), generated_at="2026-06-12T12:30:00+00:00")
127
+
128
+ assert report["per_domain_residual_risk"]["legal"]["residual_risk_level"] == "high_residual_risk"
129
+ by_entity = {item["entity_class"]: item for item in report["per_entity_class_residual_risk"]}
130
+ assert by_entity["PHONE"]["residual_risk_level"] == "high_residual_risk"
131
+ assert by_entity["BSN"]["residual_risk_level"] == "not_yet_baselined"
132
+ assert "BSN" in report["unsupported_or_not_yet_baselined_classes"]
133
+ assert report["preserve_term_risk_summary"]["failure_count"] == 1
134
+ assert report["known_trap_false_positive_risk_summary"]["known_trap_failure_count"] == 1
135
+ assert report["partial_overlap_near_miss_summary"]["partial_overlap_diagnostic_count"] == 1
136
+
137
+
138
+ def test_residual_markdown_contains_no_safety_claim_and_next_work():
139
+ module = load_residual_module()
140
+ report = module.build_residual_risk_report(sample_scorecard(), generated_at="2026-06-12T12:30:00+00:00")
141
+
142
+ markdown = module.render_markdown(report)
143
+
144
+ assert "No production safety claim" in markdown
145
+ assert "No recall/precision threshold" in markdown
146
+ assert "Current gold sidecars are schema examples only" in markdown
147
+ assert "WP29 — Scrub Key secure import/export tests" in markdown
148
+ assert "`PHONE`" in markdown
149
+ assert "`partial_overlap_near_miss_summary`" not in markdown
150
+
151
+
152
+ def test_write_residual_risk_report_outputs_json_and_markdown(tmp_path):
153
+ module = load_residual_module()
154
+ report = module.build_residual_risk_report(sample_scorecard(), generated_at="2026-06-12T12:30:00+00:00")
155
+
156
+ json_path, md_path = module.write_residual_risk_report(report, tmp_path / "reports")
157
+
158
+ parsed = json.loads(json_path.read_text(encoding="utf-8"))
159
+ assert parsed["schema_version"] == "wp24_false_negative_residual_risk_report_v1"
160
+ assert md_path.read_text(encoding="utf-8").startswith("# SolidPrivacy Scrub — False-negative residual-risk report")