Files
WealthySmart/backend/tests/test_agent_tools.py
Carlos Escalante 965e30a2d2
All checks were successful
Deploy to VPS / test (push) Successful in 1m32s
Deploy to VPS / deploy (push) Successful in 14s
Agent: add get_daily_spending for day-scoped questions
Prod showed the failure mode after the date fix: get_current_date
returned the right day, but the model answered "¿cuánto he gastado
hoy?" from cycle-level aggregates (the only spend tools it had) and
concluded ₡0. Give it a day-grained tool — per-day CRC totals mirroring
/analytics/daily-spending (COMPRA only, no installment anchors, no
future cuotas) — and a prompt rule to match tool to time grain.
Verified: fresh thread now calls get_current_date →
get_daily_spending(2026-07-04, 2026-07-05) and answers correctly.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-04 18:36:48 -06:00

217 lines
6.0 KiB
Python

"""Agent tool coverage: session binding, JSON-safe output (no Decimal leaks),
and the batched queries introduced in Phase 2."""
import json
from datetime import date, datetime
from decimal import Decimal
import pytest
from app.agent import tools
from app.agent.tools import reset_session, set_session
from app.models.models import (
Account,
AccountType,
Bank,
Currency,
MunicipalReceipt,
PensionSnapshot,
Transaction,
WaterMeterReading,
)
@pytest.fixture()
def bound_session(session):
token = set_session(session)
yield session
reset_session(token)
def make_snapshot(fund: Bank, start: date, end: date, saldo: str) -> PensionSnapshot:
zero = Decimal("0")
return PensionSnapshot(
fund=fund,
contract_number="C-1",
period_start=start,
period_end=end,
saldo_anterior=zero,
aportes=zero,
rendimientos=zero,
retiros=zero,
traslados=zero,
comision=zero,
correccion=zero,
bonificacion=zero,
saldo_final=Decimal(saldo),
source_filename="t.pdf",
)
def make_receipt(period: str) -> MunicipalReceipt:
zero = Decimal("0")
return MunicipalReceipt(
receipt_date=date(2026, 4, 1),
due_date=date(2026, 4, 15),
period=period,
account="A1",
finca="F1",
holder_name="x",
holder_cedula="x",
holder_address="x",
subtotal=Decimal("100"),
interests=zero,
iva=Decimal("13"),
total=Decimal("113"),
source_filename="r.pdf",
)
def test_unbound_session_raises(session):
with pytest.raises(RuntimeError, match="not bound to agent context"):
tools.get_accounts()
def test_accounts_output_is_json_safe(bound_session):
bound_session.add(
Account(
bank=Bank.BAC,
currency=Currency.CRC,
label="Main",
balance=Decimal("1234.56"),
account_type=AccountType.BANK,
)
)
bound_session.commit()
out = tools.get_accounts()
json.dumps(out) # would raise on Decimal leakage
assert out[0]["balance"] == 1234.56
def test_pension_latest_only_per_fund(bound_session):
bound_session.add(
make_snapshot(Bank.FCL, date(2026, 1, 1), date(2026, 1, 31), "100")
)
bound_session.add(
make_snapshot(Bank.FCL, date(2026, 2, 1), date(2026, 2, 28), "200")
)
bound_session.add(
make_snapshot(Bank.ROP, date(2026, 1, 1), date(2026, 1, 31), "900")
)
bound_session.commit()
out = tools.get_pension_snapshots(latest_only=True)
json.dumps(out)
assert len(out) == 2
by_fund = {o["fund"]: o for o in out}
assert by_fund["FCL"]["saldo_final"] == 200.0 # latest period wins
assert by_fund["ROP"]["saldo_final"] == 900.0
full = tools.get_pension_snapshots(latest_only=False)
assert len(full) == 3
def test_municipal_receipts_batched_consumption(bound_session):
r1, r2 = make_receipt("2026-03"), make_receipt("2026-04")
r2.account = "A2"
bound_session.add(r1)
bound_session.add(r2)
bound_session.commit()
bound_session.refresh(r1)
bound_session.refresh(r2)
for rid, m3 in ((r1.id, "10.5"), (r1.id, "4.5"), (r2.id, "7")):
bound_session.add(
WaterMeterReading(
meter_id="7335",
period=f"p{m3}",
consumption_m3=Decimal(m3),
receipt_id=rid,
)
)
bound_session.commit()
out = tools.get_municipal_receipts()
json.dumps(out)
by_account = {o["account"]: o for o in out}
assert by_account["A1"]["water_consumption_m3"] == 15.0
assert by_account["A2"]["water_consumption_m3"] == 7.0
def test_recent_transactions_json_safe(bound_session):
bound_session.add(
Transaction(
amount=Decimal("42.42"),
merchant="M",
date=datetime(2026, 4, 1),
)
)
bound_session.commit()
out = tools.get_recent_transactions()
json.dumps(out)
assert out[0]["amount"] == 42.42
def test_recent_transactions_exclude_future_cuotas(bound_session):
from datetime import timedelta
from app.timeutil import today_cr
today = datetime.combine(today_cr(), datetime.min.time())
bound_session.add(
Transaction(amount=Decimal("10"), merchant="BILLED", date=today)
)
bound_session.add(
Transaction(
amount=Decimal("20"),
merchant="FUTURE CUOTA:03/06",
date=today + timedelta(days=45),
)
)
bound_session.commit()
merchants = [t["merchant"] for t in tools.get_recent_transactions()]
assert "BILLED" in merchants
assert "FUTURE CUOTA:03/06" not in merchants
def test_get_current_date_is_costa_rica_and_cycle_consistent():
from app.timeutil import today_cr
out = tools.get_current_date()
today = today_cr()
assert out["date"] == today.isoformat()
# The active cycle must contain today: [start, end)
start, end = out["cycle_range"]
assert start <= out["date"] < end
assert start.endswith("-18") and end.endswith("-18")
def test_daily_spending_day_scoped(bound_session):
from datetime import timedelta
from app.timeutil import today_cr
today = datetime.combine(today_cr(), datetime.min.time())
bound_session.add(
Transaction(amount=Decimal("100"), merchant="TODAY-A", date=today.replace(hour=9))
)
bound_session.add(
Transaction(amount=Decimal("50"), merchant="TODAY-B", date=today.replace(hour=15))
)
bound_session.add(
Transaction(
amount=Decimal("999"),
merchant="FUTURE CUOTA",
date=today + timedelta(days=40),
)
)
bound_session.commit()
out = tools.get_daily_spending(
start_date=today.date().isoformat(),
end_date=(today.date() + timedelta(days=60)).isoformat(),
)
assert len(out) == 1
assert out[0]["date"] == today.date().isoformat()
assert out[0]["total_crc"] == 150.0
assert out[0]["count"] == 2