File size: 18,966 Bytes
a6a5d8e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
# SPDX-License-Identifier: Apache-2.0
# © 2026 Lutar, Stephen P. — SZL Holdings · ORCID 0009-0001-0110-4173 · Doctrine v11
# Co-Authored-By: Perplexity Computer Agent <agent@perplexity.ai>
# Signed-off-by: Stephen P. Lutar Jr. <stephenlutar2@gmail.com>
"""
Tests for szl_parity_gaps — parity gap closure + differentiator endpoints.

Coverage:
  GAP-A: /api/a11oy/v1/compliance/export
  GAP-B: /api/a11oy/v1/lineage
  GAP-C: /api/a11oy/v1/policy/validate
  DIFF-1: /api/a11oy/v1/receipts/replay
  DIFF-2: /api/a11oy/v1/lambda/score
"""
from __future__ import annotations
import json
import math
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient

import sys, os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

import szl_parity_gaps as pg

# ---------------------------------------------------------------------------
# Minimal gate manifest stubs for tests
# ---------------------------------------------------------------------------
_STUB_GATES = [
    {"name": "thresholdPolicySeverity", "file": "thresholdPolicySeverity_gate.ts",
     "description": "Threshold gate", "lean_theorem": "ThresholdAxiom",
     "lean_file": "Lutar/LambdaInvariant/Boundary.lean", "lean_verified": True},
    {"name": "adversarialRobustness", "file": "adversarialRobustness_gate.ts",
     "description": "Adversarial robustness", "lean_theorem": "", "lean_verified": False},
    {"name": "hashChainIntegrity", "file": "hashChainIntegrity_gate.ts",
     "description": "Hash chain", "lean_theorem": "", "lean_verified": False},
    {"name": "merkleDagBatch", "file": "merkleDagBatch_gate.ts",
     "description": "Merkle DAG", "lean_theorem": "", "lean_verified": False},
    {"name": "doctrineCompleteness", "file": "doctrineCompleteness_gate.ts",
     "description": "Doctrine completeness", "lean_theorem": "", "lean_verified": False},
    {"name": "constructiveTransparency", "file": "constructiveTransparency_gate.ts",
     "description": "Transparency", "lean_theorem": "", "lean_verified": False},
    {"name": "soundnessAxiom", "file": "soundnessAxiom_gate.ts",
     "description": "Soundness", "lean_theorem": "", "lean_verified": False},
    {"name": "deterministicReplay", "file": "deterministicReplay_gate.ts",
     "description": "Deterministic replay", "lean_theorem": "", "lean_verified": False},
    {"name": "certifiedRobustness", "file": "certifiedRobustness_gate.ts",
     "description": "Certified robustness", "lean_theorem": "", "lean_verified": False},
    {"name": "bekensteinBound", "file": "bekensteinBound_gate.ts",
     "description": "Bekenstein", "lean_theorem": "", "lean_verified": False},
    {"name": "composability", "file": "composability_gate.ts",
     "description": "Composability", "lean_theorem": "", "lean_verified": False},
    {"name": "crossRegionPolicy", "file": "crossRegionPolicy_gate.ts",
     "description": "Cross-region", "lean_theorem": "", "lean_verified": False},
    {"name": "witnessQuorum", "file": "witnessQuorum_gate.ts",
     "description": "Witness quorum", "lean_theorem": "", "lean_verified": False},
    {"name": "humanEscalation", "file": "humanEscalation_gate.ts",
     "description": "Human escalation", "lean_theorem": "", "lean_verified": False},
    {"name": "provenance", "file": "provenance_gate.ts",
     "description": "Provenance", "lean_theorem": "", "lean_verified": False},
]
_STUB_BY_NAME = {g["name"]: g for g in _STUB_GATES}


@pytest.fixture(scope="module")
def client():
    app = FastAPI()
    pg.register(app, _STUB_GATES, _STUB_BY_NAME)
    return TestClient(app, raise_server_exceptions=True)


# ---------------------------------------------------------------------------
# GAP-A: Compliance Evidence Export
# ---------------------------------------------------------------------------
class TestComplianceExport:
    def test_eu_ai_act_200(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=eu-ai-act")
        assert r.status_code == 200
        d = r.json()
        assert d["framework"] == "eu-ai-act"
        assert d["schema"] == "szl.compliance.export.v1"
        assert isinstance(d["controls"], list)
        assert len(d["controls"]) > 0
        assert d["doctrine"] == "v11"

    def test_nist_ai_rmf_200(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=nist-ai-rmf")
        assert r.status_code == 200
        assert r.json()["framework"] == "nist-ai-rmf"

    def test_iso_42001_200(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=iso-42001")
        assert r.status_code == 200
        assert r.json()["framework"] == "iso-42001"

    def test_soc2_200(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=soc2")
        assert r.status_code == 200
        assert r.json()["framework"] == "soc2"

    def test_unknown_framework_400(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=fedramp-high")
        assert r.status_code == 400
        d = r.json()
        assert "available" in d

    def test_controls_have_gate_evidence(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=eu-ai-act")
        d = r.json()
        for ctrl in d["controls"]:
            assert "gate_evidence" in ctrl
            assert isinstance(ctrl["gate_evidence"], list)

    def test_lambda_conjecture_labelled(self, client):
        r = client.get("/api/a11oy/v1/compliance/export?framework=eu-ai-act")
        d = r.json()
        assert "Conjecture" in d["lambda"]

    def test_default_framework_is_eu_ai_act(self, client):
        r = client.get("/api/a11oy/v1/compliance/export")
        assert r.status_code == 200
        assert r.json()["framework"] == "eu-ai-act"


# ---------------------------------------------------------------------------
# GAP-B: Decision Lineage Query
# ---------------------------------------------------------------------------
class TestDecisionLineage:
    def test_lineage_200(self, client):
        r = client.get("/api/a11oy/v1/lineage")
        assert r.status_code == 200
        d = r.json()
        assert d["schema"] == "szl.lineage.v1"
        assert "nodes" in d
        assert d["doctrine"] == "v11"

    def test_lineage_empty_on_cold_dag(self, client):
        """On a cold start with no real DAG the endpoint returns honest empty nodes."""
        r = client.get("/api/a11oy/v1/lineage?limit=10")
        d = r.json()
        # Either empty (cold start) or has nodes (if DAG populated)
        assert isinstance(d["nodes"], list)
        assert "returned" in d

    def test_lineage_limit_honoured(self, client):
        r = client.get("/api/a11oy/v1/lineage?limit=5")
        d = r.json()
        assert d["limit"] == 5
        assert len(d["nodes"]) <= 5

    def test_lineage_limit_capped_at_200(self, client):
        r = client.get("/api/a11oy/v1/lineage?limit=9999")
        d = r.json()
        assert d["limit"] == 200

    def test_lineage_leader_parity_refs(self, client):
        r = client.get("/api/a11oy/v1/lineage")
        d = r.json()
        lp = d.get("leader_parity", {})
        assert "palantir" in " ".join(lp.values()).lower()


# ---------------------------------------------------------------------------
# GAP-C: Policy-as-Code Validation
# ---------------------------------------------------------------------------
class TestPolicyValidate:
    def test_valid_policy(self, client):
        payload = {
            "name": "test-policy",
            "gates": ["thresholdPolicySeverity", "adversarialRobustness"],
            "lambda_floor": 0.90,
            "min_witnesses": 2,
            "severity": "medium",
        }
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["valid"] is True
        assert d["violations"] == []

    def test_missing_name_invalid(self, client):
        payload = {"gates": ["thresholdPolicySeverity"]}
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["valid"] is False
        assert any(v["field"] == "name" for v in d["violations"])

    def test_unknown_gate_invalid(self, client):
        payload = {"name": "test", "gates": ["nonexistent_gate_xyz"]}
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["valid"] is False
        assert any("Unknown gates" in v["message"] for v in d["violations"])

    def test_lambda_floor_out_of_range(self, client):
        payload = {"name": "test", "gates": ["thresholdPolicySeverity"],
                   "lambda_floor": 1.5}
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["valid"] is False
        assert any("lambda_floor" in v["field"] for v in d["violations"])

    def test_capital_severity_quorum_check(self, client):
        payload = {"name": "test", "gates": ["thresholdPolicySeverity"],
                   "severity": "capital", "min_witnesses": 2}
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        assert r.status_code == 200
        d = r.json()
        # capital requires 3-of-N quorum
        assert d["required_quorum"] == 3
        assert d["valid"] is False

    def test_gate_coverage_reported(self, client):
        payload = {"name": "test", "gates": ["thresholdPolicySeverity"]}
        r = client.post("/api/a11oy/v1/policy/validate", json=payload)
        d = r.json()
        assert "gate_coverage" in d
        assert d["gate_coverage"]["known"] >= 1

    def test_invalid_json_400(self, client):
        r = client.post("/api/a11oy/v1/policy/validate",
                        content=b"not json", headers={"Content-Type": "application/json"})
        assert r.status_code == 400


# ---------------------------------------------------------------------------
# DIFF-1: Receipt Replay
# ---------------------------------------------------------------------------
class TestReceiptReplay:
    def test_replay_allow(self, client):
        payload = {
            "action": {
                "severity": "medium",
                "confidence": 0.85,
                "actionId": "replay-test-001",
                "witnesses": [
                    {"id": "w1", "role": "op", "attested": True},
                    {"id": "w2", "role": "auditor", "attested": True},
                ],
            },
            "gate": "thresholdPolicySeverity",
        }
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["schema"] == "szl.receipt.replay.v1"
        assert d["replay_decision"] in ("allow", "deny")
        assert "replay_receipt_hash" in d
        assert "determinism_note" in d

    def test_replay_deny_low_confidence(self, client):
        payload = {
            "action": {
                "severity": "high",
                "confidence": 0.5,
                "actionId": "replay-deny-001",
                "witnesses": [{"id": "w1", "role": "op", "attested": True}],
            },
            "gate": "thresholdPolicySeverity",
        }
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        assert r.status_code == 200
        d = r.json()
        assert d["replay_decision"] == "deny"

    def test_replay_hash_comparison(self, client):
        payload = {
            "action": {"severity": "medium", "confidence": 0.85, "actionId": "a",
                       "witnesses": [{"id": "w1", "attested": True},
                                     {"id": "w2", "attested": True}]},
            "original_receipt_hash": "deadbeef1234567890",
            "gate": "thresholdPolicySeverity",
        }
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        d = r.json()
        assert "hashes_match" in d
        # Hashes won't match (different hash), but the field must be present
        assert d["hashes_match"] is False  # deadbeef won't match real sha256

    def test_replay_unknown_gate_400(self, client):
        payload = {"action": {"severity": "medium"}, "gate": "nonexistent_gate_xyz"}
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        assert r.status_code == 400

    def test_replay_no_action_400(self, client):
        payload = {"gate": "thresholdPolicySeverity"}
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        assert r.status_code == 400

    def test_replay_dsse_envelope_present(self, client):
        payload = {
            "action": {"severity": "medium", "confidence": 0.85, "actionId": "a",
                       "witnesses": [{"id": "w1", "attested": True},
                                     {"id": "w2", "attested": True}]},
            "gate": "thresholdPolicySeverity",
        }
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        d = r.json()
        assert "replay_dsse" in d
        # replay_dsse may wrap a {receipt, dsse} or a {payloadType, ...} envelope
        env = d["replay_dsse"]
        has_payload_type = "payloadType" in env
        has_nested_dsse = "dsse" in env or "receipt" in env
        assert has_payload_type or has_nested_dsse, f"unexpected dsse shape: {list(env.keys())}"

    def test_replay_differentiator_note_present(self, client):
        payload = {
            "action": {"severity": "medium", "confidence": 0.85, "actionId": "a",
                       "witnesses": [{"id": "w1", "attested": True},
                                     {"id": "w2", "attested": True}]},
        }
        r = client.post("/api/a11oy/v1/receipts/replay", json=payload)
        d = r.json()
        assert "differentiator" in d


# ---------------------------------------------------------------------------
# DIFF-2: Λ-Gated Decision Scoring
# ---------------------------------------------------------------------------
class TestLambdaScore:
    _PERFECT = {a: 1.0 for a in pg.CANONICAL_AXES}
    _TYPICAL = {a: 0.91 for a in pg.CANONICAL_AXES}
    _ZERO_ONE = {**{a: 0.91 for a in pg.CANONICAL_AXES}, "soundness": 0.0}

    def test_perfect_axes_lambda_one(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._PERFECT})
        assert r.status_code == 200
        d = r.json()
        assert abs(d["lambda"] - 1.0) < 1e-6
        assert d["gate_pass"] is True

    def test_typical_axes_lambda_in_range(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._TYPICAL})
        d = r.json()
        lv = d["lambda"]
        assert 0.8 < lv < 1.0

    def test_zero_axis_zero_pinning(self, client):
        """A2: zero-pinning — Λ = 0 when any positive-weight axis is 0."""
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._ZERO_ONE})
        d = r.json()
        assert d["lambda"] == 0.0
        assert d["zero_pinned"] is True
        assert d["gate_pass"] is False

    def test_gate_pass_with_high_floor(self, client):
        r = client.post("/api/a11oy/v1/lambda/score",
                        json={"axes": self._TYPICAL, "lambda_floor": 0.99})
        d = r.json()
        assert d["gate_pass"] is False

    def test_gate_decision_deny(self, client):
        r = client.post("/api/a11oy/v1/lambda/score",
                        json={"axes": self._ZERO_ONE, "lambda_floor": 0.90})
        d = r.json()
        assert d["gate_decision"] == "deny"

    def test_schema_correct(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._TYPICAL})
        d = r.json()
        assert d["schema"] == "szl.lambda.score.v1"
        assert d["doctrine"] == "v11"

    def test_lean_citation_conjecture(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._TYPICAL})
        d = r.json()
        lc = d["lean_citation"]
        assert "Conjecture 1" in lc["status"]
        assert "NOT a closed theorem" in lc["status"]

    def test_axes_breakdown_present(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._TYPICAL})
        d = r.json()
        assert len(d["axes_computed"]) == 9

    def test_missing_axes_flagged(self, client):
        r = client.post("/api/a11oy/v1/lambda/score",
                        json={"axes": {"soundness": 0.9}})
        d = r.json()
        assert len(d["missing_axes"]) == 8

    def test_monotonicity_a1(self, client):
        """A1: raising one axis while others fixed should not decrease Λ."""
        base = {a: 0.80 for a in pg.CANONICAL_AXES}
        high = {**base, "soundness": 0.95}
        r_base = client.post("/api/a11oy/v1/lambda/score", json={"axes": base})
        r_high = client.post("/api/a11oy/v1/lambda/score", json={"axes": high})
        assert r_high.json()["lambda"] >= r_base.json()["lambda"]

    def test_no_action_400(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"lambda_floor": 0.9})
        assert r.status_code == 400

    def test_differentiator_note_present(self, client):
        r = client.post("/api/a11oy/v1/lambda/score", json={"axes": self._TYPICAL})
        d = r.json()
        assert "differentiator" in d


# ---------------------------------------------------------------------------
# Lambda computation unit tests (no HTTP)
# ---------------------------------------------------------------------------
class TestLambdaComputation:
    def test_perfect_is_one(self):
        axes = {a: 1.0 for a in pg.CANONICAL_AXES}
        result = pg._compute_lambda(axes)
        assert abs(result["lambda"] - 1.0) < 1e-9

    def test_zero_pinning(self):
        axes = {a: 0.9 for a in pg.CANONICAL_AXES}
        axes["soundness"] = 0.0
        result = pg._compute_lambda(axes)
        assert result["lambda"] == 0.0
        assert result["zero_pinned"] is True

    def test_monotonicity(self):
        base = {a: 0.80 for a in pg.CANONICAL_AXES}
        high = {**base, "calibration": 0.99}
        r_base = pg._compute_lambda(base)
        r_high = pg._compute_lambda(high)
        assert r_high["lambda"] >= r_base["lambda"]

    def test_symmetry(self):
        """A4 corollary: all-equal axes → Λ = that value (geometric = arithmetic for equal values)."""
        axes = {a: 0.90 for a in pg.CANONICAL_AXES}
        result = pg._compute_lambda(axes)
        assert abs(result["lambda"] - 0.90) < 1e-6

    def test_weights_normalised(self):
        """Custom weights that don't sum to 1 should still give valid Λ ∈ [0,1]."""
        axes = {a: 0.90 for a in pg.CANONICAL_AXES}
        weights = {a: 2.0 for a in pg.CANONICAL_AXES}  # sum = 18, not 1
        result = pg._compute_lambda(axes, weights)
        assert 0.0 <= result["lambda"] <= 1.0 + 1e-9