# Author: Simon-Pierre Boucher — simon-pierre.boucher@uqo.ca
from __future__ import annotations
import json
from datetime import date
import pytest
from agentic_valuation.agents.packets import build_packet, committee_state, render_user_prompt, role_view, verify_evidence, visible_ids
from agentic_valuation.agents.roles import get_role
from agentic_valuation.schemas import Adjustment, AgentEstimate, Comparable
from tests.conftest_agents import make_comps, make_geo, make_market, make_row, packet, pool # noqa: F401
def test_packet_never_contains_price_or_street(packet):
js = packet.model_dump_json()
assert "487123" not in js and "street" not in js.lower() and "secr" not in js.lower()
assert "price" not in packet.subject.model_dump() or packet.subject.model_dump().get("price") is None
assert packet.subject.lat == 45.512 and packet.subject.lng == -73.612
assert packet.subject.information_cutoff == date(2025, 5, 9)
assert packet.evidence_ids() == {"P", "M1", "G1", *[f"C{i:02d}" for i in range(1, 7)]}
def test_missing_fields_and_leakage_guards():
row = make_row(floor_area_m2=None, year_built=None, prior_sale_price=300000.0, prior_sale_date=date(2025, 5, 9))
comps = make_comps() + [Comparable(evidence_id="C99", sale_date=date(2025, 5, 10), price=1.0, distance_km=0.1, property_class="house", municipality="X", months_before_valuation=0.0)]
pk = build_packet(row, comps, make_market(), None, {"comparables": {"max_comps": 12}})
assert "floor_area_m2" in pk.subject.missing_fields and "year_built" in pk.subject.missing_fields
assert all(c.sale_date <= pk.subject.information_cutoff for c in pk.comparables)
assert [c.evidence_id for c in pk.comparables] == [f"C{i:02d}" for i in range(1, 7)]
assert pk.subject.prior_sale_price is None # prior sale on/after the cutoff is dropped
assert pk.geo is None and any("No geospatial" in n for n in pk.notes)
def test_role_view_filtering(packet):
v = role_view(packet, get_role("building_condition"))
assert "subject" in v and "comparables" not in v and "market" not in v and "geo" not in v
assert v["evidence_ids_visible"] == ["P"]
v2 = role_view(packet, get_role("market_economist"))
assert set(v2["evidence_ids_visible"]) == {"P", "M1"}
v3 = role_view(packet, get_role("skeptic"))
assert set(v3["evidence_ids_visible"]) == packet.evidence_ids()
assert [c["evidence_id"] for c in v3["comparables"]] == [c.evidence_id for c in packet.comparables]
assert visible_ids(packet, get_role("comparables")) == {"P", *[f"C{i:02d}" for i in range(1, 7)]}
def test_render_user_prompt_blocks(packet):
v = role_view(packet, get_role("hedonic"))
p = render_user_prompt(v, None, kind="estimate")
assert "" in p and "" in p and "AgentEstimate" in p
est = AgentEstimate(point_estimate=500000, q05=420000, q25=470000, q50=500000, q75=530000, q95=590000, evidence_ids_used=["P", "C01"], reasoning_summary="x")
state = committee_state(1, {"hedonic": est, "comparables": est}, for_role="hedonic", critiques=[{"from": "committee_member", "agree": False, "points": ["check C01"]}], visible={"P"})
p2 = render_user_prompt(v, state, kind="revision", verification_flags=["unknown_evidence_id:M1"])
assert '"committee_median":500000' in p2.replace(" ", "")
assert "" in p2 and "" in p2
inner = p2.split("")[1].split("")[0]
js = json.loads(inner)
assert js["estimates"][0]["role"] == "hedonic" and js["evidence_ids_cited_by_others_outside_your_view"] == ["C01"]
assert js["critiques_addressed_to_you"][0]["points"] == ["check C01"]
with pytest.raises(ValueError):
render_user_prompt(v, None, kind="nope")
def test_verify_evidence_flags(packet):
good = AgentEstimate(point_estimate=470000, q05=400000, q25=450000, q50=470000, q75=490000, q95=540000, evidence_ids_used=["P", "C01"], adjustments=[Adjustment(comparable_id="C01", adjusted_price=460000, reason="time")])
assert verify_evidence(good, packet) == []
bad = AgentEstimate(
point_estimate=900000, q05=800000, q25=850000, q50=900000, q75=950000, q95=990000, evidence_ids_used=["P", "C77", "Z1"],
adjustments=[Adjustment(comparable_id="C01", adjusted_price=800000, reason="x"), Adjustment(comparable_id="C50", adjusted_price=1, reason="x")],
)
flags = verify_evidence(bad, packet)
assert "unknown_evidence_id:C77" in flags and "unknown_evidence_id:Z1" in flags
assert "adjustment_out_of_band:C01" in flags and "unknown_comparable:C50" in flags
assert "point_outside_comparable_hull" in flags
# citing an id outside the role's view is flagged when allowed_ids is given
assert verify_evidence(good, packet, allowed_ids={"P"}) == ["unknown_evidence_id:C01"]