noon-report-checker / tests /test_validators.py
tarunagarwal1981's picture
Noon report sanity-checker: app, deterministic validators, LLM layer, tests, demo data, field notes
0846808
Raw
History Blame Contribute Delete
3.89 kB
"""Deterministic-validator tests.
These pin the behaviour judges care about: the rules engine catches what it
should and stays quiet on a clean report. No LLM, no network — pure and fast.
Run from the project root: pytest -q
"""
import os
import sys
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import schema # noqa: E402
from validators import run_all # noqa: E402
from sample_reports import CLEAN, BROKEN # noqa: E402
def _by_category(findings):
return {f.category for f in findings}
def _with(**overrides):
"""A copy of the clean report with specific fields overridden."""
return {**CLEAN, **overrides}
def test_clean_report_has_no_findings():
findings = run_all(CLEAN)
assert findings == [], f"clean report should be silent, got: {findings}"
def test_broken_report_flags_all_three():
findings = run_all(BROKEN)
cats = _by_category(findings)
assert "INCONSISTENT_DATA" in cats # speed mismatch + FO ROB drift
assert "OUT_OF_RANGE" in cats # wind force 14 Bf
def test_speed_mismatch_detected():
findings = run_all(BROKEN)
msgs = " ".join(f.message.lower() for f in findings)
assert "speed" in msgs
def test_fo_rob_drift_is_critical():
findings = run_all(BROKEN)
rob = [f for f in findings if "ROB" in f.field]
assert rob, "expected an FO ROB reconciliation finding"
assert any(f.severity == "CRITICAL" for f in rob)
def test_wind_force_out_of_range():
findings = run_all(BROKEN)
assert any(f.category == "OUT_OF_RANGE" and "Wind" in f.field for f in findings)
def test_missing_required_field_is_critical():
incomplete = {k: v for k, v in CLEAN.items() if k != "fo_rob"}
findings = run_all(incomplete)
assert any(f.category == "MISSING_DATA" and f.severity == "CRITICAL"
for f in findings)
def test_findings_sorted_critical_first():
findings = run_all(BROKEN)
order = {"CRITICAL": 0, "ERROR": 1, "WARNING": 2}
sev = [order[f.severity] for f in findings]
assert sev == sorted(sev), "findings should be sorted CRITICAL-first"
# --- advanced checks -------------------------------------------------------
def test_me_fo_rate_out_of_band():
# 240 mt over 24 h = 10 mt/h, well above the band -> WARNING.
findings = run_all(_with(me_fo_cons=240.0, fo_rob_prev=None))
assert any("ME FO" in f.field and "mt/h" in f.message for f in findings)
def test_me_fo_rate_in_band_is_silent():
# CLEAN burns 38.5 over 24 h = 1.6 mt/h -> inside band, no rate finding.
findings = run_all(CLEAN)
assert not any("mt/h" in f.message for f in findings)
def test_high_slip_in_calm_water():
findings = run_all(_with(slip_pct=25.0, wind_force=2.0))
assert any(f.field == "Slip" and "calm" in f.message.lower()
for f in findings)
def test_high_speed_in_heavy_weather():
# keep distance/hours consistent with the higher speed so only the
# weather rule fires, not the speed/distance mismatch.
findings = run_all(_with(avg_speed=15.0, distance_run=360.0, wind_force=9.0))
assert any(f.field == "Average speed" and "heavy weather" in f.message
for f in findings)
def test_slip_reconciliation_disabled_without_pitch():
# Default pitch is None -> the RPM x pitch reconciliation must not fire,
# even with an absurd reported slip.
findings = run_all(_with(slip_pct=20.0))
assert not any("computed from" in f.message for f in findings)
def test_slip_reconciliation_fires_with_pitch(monkeypatch):
monkeypatch.setattr(schema, "PROP_PITCH_M", 5.6)
# geometry implies ~1% slip; reporting 20% is a clear disagreement.
findings = run_all(_with(slip_pct=20.0))
assert any(f.field == "Slip" and "computed from" in f.message
for f in findings)