Feat/math validation agent (#6012)

Co-authored-by: James Brunton <[email protected]>
Co-authored-by: EthanHealy01 <[email protected]>
This commit is contained in:
ConnorYoh
2026-04-17 10:36:45 +01:00
committed by GitHub
co-authored by James Brunton EthanHealy01
parent 688f7f2013
commit de8c483054
49 changed files with 3726 additions and 17 deletions
View File
@@ -0,0 +1,133 @@
"""
ArithmeticScanner — unit tests.
Tests cover the two inline arithmetic patterns the scanner targets:
1. Equals expressions: A + B = C
2. Total-then-addends: Total: C (A + B)
"""
from decimal import Decimal
import pytest
from stirling.agents.ledger.validators.arithmetic import ArithmeticScanner
@pytest.fixture
def scanner() -> ArithmeticScanner:
return ArithmeticScanner(tolerance=Decimal("0.01"))
# ---------------------------------------------------------------------------
# Equals expressions: A + B + C = D
# ---------------------------------------------------------------------------
def test_correct_equals_expression(scanner: ArithmeticScanner) -> None:
"""A correct sum should produce no findings."""
text = "The total cost is 100 + 200 + 150 = 450."
assert scanner.scan(page=0, text=text) == []
def test_wrong_equals_expression(scanner: ArithmeticScanner) -> None:
"""An incorrect sum should produce one error discrepancy."""
text = "Revenue: 500 + 300 = 900" # should be 800
discrepancies = scanner.scan(page=3, text=text)
assert len(discrepancies) == 1
d = discrepancies[0]
assert d.page == 3
assert d.kind == "arithmetic"
assert d.severity == "error"
assert d.stated == "900"
assert d.expected == "800"
def test_subtraction_expression(scanner: ArithmeticScanner) -> None:
"""Subtraction in expressions should be evaluated correctly."""
text = "Net: 1000 - 250 = 750"
assert scanner.scan(page=0, text=text) == []
def test_wrong_subtraction(scanner: ArithmeticScanner) -> None:
text = "Net: 1000 - 250 = 800" # should be 750
discrepancies = scanner.scan(page=0, text=text)
assert len(discrepancies) == 1
assert discrepancies[0].expected == "750"
def test_currency_symbols_stripped(scanner: ArithmeticScanner) -> None:
"""Currency symbols and thousand separators must not break parsing."""
text = "Total: £1,000 + £500 = £1,500"
assert scanner.scan(page=0, text=text) == []
def test_multiple_expressions_in_text(scanner: ArithmeticScanner) -> None:
"""Multiple expressions in the same text should each be evaluated."""
text = (
"Q1 revenue: 100 + 200 = 300. "
"Q2 revenue: 150 + 100 = 350. " # wrong: should be 250
)
discrepancies = scanner.scan(page=0, text=text)
assert len(discrepancies) == 1
assert discrepancies[0].expected == "250"
# ---------------------------------------------------------------------------
# Total-then-addends: "Total: X (A + B + C)"
# ---------------------------------------------------------------------------
def test_correct_total_then_addends(scanner: ArithmeticScanner) -> None:
text = "Grand Total: 750 (300 + 250 + 200)"
assert scanner.scan(page=0, text=text) == []
def test_wrong_total_then_addends(scanner: ArithmeticScanner) -> None:
text = "Grand Total: 900 (300 + 250 + 200)" # addends sum to 750
discrepancies = scanner.scan(page=0, text=text)
assert len(discrepancies) == 1
d = discrepancies[0]
assert d.stated == "900"
assert d.expected == "750"
def test_total_keyword_variations(scanner: ArithmeticScanner) -> None:
"""The pattern must work for 'Sum', 'Subtotal', 'Grand Total' etc."""
cases = [
("Sum: 600 (200 + 200 + 200)", True),
("Subtotal: 600 (200 + 200 + 200)", True),
("Total: 999 (200 + 200 + 200)", False), # wrong
]
for text, should_be_clean in cases:
result = scanner.scan(page=0, text=text)
if should_be_clean:
assert result == [], f"Expected clean for: {text!r}"
else:
assert len(result) == 1, f"Expected error for: {text!r}"
# ---------------------------------------------------------------------------
# Edge cases
# ---------------------------------------------------------------------------
def test_no_expressions_in_text(scanner: ArithmeticScanner) -> None:
text = "This paragraph discusses revenue trends but contains no arithmetic."
assert scanner.scan(page=0, text=text) == []
def test_empty_text(scanner: ArithmeticScanner) -> None:
assert scanner.scan(page=0, text="") == []
def test_leading_negative_expression(scanner: ArithmeticScanner) -> None:
"""Expressions starting with a negative number should evaluate correctly."""
text = "Adjustment: -100 + 250 = 150"
assert scanner.scan(page=0, text=text) == []
def test_leading_negative_wrong(scanner: ArithmeticScanner) -> None:
text = "Adjustment: -100 + 250 = 200" # should be 150
discrepancies = scanner.scan(page=0, text=text)
assert len(discrepancies) == 1
assert discrepancies[0].expected == "150"
+100
View File
@@ -0,0 +1,100 @@
"""
FigureTracker — unit tests.
Tests that named figures are correctly accumulated and that conflicting
sightings (same label, different value) are surfaced as consistency warnings.
"""
from decimal import Decimal
import pytest
from stirling.agents.ledger.validators.figures import FigureTracker
@pytest.fixture
def tracker() -> FigureTracker:
return FigureTracker(tolerance=Decimal("0.01"))
# ---------------------------------------------------------------------------
# No conflicts
# ---------------------------------------------------------------------------
def test_no_conflicts_single_figure(tracker: FigureTracker) -> None:
tracker.record("Net Profit", Decimal("1200.00"), page=3, raw="£1,200.00")
assert tracker.conflicts() == []
def test_no_conflicts_consistent_figure(tracker: FigureTracker) -> None:
"""The same figure cited identically on two pages must not raise a conflict."""
tracker.record("Total Revenue", Decimal("5000.00"), page=1, raw="£5,000")
tracker.record("Total Revenue", Decimal("5000.00"), page=8, raw="£5,000")
assert tracker.conflicts() == []
def test_no_conflicts_within_tolerance(tracker: FigureTracker) -> None:
"""A difference within tolerance must not be flagged."""
tracker.record("VAT", Decimal("100.00"), page=2, raw="£100.00")
tracker.record("VAT", Decimal("100.005"), page=5, raw="£100.005")
assert tracker.conflicts() == []
# ---------------------------------------------------------------------------
# Conflicts
# ---------------------------------------------------------------------------
def test_conflict_different_values(tracker: FigureTracker) -> None:
"""Same label, different value on two pages → one consistency warning."""
tracker.record("Net Profit", Decimal("1200.00"), page=3, raw="£1,200")
tracker.record("Net Profit", Decimal("1250.00"), page=7, raw="£1,250")
conflicts = tracker.conflicts()
assert len(conflicts) == 1
d = conflicts[0]
assert d.kind == "consistency"
assert d.severity == "warning"
assert d.page == 7 # later occurrence is flagged
def test_conflict_three_sightings_two_values(tracker: FigureTracker) -> None:
"""Three sightings where one differs from canonical → 1 conflict."""
tracker.record("Revenue", Decimal("1000"), page=1, raw="£1,000")
tracker.record("Revenue", Decimal("1000"), page=3, raw="£1,000")
tracker.record("Revenue", Decimal("999"), page=5, raw="£999")
conflicts = tracker.conflicts()
# Canonical=p1 (1000). p3 matches, p5 differs → 1 conflict
assert len(conflicts) == 1
assert conflicts[0].page == 5
# ---------------------------------------------------------------------------
# Label normalisation
# ---------------------------------------------------------------------------
def test_label_normalisation_case_insensitive(tracker: FigureTracker) -> None:
"""Labels must be compared case-insensitively."""
tracker.record("Net Profit", Decimal("1200"), page=2, raw="1200")
tracker.record("net profit", Decimal("1100"), page=4, raw="1100")
assert len(tracker.conflicts()) == 1
def test_label_normalisation_punctuation(tracker: FigureTracker) -> None:
"""Colons and dashes in labels must be normalised before comparison."""
tracker.record("Total Revenue:", Decimal("5000"), page=1, raw="5000")
tracker.record("Total Revenue —", Decimal("4000"), page=9, raw="4000")
assert len(tracker.conflicts()) == 1
# ---------------------------------------------------------------------------
# Entry count
# ---------------------------------------------------------------------------
def test_entry_count(tracker: FigureTracker) -> None:
tracker.record("A", Decimal("1"), page=0, raw="1")
tracker.record("A", Decimal("1"), page=1, raw="1")
tracker.record("B", Decimal("2"), page=2, raw="2")
assert tracker.entry_count == 3
@@ -0,0 +1,205 @@
"""
FormulaEvaluator — unit tests.
Tests cover:
- Operator precedence (* / before + -)
- Column reference replacement (colN with word boundaries)
- Negative number handling
- each_row, column_total, and single_cell scopes
"""
from decimal import Decimal
import pytest
from stirling.agents.ledger.validators.formula import FormulaEvaluator
@pytest.fixture
def evaluator() -> FormulaEvaluator:
return FormulaEvaluator(tolerance=Decimal("0.01"))
# ---------------------------------------------------------------------------
# _safe_eval — operator precedence
# ---------------------------------------------------------------------------
def test_safe_eval_addition(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("2 + 3") == Decimal("5")
def test_safe_eval_multiplication_before_addition(evaluator: FormulaEvaluator) -> None:
"""2 + 3 * 4 should be 14, not 20."""
assert evaluator._safe_eval("2 + 3 * 4") == Decimal("14")
def test_safe_eval_division_before_subtraction(evaluator: FormulaEvaluator) -> None:
"""10 - 6 / 2 should be 7, not 2."""
assert evaluator._safe_eval("10 - 6 / 2") == Decimal("7")
def test_safe_eval_mixed_precedence(evaluator: FormulaEvaluator) -> None:
"""1 + 2 * 3 - 4 / 2 should be 1 + 6 - 2 = 5."""
assert evaluator._safe_eval("1 + 2 * 3 - 4 / 2") == Decimal("5")
def test_safe_eval_all_multiplication(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("2 * 3 * 4") == Decimal("24")
def test_safe_eval_division_by_zero(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("10 / 0") is None
def test_safe_eval_negative_result(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("3 - 5") == Decimal("-2")
def test_safe_eval_leading_negative(evaluator: FormulaEvaluator) -> None:
"""Expressions starting with a negative number should work."""
result = evaluator._safe_eval("-100 + 200")
assert result == Decimal("100")
def test_safe_eval_empty(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("") is None
def test_safe_eval_single_number(evaluator: FormulaEvaluator) -> None:
assert evaluator._safe_eval("42") == Decimal("42")
def test_safe_eval_decimal_numbers(evaluator: FormulaEvaluator) -> None:
result = evaluator._safe_eval("1.5 * 2 + 0.5")
assert result == Decimal("3.5")
# ---------------------------------------------------------------------------
# colN replacement — word boundary safety
# ---------------------------------------------------------------------------
def test_col1_does_not_corrupt_col12(evaluator: FormulaEvaluator) -> None:
"""col1 replacement must not alter col12."""
csv = "a,b,c,d,e,f,g,h,i,j,k,l,m\n0,10,0,0,0,0,0,0,0,0,0,0,120\n"
# col1=10, col12=120 → col12 - col1 should be 110
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="col0 = col12 - col1",
scope="each_row",
description="test",
)
# row 1: col0=0, expected=120-10=110 → discrepancy
assert len(result) == 1
assert result[0].expected == "110"
def test_col_replacement_adjacent_columns(evaluator: FormulaEvaluator) -> None:
"""col1 and col10 should both be replaced correctly."""
csv = "a,b,c,d,e,f,g,h,i,j,k\n55,5,0,0,0,0,0,0,0,0,50\n"
# col0=55, col1=5, col10=50 → col1 + col10 = 55
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="col0 = col1 + col10",
scope="each_row",
description="test",
)
assert result == [] # 5 + 50 = 55, matches col0
# ---------------------------------------------------------------------------
# each_row scope
# ---------------------------------------------------------------------------
def test_each_row_correct(evaluator: FormulaEvaluator) -> None:
csv = "Item,Qty,Price,Total\nWidget,10,5,50\nGadget,3,20,60\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="col3 = col1 * col2",
scope="each_row",
description="unit price check",
)
assert result == []
def test_each_row_error(evaluator: FormulaEvaluator) -> None:
csv = "Item,Qty,Price,Total\nWidget,10,5,50\nGadget,3,20,99\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="col3 = col1 * col2",
scope="each_row",
description="unit price check",
)
assert len(result) == 1
assert result[0].expected == "60"
assert result[0].stated == "99"
# ---------------------------------------------------------------------------
# column_total scope
# ---------------------------------------------------------------------------
def test_column_total_correct(evaluator: FormulaEvaluator) -> None:
csv = "Name,Amount\nA,100\nB,200\nTotal,300\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="sum",
scope="column_total",
description="total check",
target_row=3,
target_col=1,
)
assert result == []
def test_column_total_error(evaluator: FormulaEvaluator) -> None:
csv = "Name,Amount\nA,100\nB,200\nTotal,400\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="sum",
scope="column_total",
description="total check",
target_row=3,
target_col=1,
)
assert len(result) == 1
assert result[0].expected == "300"
# ---------------------------------------------------------------------------
# single_cell scope
# ---------------------------------------------------------------------------
def test_single_cell_correct(evaluator: FormulaEvaluator) -> None:
csv = "A,B,C\n10,20,30\n5,15,20\n15,35,50\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="cell(3,2) = cell(1,2) + cell(2,2)",
scope="single_cell",
description="grand total",
)
assert result == []
def test_single_cell_error(evaluator: FormulaEvaluator) -> None:
csv = "A,B,C\n10,20,30\n5,15,20\n15,35,99\n"
result = evaluator.evaluate(
page=0,
table_csv=csv,
formula="cell(3,2) = cell(1,2) + cell(2,2)",
scope="single_cell",
description="grand total",
)
assert len(result) == 1
assert result[0].expected == "50"
+144
View File
@@ -0,0 +1,144 @@
"""
Ledger models — unit tests for serialisation and business logic.
These tests confirm the wire contract: models round-trip through JSON
correctly and their helper properties behave as documented.
"""
import pytest
from pydantic import ValidationError
from stirling.contracts.ledger import (
Discrepancy,
DiscrepancyKind,
Evidence,
Folio,
FolioManifest,
FolioType,
Requisition,
Severity,
Verdict,
)
# ---------------------------------------------------------------------------
# FolioManifest
# ---------------------------------------------------------------------------
def test_folio_manifest_round_trip() -> None:
manifest = FolioManifest(
session_id="abc-123",
page_count=3,
folio_types=[FolioType.TEXT, FolioType.IMAGE, FolioType.MIXED],
)
reloaded = FolioManifest.model_validate_json(manifest.model_dump_json())
assert reloaded == manifest
def test_folio_manifest_round_bounds() -> None:
with pytest.raises(ValidationError):
FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=0)
with pytest.raises(ValidationError):
FolioManifest(session_id="x", page_count=1, folio_types=[FolioType.TEXT], round=4)
# ---------------------------------------------------------------------------
# Requisition
# ---------------------------------------------------------------------------
def test_requisition_empty() -> None:
req = Requisition(rationale="nothing needed")
assert req.need_text == []
assert req.need_tables == []
assert req.need_ocr == []
def test_requisition_type_discriminator() -> None:
req = Requisition(need_text=[0, 1], rationale="needs text")
assert req.type == "requisition"
# ---------------------------------------------------------------------------
# Folio.readable_text
# ---------------------------------------------------------------------------
def test_folio_readable_text_prefers_ocr() -> None:
folio = Folio(page=0, text="digital text", ocr_text="ocr text")
assert folio.readable_text == "ocr text"
def test_folio_readable_text_falls_back_to_text() -> None:
folio = Folio(page=0, text="digital text")
assert folio.readable_text == "digital text"
def test_folio_readable_text_empty_when_none() -> None:
folio = Folio(page=0)
assert folio.readable_text == ""
# ---------------------------------------------------------------------------
# Verdict
# ---------------------------------------------------------------------------
def test_verdict_clean_flag() -> None:
verdict = Verdict(
session_id="s1",
discrepancies=[],
pages_examined=[0, 1],
rounds_taken=2,
summary="All figures balance.",
clean=True,
)
assert verdict.error_count == 0
assert verdict.warning_count == 0
assert verdict.clean is True
def test_verdict_error_and_warning_counts() -> None:
discrepancies = [
Discrepancy(
page=0,
kind=DiscrepancyKind.TALLY,
severity=Severity.ERROR,
description="bad sum",
stated="100",
expected="110",
),
Discrepancy(
page=1,
kind=DiscrepancyKind.CONSISTENCY,
severity=Severity.WARNING,
description="mismatched figure",
stated="500",
expected="550",
),
]
verdict = Verdict(
session_id="s1",
discrepancies=discrepancies,
pages_examined=[0, 1],
rounds_taken=1,
summary="Issues found.",
clean=False,
)
assert verdict.error_count == 1
assert verdict.warning_count == 1
# ---------------------------------------------------------------------------
# Evidence.final_round
# ---------------------------------------------------------------------------
def test_evidence_final_round() -> None:
evidence = Evidence(
session_id="s",
folios=[Folio(page=0, text="hello")],
round=3,
final_round=True,
)
assert evidence.final_round is True
+249
View File
@@ -0,0 +1,249 @@
"""
Ledger Auditor — FastAPI route tests.
Uses FastAPI's TestClient with dependency overrides. All LLM calls are
mocked out; these tests exercise HTTP parsing, serialisation, and response
enveloping only — not the agent's reasoning.
"""
from __future__ import annotations
from collections.abc import Iterator
from decimal import Decimal
import pytest
from fastapi.testclient import TestClient
from stirling.api import app
from stirling.api.dependencies import get_math_auditor_agent
from stirling.config import AppSettings, load_settings
from stirling.contracts.ledger import (
Discrepancy,
DiscrepancyKind,
Evidence,
FolioManifest,
Requisition,
Severity,
Verdict,
)
# ---------------------------------------------------------------------------
# Stubs
# ---------------------------------------------------------------------------
class StubSettingsProvider:
def __call__(self) -> AppSettings:
return AppSettings(
smart_model_name="test",
fast_model_name="test",
smart_model_max_tokens=8192,
fast_model_max_tokens=2048,
posthog_enabled=False,
posthog_api_key="",
posthog_host="https://eu.i.posthog.com",
)
class StubLedgerAgent:
"""Stub that returns canned responses without touching any model."""
def __init__(
self,
requisition: Requisition | None = None,
verdict: Verdict | None = None,
) -> None:
self._requisition = requisition or _stub_requisition()
self._verdict = verdict or _stub_verdict()
self.examine_calls: list[FolioManifest] = []
self.audit_calls: list[tuple[Evidence, Decimal]] = []
async def examine(self, manifest: FolioManifest) -> Requisition:
self.examine_calls.append(manifest)
return self._requisition
async def audit(self, evidence: Evidence, tolerance: Decimal = Decimal("0.01")) -> Verdict:
self.audit_calls.append((evidence, tolerance))
return self._verdict
# ---------------------------------------------------------------------------
# Helpers
# ---------------------------------------------------------------------------
def _stub_requisition() -> Requisition:
return Requisition(
need_text=[0, 2],
need_tables=[0],
need_ocr=[1],
rationale="Page 1 is image-only; pages 0 and 2 have financial text.",
)
def _stub_verdict(
clean: bool = True,
discrepancies: list[Discrepancy] | None = None,
) -> Verdict:
return Verdict(
session_id="test-session",
discrepancies=discrepancies or [],
pages_examined=[0, 2],
rounds_taken=2,
summary="No errors found." if clean else "1 tally error found.",
clean=clean,
)
def _manifest_body(**overrides: object) -> dict[str, object]:
base: dict[str, object] = {
"sessionId": "test-session",
"pageCount": 3,
"folioTypes": ["text", "image", "mixed"],
"round": 1,
}
return {**base, **overrides}
def _evidence_body(**overrides: object) -> dict[str, object]:
base: dict[str, object] = {
"sessionId": "test-session",
"folios": [
{"page": 0, "text": "Fee: £100\nTax: £20\nTotal: £120"},
{"page": 2, "text": "Summary: all tallies correct"},
],
"round": 2,
"finalRound": False,
}
return {**base, **overrides}
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture
def stub_agent() -> StubLedgerAgent:
return StubLedgerAgent()
@pytest.fixture
def client(stub_agent: StubLedgerAgent) -> Iterator[TestClient]:
app.dependency_overrides[load_settings] = StubSettingsProvider()
app.dependency_overrides[get_math_auditor_agent] = lambda: stub_agent
yield TestClient(app, raise_server_exceptions=False)
app.dependency_overrides.pop(load_settings, None)
app.dependency_overrides.pop(get_math_auditor_agent, None)
# ---------------------------------------------------------------------------
# POST /api/v1/ai/math-auditor-agent/examine
# ---------------------------------------------------------------------------
class TestExamineEndpoint:
"""Tests for POST /api/v1/ai/math-auditor-agent/examine."""
def test_returns_200(self, client: TestClient) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body())
assert resp.status_code == 200
def test_response_is_requisition(self, client: TestClient) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body())
body = resp.json()
assert body["type"] == "requisition"
assert body["needText"] == [0, 2]
assert body["needTables"] == [0]
assert body["needOcr"] == [1]
assert "rationale" in body
def test_examine_called_with_parsed_manifest(
self,
client: TestClient,
stub_agent: StubLedgerAgent,
) -> None:
client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body(sessionId="my-session", pageCount=3))
assert len(stub_agent.examine_calls) == 1
manifest = stub_agent.examine_calls[0]
assert manifest.session_id == "my-session"
assert manifest.page_count == 3
def test_content_type_is_json(self, client: TestClient) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/examine", json=_manifest_body())
assert "application/json" in resp.headers["content-type"]
# ---------------------------------------------------------------------------
# POST /api/v1/ai/math-auditor-agent/deliberate
# ---------------------------------------------------------------------------
class TestDeliberateEndpoint:
"""Tests for POST /api/v1/ai/math-auditor-agent/deliberate."""
def test_returns_200_clean(self, client: TestClient) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body())
assert resp.status_code == 200
def test_response_is_verdict(self, client: TestClient) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body())
body = resp.json()
assert body["type"] == "verdict"
assert body["clean"] is True
def test_discrepancies_serialised(self, client: TestClient) -> None:
d = Discrepancy(
page=0,
kind=DiscrepancyKind.TALLY,
severity=Severity.ERROR,
description="Column total wrong",
stated="250",
expected="300",
)
stub = StubLedgerAgent(verdict=_stub_verdict(clean=False, discrepancies=[d]))
app.dependency_overrides[get_math_auditor_agent] = lambda: stub
resp = client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body())
body = resp.json()
discrepancies = body["discrepancies"]
assert len(discrepancies) == 1
assert discrepancies[0]["kind"] == "tally"
assert discrepancies[0]["severity"] == "error"
assert discrepancies[0]["stated"] == "250"
assert discrepancies[0]["expected"] == "300"
def test_tolerance_query_param_forwarded(
self,
client: TestClient,
stub_agent: StubLedgerAgent,
) -> None:
client.post("/api/v1/ai/math-auditor-agent/deliberate?tolerance=0.05", json=_evidence_body())
assert len(stub_agent.audit_calls) == 1
_, tolerance = stub_agent.audit_calls[0]
assert tolerance == Decimal("0.05")
def test_default_tolerance_when_omitted(
self,
client: TestClient,
stub_agent: StubLedgerAgent,
) -> None:
client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body())
_, tolerance = stub_agent.audit_calls[0]
assert tolerance == Decimal("0.01")
def test_invalid_tolerance_returns_400(
self,
client: TestClient,
stub_agent: StubLedgerAgent,
) -> None:
resp = client.post("/api/v1/ai/math-auditor-agent/deliberate?tolerance=notanumber", json=_evidence_body())
assert resp.status_code == 400
def test_final_round_flag_parsed(
self,
client: TestClient,
stub_agent: StubLedgerAgent,
) -> None:
client.post("/api/v1/ai/math-auditor-agent/deliberate", json=_evidence_body(finalRound=True))
evidence, _ = stub_agent.audit_calls[0]
assert evidence.final_round is True