Noon report sanity-checker: app, deterministic validators, LLM layer, tests, demo data, field notes
0846808 | """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) | |