import json import sys from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent)) from core.confidence_signal import ( WEIGHTS, PENALTY, THRESHOLDS, compute, Signal, _per_input_score, ) class TestWeights: def test_weights_sum_to_one(self): assert sum(WEIGHTS.values()) == pytest.approx(1.0) def test_policy_weight_highest(self): assert WEIGHTS["policy"] == 0.30 def test_validation_weight(self): assert WEIGHTS["validation"] == 0.25 class TestThresholds: def test_dev_threshold(self): assert THRESHOLDS["dev"] == 0.50 def test_qa_threshold(self): assert THRESHOLDS["qa"] == 0.75 def test_prod_threshold(self): assert THRESHOLDS["prod"] == 0.90 def test_dr_threshold(self): assert THRESHOLDS["dr"] == 0.95 class TestPenalty: def test_critical_is_none(self): assert PENALTY["critical"] is None def test_high_penalty(self): assert PENALTY["high"] == 0.20 def test_medium_penalty(self): assert PENALTY["medium"] == 0.05 def test_low_penalty(self): assert PENALTY["low"] == 0.01 def test_info_no_penalty(self): assert PENALTY["info"] == 0.0 class TestPerInputScore: def test_missing_input_returns_half(self): score, reasons = _per_input_score("policy", None) assert score == 0.5 assert "INPUT_MISSING:policy" in reasons def test_empty_policy_list(self): score, reasons = _per_input_score("policy", []) assert score == 0.5 assert reasons == [] def test_all_pass_policy(self): pcrs = [{"result": "pass"}, {"result": "pass"}] score, reasons = _per_input_score("policy", pcrs) assert score == 1.0 assert reasons == [] def test_mixed_policy(self): pcrs = [{"result": "pass"}, {"result": "fail"}] score, reasons = _per_input_score("policy", pcrs) assert score == 0.5 def test_skipped_counts_as_pass(self): pcrs = [{"result": "skipped"}] score, reasons = _per_input_score("policy", pcrs) assert score == 1.0 def test_validation_all_true(self): score, reasons = _per_input_score("validation", { "schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True }) assert score == 1.0 def test_validation_partial(self): score, reasons = _per_input_score("validation", { "schema": True, "stack_resolved": True, "tf_validated": False, "tf_planned": False }) assert score == 0.5 def test_freshness_fresh(self): score, _ = _per_input_score("freshness", {"age_days": 0, "max_age_days": 7}) assert score == 1.0 def test_freshness_stale(self): score, _ = _per_input_score("freshness", {"age_days": 7, "max_age_days": 7}) assert score == pytest.approx(0.0) def test_source_complete(self): score, _ = _per_input_score("source", {"submitter": "dev", "commit_sha": "abc"}) assert score == 1.0 def test_source_partial(self): score, _ = _per_input_score("source", {"submitter": "dev"}) assert score == 0.5 def test_history_clean(self): score, _ = _per_input_score("history", {"prior_rollbacks": 0, "prior_policy_fails": 0}) assert score == 1.0 def test_history_with_failures(self): score, _ = _per_input_score("history", {"prior_rollbacks": 2, "prior_policy_fails": 3}) assert score == pytest.approx(0.3) def test_nfrs_none(self): score, _ = _per_input_score("nfrs", {"conformance": None}) assert score == 0.5 def test_nfrs_full(self): score, _ = _per_input_score("nfrs", {"conformance": 0.95}) assert score == 0.95 class TestCompute: def _base_inputs(self): return { "policy": [{"result": "pass"}], "validation": {"schema": True, "stack_resolved": True, "tf_validated": True, "tf_planned": True}, "freshness": {"age_days": 0, "max_age_days": 7}, "source": {"submitter": "dev", "commit_sha": "abc"}, "history": {"prior_rollbacks": 0, "prior_policy_fails": 0}, "nfrs": {"conformance": None}, } def test_dev_pass(self): sig = compute("test-001", "dev", self._base_inputs()) assert sig.band == "pass" assert sig.score >= 0.50 def test_missing_input_blocks(self): inputs = self._base_inputs() del inputs["policy"] sig = compute("test-002", "dev", inputs) assert sig.band == "block" assert sig.score == 0.0 assert any("INPUT_MISSING" in r for r in sig.reasonCodes) def test_critical_policy_blocks(self): inputs = self._base_inputs() inputs["policy"] = [{"result": "fail", "severity": "critical", "ruleId": "CKV_X"}] sig = compute("test-003", "dev", inputs) assert sig.band == "block" assert sig.score == 0.0 assert any("CRITICAL_OVERRIDE" in r for r in sig.reasonCodes) def test_high_policy_lowers_score(self): inputs = self._base_inputs() inputs["policy"] = [{"result": "fail", "severity": "high", "ruleId": "CKV_Y"}] sig = compute("test-004", "dev", inputs) assert sig.score < 1.0 def test_dev_warn_becomes_block(self): sig = compute("test-005", "dev", self._base_inputs()) assert sig.band != "warn" def test_signal_has_per_input(self): sig = compute("test-006", "dev", self._base_inputs()) assert "policy" in sig.perInput assert "validation" in sig.perInput assert "nfrs" in sig.perInput def test_all_six_inputs_present(self): sig = compute("test-007", "dev", self._base_inputs()) assert len(sig.perInput) == 6