"""The schema-level anti-hallucination guarantee, and how a brief renders. The claim: a figure cannot enter the brief as prose. It must be a `{{cN}}` reference to a tool-computed value. These tests hold the schema to that. """ from __future__ import annotations import pytest from pydantic import ValidationError from app.graph.compose import compose_brief from app.graph.state import RunState from app.models.brief import ( Brief, KeyMove, NumericClaim, WatchItem, extract_claim_refs, strip_claim_refs, validate_narrative, ) from app.services.render import ( UNVERIFIED_MARKER, format_claim, render_html, render_markdown, substitute, ) def _brief(state: RunState) -> Brief: return compose_brief( session_date=str(state["session_date"]), tickers=list(state["tickers"]), metrics=state["metrics"], fundamentals=state["fundamentals"], sentiment=state["sentiment"], news=state["news"], risk_events=[], data_gaps=[], ) class TestNarrativeDigitRule: @pytest.mark.parametrize( "text", [ "The stock rose 5 percent", "Closed at 340.28", "Up 12%", "Revenue of $1B", "Q3 results", ], ) def test_bare_numerals_are_rejected(self, text: str) -> None: with pytest.raises(ValueError, match="bare numerals"): validate_narrative(text, "field") @pytest.mark.parametrize( "text", [ "The stock rose {{c1}} on the session", "Closed at {{c12}} after a {{c13}} move", "Coverage reads positive with no figures at all", "Over the trailing thirty-day window it advanced", ], ) def test_claim_references_are_accepted(self, text: str) -> None: assert validate_narrative(text, "field") == text def test_claim_ids_themselves_do_not_count_as_digits(self) -> None: """`{{c999}}` contains digits, but the whole token is stripped first.""" residue = strip_claim_refs("value {{c999}} here") assert not any(char.isdigit() for char in residue) assert residue.split() == ["value", "here"] assert validate_narrative("value {{c999}} here", "field") def test_extract_claim_refs_preserves_order(self) -> None: assert extract_claim_refs("{{c3}} then {{c1}} then {{c3}}") == ["c3", "c1", "c3"] def test_key_move_narrative_is_validated(self) -> None: with pytest.raises(ValidationError): KeyMove(ticker="AAPL", narrative="Rose 4 percent", direction="up") def test_watch_item_is_validated(self) -> None: with pytest.raises(ValidationError): WatchItem(ticker="AAPL", item="Watch the 200 day average") def test_brief_headline_is_validated(self) -> None: with pytest.raises(ValidationError): Brief( generated_for="2026-01-02", watchlist=["AAPL"], headline="AAPL up 4 percent", executive_summary="Fine.", ) class TestClaimIntegrity: def test_claim_id_format_is_enforced(self) -> None: with pytest.raises(ValidationError): NumericClaim( claim_id="not-a-claim", ticker="AAPL", metric="last_close", value=1.0, unit="usd" ) def test_unknown_metric_is_rejected(self) -> None: with pytest.raises(ValidationError): NumericClaim(claim_id="c1", ticker="AAPL", metric="made_up", value=1.0, unit="usd") def test_ticker_is_normalised(self) -> None: claim = NumericClaim( claim_id="c1", ticker=" aapl ", metric="last_close", value=1.0, unit="usd" ) assert claim.ticker == "AAPL" def test_extra_fields_are_forbidden(self) -> None: with pytest.raises(ValidationError): NumericClaim( claim_id="c1", ticker="AAPL", metric="last_close", value=1.0, unit="usd", smuggled="payload", # type: ignore[call-arg] ) def test_every_reference_in_a_real_brief_resolves(self, live_state: RunState) -> None: brief = _brief(live_state) defined = set(brief.claims_by_id()) assert brief.referenced_claim_ids() <= defined class TestRendering: def test_units_render_correctly(self) -> None: assert ( format_claim( NumericClaim( claim_id="c1", ticker="A", metric="last_close", value=340.2, unit="usd" ) ) == "$340.20" ) assert ( format_claim( NumericClaim( claim_id="c2", ticker="A", metric="change_1d_pct", value=1.5, unit="percent" ) ) == "+1.50%" ) assert ( format_claim( NumericClaim(claim_id="c3", ticker="A", metric="pe_ratio", value=23.5, unit="ratio") ) == "23.50" ) def test_substitution_uses_verified_values(self, live_state: RunState) -> None: brief = _brief(live_state) claims = brief.claims_by_id() rendered = substitute(brief.key_moves[0].narrative, claims) if brief.key_moves else "" assert "{{" not in rendered def test_unknown_reference_renders_as_unverified(self) -> None: assert substitute("value {{c404}}", {}) == f"value {UNVERIFIED_MARKER}" def test_markdown_contains_the_governance_statement(self, live_state: RunState) -> None: markdown = render_markdown(_brief(live_state)) assert markdown.startswith("# AlphaBrief") assert "independently" in markdown assert "human approval" in markdown assert "{{" not in markdown def test_html_escapes_third_party_text(self, live_state: RunState) -> None: brief = _brief(live_state) blocks = [b.model_copy() for b in brief.news_and_sentiment] if blocks: blocks[0] = blocks[0].model_copy( update={"top_headline": ""} ) brief = brief.model_copy(update={"news_and_sentiment": blocks}) html = render_html(brief) assert "