examples: simulator response gains examples_overlay block
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful

Adds an informational `examples_overlay` field to SimulateResult,
populated from `summary_for_country` for the scenario's target country.
Never affects simulation paths — lookup failures are caught and logged,
yielding overlay=None. Wired into both /simulate and /compare; the
shared session in /compare is used sequentially because AsyncSession
is not safe for concurrent use.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Viktor Barzin 2026-05-28 22:45:15 +00:00
parent 249991557b
commit 9b32247fea
3 changed files with 146 additions and 8 deletions

View file

@ -6,6 +6,7 @@ point, the point is the endpoint produces a valid response shape.
from __future__ import annotations
from collections.abc import AsyncIterator
from datetime import date
from decimal import Decimal
import pytest_asyncio
@ -14,6 +15,7 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker
from fire_planner.api.dependencies import get_session
from fire_planner.app import app
from fire_planner.db import FireExample
@pytest_asyncio.fixture
@ -165,3 +167,48 @@ async def test_compare_rejects_single_scenario(client: AsyncClient) -> None:
},
)
assert resp.status_code == 422 # pydantic validation
async def test_simulate_response_includes_examples_overlay(
client: AsyncClient, session: AsyncSession) -> None:
"""When examples exist for the scenario's target country, the
simulator response surfaces them as `examples_overlay` purely
informational, never affects the simulation path."""
for i, amount in enumerate([300_000, 400_000, 500_000], start=1):
session.add(FireExample(
reddit_id=f"th{i:02d}",
source_sub="ExpatFIRE",
post_url=f"https://reddit.com/r/ExpatFIRE/comments/th{i:02d}",
post_date=date(2026, 1, 1),
post_title=f"Thailand FIRE {i}",
country="Thailand",
portfolio_gbp=Decimal(str(amount)),
annual_exp_gbp=Decimal("24000"),
fi_status="FIRE",
llm_model="qwen3-8b",
))
await session.commit()
resp = await client.post(
"/simulate",
json={
"jurisdiction": "thailand",
"strategy": "trinity",
"leave_uk_year": 0,
"glide_path": "static_60_40",
"spending_gbp": "60000",
"nw_seed_gbp": "1500000",
"horizon_years": 30,
"n_paths": 100,
"seed": 42,
},
)
assert resp.status_code == 200, resp.text
body = resp.json()
assert "examples_overlay" in body
overlay = body["examples_overlay"]
assert overlay is not None
assert overlay["country"] == "Thailand"
assert overlay["count"] == 3
assert Decimal(overlay["portfolio_gbp_median"]) == Decimal("400000.00")
assert len(overlay["sample_links"]) == 3