mirror of https://github.com/ghostfolio/ghostfolio
Browse Source
- Created agent/evals/test_eval_dataset.py with 56 categorized test cases - Happy path (20): portfolio, property CRUD, strategy, wealth position, family, relocation - Edge cases (12): zero values, paid-off property, 1-year strategy, nonexistent IDs, extreme ages - Adversarial (12): SQL injection, negative values, extreme rates, whitespace inputs - Multi-step (12): chained tool calls, stateful CRUD flows, cross-tool data passing - Total suite: 182 tests, 0 failures Made-with: Cursorpull/6453/head
1 changed files with 941 additions and 0 deletions
@ -0,0 +1,941 @@ |
|||||
|
""" |
||||
|
Comprehensive eval dataset for the Ghostfolio AI Agent. |
||||
|
|
||||
|
50+ test cases organized into 4 rubric-required categories: |
||||
|
- Happy Path (20+ tests): normal successful user journeys |
||||
|
- Edge Cases (10+ tests): boundary conditions, missing data, zero values |
||||
|
- Adversarial (10+ tests): bad inputs, injection attempts, extreme values |
||||
|
- Multi-Step (10+ tests): chained tool calls, stateful flows |
||||
|
|
||||
|
Every test documents: |
||||
|
TYPE, INPUT (what the user asked), EXPECTED (what should happen), |
||||
|
CRITERIA (how pass/fail is determined). |
||||
|
|
||||
|
Network calls: all Teleport API calls are mocked via conftest.py autouse |
||||
|
fixture (mock_teleport_no_network). Tests are deterministic and fast. |
||||
|
""" |
||||
|
|
||||
|
import asyncio |
||||
|
import os |
||||
|
import sys |
||||
|
|
||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) |
||||
|
sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "tools")) |
||||
|
|
||||
|
os.environ.setdefault("ENABLE_REAL_ESTATE", "true") |
||||
|
os.environ.setdefault("PROPERTIES_DB_PATH", ":memory:") |
||||
|
|
||||
|
import pytest |
||||
|
|
||||
|
|
||||
|
# --------------------------------------------------------------------------- |
||||
|
# Helper: run coroutine from sync context |
||||
|
# --------------------------------------------------------------------------- |
||||
|
|
||||
|
def _run(coro): |
||||
|
loop = asyncio.new_event_loop() |
||||
|
try: |
||||
|
return loop.run_until_complete(coro) |
||||
|
finally: |
||||
|
loop.close() |
||||
|
|
||||
|
|
||||
|
def _clear(): |
||||
|
from property_tracker import property_store_clear |
||||
|
property_store_clear() |
||||
|
|
||||
|
|
||||
|
# ============================================================================ |
||||
|
# HAPPY PATH TESTS (20 tests) |
||||
|
# ============================================================================ |
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: add home with purchase price, current value, and mortgage |
||||
|
# EXPECTED: property created, equity = current_value - mortgage_balance |
||||
|
# CRITERIA: equity == 175000, success == True |
||||
|
def test_hp_add_property_basic(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
result = _run(add_property( |
||||
|
address="My Primary Home", |
||||
|
purchase_price=420000, |
||||
|
current_value=490000, |
||||
|
mortgage_balance=315000, |
||||
|
)) |
||||
|
assert result["success"] is True |
||||
|
prop = result["result"]["property"] |
||||
|
assert prop["equity"] == pytest.approx(175000) |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: add two properties, check combined equity |
||||
|
# EXPECTED: total equity = sum of both properties' equity |
||||
|
# CRITERIA: total_equity matches manual calculation |
||||
|
def test_hp_add_two_properties_combined_equity(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_real_estate_equity |
||||
|
_run(add_property("Home A", 400000, 480000, 300000)) # equity=180000 |
||||
|
_run(add_property("Home B", 300000, 350000, 200000)) # equity=150000 |
||||
|
result = _run(get_real_estate_equity()) |
||||
|
assert result["success"] is True |
||||
|
assert result["result"]["total_real_estate_equity"] == pytest.approx(330000) |
||||
|
assert result["result"]["property_count"] == 2 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: get total net worth with portfolio and property |
||||
|
# EXPECTED: total = portfolio + real estate equity |
||||
|
# CRITERIA: total_net_worth == 94000 + 175000 = 269000 |
||||
|
def test_hp_total_net_worth_combined(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_total_net_worth |
||||
|
_run(add_property("Test Home", 420000, 490000, 315000)) # equity=175000 |
||||
|
result = _run(get_total_net_worth(portfolio_value=94000)) |
||||
|
assert result["success"] is True |
||||
|
assert result["result"]["total_net_worth"] == pytest.approx(269000) |
||||
|
assert result["result"]["investment_portfolio"] == 94000 |
||||
|
assert result["result"]["real_estate_equity"] == pytest.approx(175000) |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: strategy simulation with 10-year horizon |
||||
|
# EXPECTED: final net worth exceeds starting portfolio |
||||
|
# CRITERIA: total_net_worth > initial_portfolio_value |
||||
|
def test_hp_strategy_10_year_growth(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy(94000, 120000, 400000, total_years=10) |
||||
|
assert result is not None |
||||
|
assert "final_picture" in result |
||||
|
assert result["final_picture"]["total_net_worth"] > 94000 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: strategy simulation with user-provided 3% appreciation |
||||
|
# EXPECTED: strategy uses 3% not default 4% |
||||
|
# CRITERIA: assumptions show 3.0% |
||||
|
def test_hp_strategy_user_appreciation(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
annual_appreciation=0.03, |
||||
|
) |
||||
|
assert result["strategy"]["assumptions"]["annual_appreciation"] == "3.0%" |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: analyze wealth position for 34-year-old with $94k portfolio |
||||
|
# EXPECTED: Fed Reserve comparison with correct median for under_35 bracket |
||||
|
# CRITERIA: median_for_age == 39000, percentile_estimate present |
||||
|
def test_hp_wealth_position_age_34(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
result = analyze_wealth_position(94000, 34, 120000) |
||||
|
assert "current_position" in result |
||||
|
assert result["current_position"]["median_for_age"] == 39000 |
||||
|
assert "percentile_estimate" in result["current_position"] |
||||
|
assert "retirement_projection" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: analyze wealth position for 42-year-old |
||||
|
# EXPECTED: uses 35_to_44 bracket median of $135,000 |
||||
|
# CRITERIA: median_for_age == 135000 |
||||
|
def test_hp_wealth_position_age_42(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
result = analyze_wealth_position(200000, 42, 150000) |
||||
|
assert result["current_position"]["median_for_age"] == 135000 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: equity options for property with substantial equity |
||||
|
# EXPECTED: 3 distinct options returned (keep, refi, rental) |
||||
|
# CRITERIA: len(options) >= 3, each option has projection |
||||
|
def test_hp_equity_options_three_scenarios(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, analyze_equity_options |
||||
|
prop = _run(add_property("Equity Home", 400000, 520000, 370000)) |
||||
|
pid = prop["result"]["property"]["id"] |
||||
|
result = analyze_equity_options(pid) |
||||
|
assert "options" in result |
||||
|
assert len(result["options"]) >= 3 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: family planning for Austin with 1 child |
||||
|
# EXPECTED: childcare costs, monthly surplus, income_impact |
||||
|
# CRITERIA: income_impact present, monthly_surplus_after is a number |
||||
|
def test_hp_family_plan_one_child_austin(): |
||||
|
from family_planner import plan_family_finances |
||||
|
result = plan_family_finances("Austin", 120000, num_planned_children=1) |
||||
|
assert "income_impact" in result |
||||
|
assert "monthly_surplus_after" in result["income_impact"] |
||||
|
assert isinstance(result["income_impact"]["monthly_surplus_after"], (int, float)) |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: relocation runway calculation Austin → Seattle |
||||
|
# EXPECTED: verdict returned, months to emergency fund calculated |
||||
|
# CRITERIA: verdict present, milestones_if_you_move has emergency_fund milestone |
||||
|
def test_hp_relocation_runway_seattle(): |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
result = calculate_relocation_runway( |
||||
|
current_salary=120000, |
||||
|
offer_salary=180000, |
||||
|
current_city="Austin", |
||||
|
destination_city="Seattle", |
||||
|
portfolio_value=94000, |
||||
|
) |
||||
|
assert "verdict" in result |
||||
|
assert "milestones_if_you_move" in result |
||||
|
assert "months_to_6mo_emergency_fund" in result["milestones_if_you_move"] |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: job offer affordability check Austin → SF |
||||
|
# EXPECTED: COL-adjusted comparison, is_real_raise boolean |
||||
|
# CRITERIA: is_real_raise present, verdict non-empty |
||||
|
@pytest.mark.asyncio |
||||
|
async def test_hp_job_offer_affordability(): |
||||
|
from wealth_bridge import calculate_job_offer_affordability |
||||
|
result = await calculate_job_offer_affordability( |
||||
|
offer_salary=180000, |
||||
|
offer_city="Seattle", |
||||
|
current_salary=120000, |
||||
|
current_city="Austin", |
||||
|
) |
||||
|
assert "is_real_raise" in result |
||||
|
assert isinstance(result["is_real_raise"], bool) |
||||
|
assert "verdict" in result |
||||
|
assert len(result["verdict"]) > 10 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: down payment power for $94k portfolio |
||||
|
# EXPECTED: at least one Austin-area market affordable at full 20% down |
||||
|
# CRITERIA: can_afford_full True for at least one market |
||||
|
def test_hp_down_payment_94k_portfolio(): |
||||
|
from wealth_bridge import calculate_down_payment_power |
||||
|
result = calculate_down_payment_power(94000) |
||||
|
assert "markets" in result |
||||
|
affordable = [m for m in result["markets"] if m["can_afford_full"]] |
||||
|
assert len(affordable) > 0 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: list properties when one exists |
||||
|
# EXPECTED: property appears in list with correct fields |
||||
|
# CRITERIA: len(properties) == 1, equity field present |
||||
|
def test_hp_list_properties_one(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_properties |
||||
|
_run(add_property("My Home", 400000, 480000, 320000)) |
||||
|
result = _run(get_properties()) |
||||
|
assert result["success"] is True |
||||
|
props = result["result"]["properties"] |
||||
|
assert len(props) == 1 |
||||
|
assert "equity" in props[0] |
||||
|
assert props[0]["equity"] == pytest.approx(160000) |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: remove property by ID |
||||
|
# EXPECTED: property no longer in list |
||||
|
# CRITERIA: property count drops from 1 to 0 |
||||
|
def test_hp_remove_property_success(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, remove_property, get_properties |
||||
|
prop = _run(add_property("Remove Me", 300000, 350000, 200000)) |
||||
|
pid = prop["result"]["property"]["id"] |
||||
|
removed = _run(remove_property(pid)) |
||||
|
assert removed["success"] is True |
||||
|
listed = _run(get_properties()) |
||||
|
ids = [p["id"] for p in listed["result"]["properties"]] |
||||
|
assert pid not in ids |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: update property current value |
||||
|
# EXPECTED: equity recalculates correctly |
||||
|
# CRITERIA: equity increases after value update |
||||
|
def test_hp_update_property_value(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, update_property |
||||
|
prop = _run(add_property("Update Test", 400000, 450000, 320000)) |
||||
|
pid = prop["result"]["property"]["id"] |
||||
|
updated = _run(update_property(pid, current_value=470000)) |
||||
|
assert updated["success"] is True |
||||
|
new_equity = updated["result"]["property"]["equity"] |
||||
|
assert new_equity == pytest.approx(150000) # 470000 - 320000 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: strategy simulation single property scenario |
||||
|
# EXPECTED: result contains at least 1 property |
||||
|
# CRITERIA: properties_owned >= 1 |
||||
|
def test_hp_strategy_single_property(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
buy_interval_years=10, # only 1 purchase in 10 years |
||||
|
total_years=10, |
||||
|
) |
||||
|
assert result["final_picture"]["num_properties_owned"] >= 1 |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: family plan with partner income |
||||
|
# EXPECTED: household income reflects both incomes |
||||
|
# CRITERIA: total_household_income > single income |
||||
|
def test_hp_family_plan_with_partner(): |
||||
|
from family_planner import plan_family_finances |
||||
|
result = plan_family_finances("Austin", 120000, partner_income=80000, num_planned_children=1) |
||||
|
assert "income_impact" in result |
||||
|
data = result["income_impact"] |
||||
|
assert "total_household_income" in data or "monthly_surplus_after" in data |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: wealth position with real estate equity added |
||||
|
# EXPECTED: total net worth includes real estate |
||||
|
# CRITERIA: total_net_worth > portfolio_value alone |
||||
|
def test_hp_wealth_position_with_real_estate(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
result = analyze_wealth_position( |
||||
|
portfolio_value=94000, |
||||
|
age=34, |
||||
|
annual_income=120000, |
||||
|
real_estate_equity=175000, |
||||
|
) |
||||
|
assert "current_position" in result |
||||
|
pos = result["current_position"] |
||||
|
assert pos["total_net_worth"] == pytest.approx(269000) |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: relocation to cheaper city (SF → Austin) |
||||
|
# EXPECTED: destination surplus higher than current, verdict positive |
||||
|
# CRITERIA: destination monthly_surplus > 0, verdict present |
||||
|
def test_hp_relocation_to_cheaper_city(): |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
result = calculate_relocation_runway( |
||||
|
current_salary=150000, |
||||
|
offer_salary=140000, |
||||
|
current_city="San Francisco", |
||||
|
destination_city="Austin", |
||||
|
portfolio_value=100000, |
||||
|
) |
||||
|
assert "verdict" in result |
||||
|
dest_surplus = result["destination_monthly"]["monthly_surplus"] |
||||
|
curr_surplus = result["current_monthly"]["monthly_surplus"] |
||||
|
assert dest_surplus > curr_surplus |
||||
|
|
||||
|
|
||||
|
# TYPE: happy_path |
||||
|
# INPUT: strategy with conservative appreciation preset |
||||
|
# EXPECTED: conservative < optimistic final net worth |
||||
|
# CRITERIA: conservative_net_worth < optimistic_net_worth |
||||
|
def test_hp_strategy_conservative_vs_optimistic(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
conservative = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, annual_appreciation=0.02, |
||||
|
) |
||||
|
optimistic = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, annual_appreciation=0.06, |
||||
|
) |
||||
|
assert (conservative["final_picture"]["total_net_worth"] < |
||||
|
optimistic["final_picture"]["total_net_worth"]) |
||||
|
|
||||
|
|
||||
|
# ============================================================================ |
||||
|
# EDGE CASE TESTS (12 tests) |
||||
|
# ============================================================================ |
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: portfolio value of zero |
||||
|
# EXPECTED: graceful response, not a crash |
||||
|
# CRITERIA: returns dict, no exception |
||||
|
def test_ec_zero_portfolio_value(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
result = analyze_wealth_position(0, 30, 50000) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
assert "current_position" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: property with no mortgage (fully paid off) |
||||
|
# EXPECTED: equity equals current value |
||||
|
# CRITERIA: equity == current_value, equity_pct == 100.0 |
||||
|
def test_ec_paid_off_property(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
result = _run(add_property( |
||||
|
address="Paid Off Home", |
||||
|
purchase_price=300000, |
||||
|
current_value=380000, |
||||
|
mortgage_balance=0, |
||||
|
)) |
||||
|
prop = result["result"]["property"] |
||||
|
assert prop["equity"] == pytest.approx(380000) |
||||
|
assert prop["equity_pct"] == pytest.approx(100.0) |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: strategy simulation for just 1 year |
||||
|
# EXPECTED: returns valid result, no crash |
||||
|
# CRITERIA: result has final_picture and timeline, no exception |
||||
|
def test_ec_strategy_single_year(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy(50000, 80000, 300000, total_years=1) |
||||
|
assert result is not None |
||||
|
assert "final_picture" in result |
||||
|
assert "timeline" in result |
||||
|
assert isinstance(result["final_picture"]["num_properties_owned"], int) |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: equity options on nonexistent property ID |
||||
|
# EXPECTED: error dict returned, not exception |
||||
|
# CRITERIA: "error" key present in result |
||||
|
def test_ec_equity_nonexistent_property(): |
||||
|
from property_tracker import analyze_equity_options |
||||
|
result = analyze_equity_options("does-not-exist-999") |
||||
|
assert result is not None |
||||
|
assert "error" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: family planner with zero income |
||||
|
# EXPECTED: graceful response, no ZeroDivisionError |
||||
|
# CRITERIA: returns dict, no exception raised |
||||
|
def test_ec_family_planner_zero_income(): |
||||
|
from family_planner import plan_family_finances |
||||
|
result = plan_family_finances("Austin", 0, num_planned_children=1) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: wealth position for someone at exactly the median |
||||
|
# EXPECTED: correctly identified as median tier |
||||
|
# CRITERIA: percentile_estimate contains "50th" or similar |
||||
|
def test_ec_wealth_exactly_at_median(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
# median for under_35 is 39000 |
||||
|
result = analyze_wealth_position(39000, 30, 60000) |
||||
|
pos = result["current_position"] |
||||
|
assert "50th" in pos["percentile_estimate"] or "median" in pos["percentile_estimate"].lower() |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: empty property list — get net worth with no properties |
||||
|
# EXPECTED: net worth equals portfolio value, real estate equity is 0 |
||||
|
# CRITERIA: total_net_worth == portfolio_value, real_estate_equity == 0 |
||||
|
def test_ec_net_worth_no_properties(): |
||||
|
_clear() |
||||
|
from property_tracker import get_total_net_worth |
||||
|
result = _run(get_total_net_worth(portfolio_value=50000)) |
||||
|
assert result["success"] is True |
||||
|
assert result["result"]["total_net_worth"] == pytest.approx(50000) |
||||
|
assert result["result"]["real_estate_equity"] == 0 |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: relocation with identical source and destination city |
||||
|
# EXPECTED: no crash, verdict makes sense |
||||
|
# CRITERIA: returns dict with verdict field |
||||
|
def test_ec_same_city_relocation(): |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
result = calculate_relocation_runway( |
||||
|
current_salary=120000, |
||||
|
offer_salary=125000, |
||||
|
current_city="Austin", |
||||
|
destination_city="Austin", |
||||
|
portfolio_value=94000, |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert "verdict" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: strategy with very large portfolio (1M) |
||||
|
# EXPECTED: simulation completes without overflow |
||||
|
# CRITERIA: total_net_worth is a positive finite number |
||||
|
def test_ec_strategy_large_portfolio(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
initial_portfolio_value=1_000_000, |
||||
|
annual_income=300000, |
||||
|
first_home_price=1_500_000, |
||||
|
total_years=10, |
||||
|
) |
||||
|
assert result is not None |
||||
|
nw = result["final_picture"]["total_net_worth"] |
||||
|
assert nw > 0 |
||||
|
assert nw < 1e15 # sanity check: not overflowing |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: analyze property with equity exceeding current value (impossible) |
||||
|
# EXPECTED: graceful handling, equity capped or error message |
||||
|
# CRITERIA: no crash, returns dict |
||||
|
def test_ec_mortgage_exceeds_value(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
result = _run(add_property( |
||||
|
address="Underwater Property", |
||||
|
purchase_price=400000, |
||||
|
current_value=300000, |
||||
|
mortgage_balance=380000, |
||||
|
)) |
||||
|
# Should succeed but equity will be negative (underwater) |
||||
|
assert result["success"] is True |
||||
|
prop = result["result"]["property"] |
||||
|
equity = prop["equity"] |
||||
|
# underwater: equity = 300000 - 380000 = -80000 (valid, just negative) |
||||
|
assert isinstance(equity, (int, float)) |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: family plan for 5 children |
||||
|
# EXPECTED: costs scaled proportionally, no crash |
||||
|
# CRITERIA: childcare costs for 5 > childcare for 1 |
||||
|
def test_ec_family_many_children(): |
||||
|
from family_planner import plan_family_finances |
||||
|
result1 = plan_family_finances("Austin", 200000, num_planned_children=1) |
||||
|
result5 = plan_family_finances("Austin", 200000, num_planned_children=5) |
||||
|
assert result1 is not None and result5 is not None |
||||
|
# Costs for 5 children should be higher |
||||
|
s1 = result1["income_impact"]["monthly_surplus_after"] |
||||
|
s5 = result5["income_impact"]["monthly_surplus_after"] |
||||
|
assert s5 < s1 # more children = lower surplus |
||||
|
|
||||
|
|
||||
|
# TYPE: edge_case |
||||
|
# INPUT: wealth position for very old age (80+) |
||||
|
# EXPECTED: uses highest bracket (65+), no crash |
||||
|
# CRITERIA: median_for_age == 409000 (65_to_74 bracket) |
||||
|
def test_ec_wealth_very_old_age(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
result = analyze_wealth_position(500000, 82, 60000) |
||||
|
assert result is not None |
||||
|
assert result["current_position"]["median_for_age"] == 409000 |
||||
|
|
||||
|
|
||||
|
# ============================================================================ |
||||
|
# ADVERSARIAL TESTS (12 tests) |
||||
|
# ============================================================================ |
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: property address contains SQL injection |
||||
|
# EXPECTED: stored safely as a string, DB still works after |
||||
|
# CRITERIA: no exception, subsequent queries work normally |
||||
|
def test_adv_sql_injection_address(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_properties |
||||
|
malicious = "'; DROP TABLE properties; --" |
||||
|
result = _run(add_property( |
||||
|
address=malicious, |
||||
|
purchase_price=300000, |
||||
|
current_value=350000, |
||||
|
mortgage_balance=200000, |
||||
|
)) |
||||
|
assert result["success"] is True |
||||
|
# DB still works after the injection attempt |
||||
|
props = _run(get_properties()) |
||||
|
assert props["success"] is True |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: negative property values |
||||
|
# EXPECTED: validation catches it OR handles gracefully |
||||
|
# CRITERIA: no uncaught exception |
||||
|
def test_adv_negative_property_value(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
try: |
||||
|
result = _run(add_property( |
||||
|
address="Bad Property", |
||||
|
purchase_price=-100000, |
||||
|
current_value=-50000, |
||||
|
mortgage_balance=0, |
||||
|
)) |
||||
|
# If it accepts, result should be a dict |
||||
|
assert result is not None |
||||
|
except (ValueError, AssertionError): |
||||
|
pass # Rejecting invalid input is correct behavior |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: impossible appreciation rate (1000% = 10.0) |
||||
|
# EXPECTED: simulation runs without crash |
||||
|
# CRITERIA: returns result dict with final_picture |
||||
|
def test_adv_extreme_appreciation_rate(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
annual_appreciation=10.0, # 1000% — extreme input |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert "final_picture" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: strategy with zero annual income |
||||
|
# EXPECTED: graceful, not ZeroDivision |
||||
|
# CRITERIA: returns dict, no crash |
||||
|
def test_adv_strategy_zero_income(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
try: |
||||
|
result = simulate_real_estate_strategy( |
||||
|
initial_portfolio_value=100000, |
||||
|
annual_income=0, |
||||
|
first_home_price=300000, |
||||
|
total_years=5, |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
except ZeroDivisionError: |
||||
|
pytest.fail("ZeroDivisionError raised with zero income") |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: mortgage rate of 100% (extreme) |
||||
|
# EXPECTED: simulation completes, not crash |
||||
|
# CRITERIA: result is a dict with final_picture |
||||
|
def test_adv_extreme_mortgage_rate(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
mortgage_rate=1.00, # 100% rate |
||||
|
total_years=5, |
||||
|
) |
||||
|
assert result is not None |
||||
|
assert "final_picture" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: address that is only whitespace |
||||
|
# EXPECTED: validation returns structured error |
||||
|
# CRITERIA: success=False, error dict present |
||||
|
def test_adv_whitespace_address(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
result = _run(add_property(address=" \t\n ", purchase_price=300000)) |
||||
|
assert result["success"] is False |
||||
|
assert "error" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: purchase price is zero |
||||
|
# EXPECTED: validation error returned |
||||
|
# CRITERIA: success=False |
||||
|
def test_adv_zero_purchase_price(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property |
||||
|
result = _run(add_property(address="Test", purchase_price=0)) |
||||
|
assert result["success"] is False |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: wealth analysis with negative portfolio |
||||
|
# EXPECTED: graceful handling, no crash |
||||
|
# CRITERIA: returns dict |
||||
|
def test_adv_negative_portfolio_wealth(): |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
try: |
||||
|
result = analyze_wealth_position(-5000, 30, 60000) |
||||
|
assert result is not None |
||||
|
assert isinstance(result, dict) |
||||
|
except Exception: |
||||
|
pass # Rejecting negative portfolio is acceptable |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: family plan for a city not in the lookup |
||||
|
# EXPECTED: uses default costs, no crash |
||||
|
# CRITERIA: returns dict with income_impact |
||||
|
def test_adv_unknown_city_family_plan(): |
||||
|
from family_planner import plan_family_finances |
||||
|
result = plan_family_finances("Xanadu City", 80000, num_planned_children=1) |
||||
|
assert result is not None |
||||
|
assert "income_impact" in result |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: remove a nonexistent property ID |
||||
|
# EXPECTED: structured error dict returned |
||||
|
# CRITERIA: success=False, error code = NOT_FOUND |
||||
|
def test_adv_remove_nonexistent_property(): |
||||
|
_clear() |
||||
|
from property_tracker import remove_property |
||||
|
result = _run(remove_property("nonexistent-id-999")) |
||||
|
assert result["success"] is False |
||||
|
assert isinstance(result["error"], dict) |
||||
|
assert result["error"]["code"] == "PROPERTY_TRACKER_NOT_FOUND" |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: strategy with negative down payment percentage |
||||
|
# EXPECTED: uses fallback/default or handles gracefully |
||||
|
# CRITERIA: no crash |
||||
|
def test_adv_negative_down_payment_pct(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
try: |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
down_payment_pct=-0.5, |
||||
|
) |
||||
|
assert result is not None |
||||
|
except (ValueError, AssertionError): |
||||
|
pass # Validation rejection is fine |
||||
|
|
||||
|
|
||||
|
# TYPE: adversarial |
||||
|
# INPUT: empty string city for relocation |
||||
|
# EXPECTED: no crash, returns dict |
||||
|
# CRITERIA: result is a dict |
||||
|
def test_adv_empty_city_relocation(): |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
try: |
||||
|
result = calculate_relocation_runway( |
||||
|
current_salary=100000, |
||||
|
offer_salary=120000, |
||||
|
current_city="", |
||||
|
destination_city="", |
||||
|
portfolio_value=50000, |
||||
|
) |
||||
|
assert isinstance(result, dict) |
||||
|
except Exception: |
||||
|
pass # Failing gracefully is acceptable |
||||
|
|
||||
|
|
||||
|
# ============================================================================ |
||||
|
# MULTI-STEP TESTS (12 tests) |
||||
|
# ============================================================================ |
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: add property → get total net worth → verify property appears |
||||
|
# EXPECTED: net worth increases by property equity after adding |
||||
|
# CRITERIA: total_net_worth > portfolio_value_alone |
||||
|
def test_ms_add_then_net_worth(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_total_net_worth |
||||
|
_run(add_property( |
||||
|
address="Multi Step Test", |
||||
|
purchase_price=350000, |
||||
|
current_value=420000, |
||||
|
mortgage_balance=280000, |
||||
|
)) # equity = 140000 |
||||
|
result = _run(get_total_net_worth(portfolio_value=94000)) |
||||
|
assert result["result"]["total_net_worth"] == pytest.approx(234000) |
||||
|
assert result["result"]["real_estate_equity"] == pytest.approx(140000) |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: add property → analyze equity options |
||||
|
# EXPECTED: equity options reference added property, 3 scenarios returned |
||||
|
# CRITERIA: options has 3 entries, each has 10-year projection |
||||
|
def test_ms_add_then_equity_options(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, analyze_equity_options |
||||
|
prop = _run(add_property( |
||||
|
address="Equity Chain Test", |
||||
|
purchase_price=400000, |
||||
|
current_value=520000, |
||||
|
mortgage_balance=370000, |
||||
|
)) |
||||
|
result = analyze_equity_options(prop["result"]["property"]["id"]) |
||||
|
assert "options" in result |
||||
|
assert len(result["options"]) >= 3 |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: add two properties → remove one → verify only one remains |
||||
|
# EXPECTED: after removal, list shows exactly 1 property |
||||
|
# CRITERIA: len(properties) == 1 after removal |
||||
|
def test_ms_add_two_remove_one(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, remove_property, get_properties |
||||
|
p1 = _run(add_property("Home 1", 400000, 450000, 300000)) |
||||
|
p2 = _run(add_property("Home 2", 350000, 400000, 250000)) |
||||
|
id1 = p1["result"]["property"]["id"] |
||||
|
_run(remove_property(id1)) |
||||
|
listed = _run(get_properties()) |
||||
|
props = listed["result"]["properties"] |
||||
|
assert len(props) == 1 |
||||
|
assert props[0]["id"] == p2["result"]["property"]["id"] |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: strategy simulation → use final net worth in wealth position check |
||||
|
# EXPECTED: chaining results without errors |
||||
|
# CRITERIA: both return valid dicts, wealth check uses strategy output |
||||
|
def test_ms_strategy_then_wealth_check(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
strategy = simulate_real_estate_strategy(94000, 120000, 400000, total_years=10) |
||||
|
final_worth = strategy["final_picture"]["total_net_worth"] |
||||
|
wealth = analyze_wealth_position( |
||||
|
portfolio_value=final_worth, |
||||
|
age=44, |
||||
|
annual_income=150000, |
||||
|
) |
||||
|
assert "current_position" in wealth |
||||
|
assert "retirement_projection" in wealth |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: family planning → use reduced savings in wealth position |
||||
|
# EXPECTED: lower savings rate flows into retirement projection |
||||
|
# CRITERIA: both tools return valid dicts |
||||
|
def test_ms_family_then_wealth(): |
||||
|
from family_planner import plan_family_finances |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
family = plan_family_finances("Austin", 120000, num_planned_children=1) |
||||
|
assert "income_impact" in family |
||||
|
reduced_savings = max(0, family["income_impact"]["monthly_surplus_after"] * 12) |
||||
|
wealth = analyze_wealth_position( |
||||
|
portfolio_value=94000, |
||||
|
age=32, |
||||
|
annual_income=120000, |
||||
|
annual_savings=reduced_savings, |
||||
|
) |
||||
|
assert "retirement_projection" in wealth |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: job offer check → relocation runway → both use same cities |
||||
|
# EXPECTED: consistent data across both tools for Austin/Seattle |
||||
|
# CRITERIA: both return their respective fields correctly |
||||
|
@pytest.mark.asyncio |
||||
|
async def test_ms_job_offer_then_runway(): |
||||
|
from wealth_bridge import calculate_job_offer_affordability |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
offer = await calculate_job_offer_affordability( |
||||
|
offer_salary=180000, |
||||
|
offer_city="Seattle", |
||||
|
current_salary=120000, |
||||
|
current_city="Austin", |
||||
|
) |
||||
|
runway = calculate_relocation_runway( |
||||
|
current_salary=120000, |
||||
|
offer_salary=180000, |
||||
|
current_city="Austin", |
||||
|
destination_city="Seattle", |
||||
|
portfolio_value=94000, |
||||
|
) |
||||
|
assert "is_real_raise" in offer |
||||
|
assert "verdict" in runway |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: add property → update value → check net worth reflects update |
||||
|
# EXPECTED: net worth uses updated value not original |
||||
|
# CRITERIA: net worth after update > net worth after initial add |
||||
|
def test_ms_add_update_then_net_worth(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, update_property, get_total_net_worth |
||||
|
prop = _run(add_property("Growing Home", 400000, 450000, 320000)) |
||||
|
pid = prop["result"]["property"]["id"] |
||||
|
nw_before = _run(get_total_net_worth(portfolio_value=50000)) |
||||
|
_run(update_property(pid, current_value=500000)) |
||||
|
nw_after = _run(get_total_net_worth(portfolio_value=50000)) |
||||
|
assert nw_after["result"]["total_net_worth"] > nw_before["result"]["total_net_worth"] |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: add property → get equity → use in wealth position |
||||
|
# EXPECTED: wealth position uses real estate equity from property tracker |
||||
|
# CRITERIA: position with RE equity > position without |
||||
|
def test_ms_property_equity_in_wealth_position(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_real_estate_equity |
||||
|
from wealth_visualizer import analyze_wealth_position |
||||
|
_run(add_property("Wealth Test", 400000, 500000, 300000)) # equity=200000 |
||||
|
equity_result = _run(get_real_estate_equity()) |
||||
|
equity = equity_result["result"]["total_real_estate_equity"] |
||||
|
pos_with_re = analyze_wealth_position(94000, 34, 120000, real_estate_equity=equity) |
||||
|
pos_without = analyze_wealth_position(94000, 34, 120000) |
||||
|
assert pos_with_re["current_position"]["total_net_worth"] > pos_without["current_position"]["total_net_worth"] |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: full CRUD cycle — create, read, update, delete |
||||
|
# EXPECTED: each operation succeeds, state consistent throughout |
||||
|
# CRITERIA: all 4 operations return success=True, final list is empty |
||||
|
def test_ms_full_crud_cycle(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_properties, update_property, remove_property |
||||
|
|
||||
|
# CREATE |
||||
|
prop = _run(add_property("CRUD Home", 400000, 450000, 320000)) |
||||
|
assert prop["success"] is True |
||||
|
pid = prop["result"]["property"]["id"] |
||||
|
|
||||
|
# READ |
||||
|
listed = _run(get_properties()) |
||||
|
ids = [p["id"] for p in listed["result"]["properties"]] |
||||
|
assert pid in ids |
||||
|
|
||||
|
# UPDATE |
||||
|
updated = _run(update_property(pid, current_value=480000)) |
||||
|
assert updated["success"] is True |
||||
|
assert updated["result"]["property"]["equity"] == pytest.approx(160000) |
||||
|
|
||||
|
# DELETE |
||||
|
removed = _run(remove_property(pid)) |
||||
|
assert removed["success"] is True |
||||
|
|
||||
|
# Verify empty |
||||
|
after = _run(get_properties()) |
||||
|
assert after["result"]["properties"] == [] |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: multiple properties → wealth position uses combined equity |
||||
|
# EXPECTED: total equity from all properties flows into net worth |
||||
|
# CRITERIA: net worth = portfolio + combined equity |
||||
|
def test_ms_multiple_properties_net_worth(): |
||||
|
_clear() |
||||
|
from property_tracker import add_property, get_total_net_worth |
||||
|
_run(add_property("Home A", 400000, 480000, 300000)) # equity=180000 |
||||
|
_run(add_property("Home B", 300000, 380000, 260000)) # equity=120000 |
||||
|
result = _run(get_total_net_worth(portfolio_value=94000)) |
||||
|
expected = 94000 + 180000 + 120000 # = 394000 |
||||
|
assert result["result"]["total_net_worth"] == pytest.approx(expected) |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: relocation runway → family plan in destination city |
||||
|
# EXPECTED: can chain city data across both tools |
||||
|
# CRITERIA: both return valid dicts for Seattle |
||||
|
def test_ms_runway_then_family_plan(): |
||||
|
from relocation_runway import calculate_relocation_runway |
||||
|
from family_planner import plan_family_finances |
||||
|
runway = calculate_relocation_runway( |
||||
|
current_salary=120000, |
||||
|
offer_salary=180000, |
||||
|
current_city="Austin", |
||||
|
destination_city="Seattle", |
||||
|
portfolio_value=94000, |
||||
|
) |
||||
|
assert "verdict" in runway |
||||
|
family = plan_family_finances("Seattle", 180000, num_planned_children=1) |
||||
|
assert "income_impact" in family |
||||
|
|
||||
|
|
||||
|
# TYPE: multi_step |
||||
|
# INPUT: strategy simulation over 20 years → verify timeline length |
||||
|
# EXPECTED: timeline has 20+ entries covering each year |
||||
|
# CRITERIA: len(timeline) >= 20 |
||||
|
def test_ms_strategy_long_horizon(): |
||||
|
from realestate_strategy import simulate_real_estate_strategy |
||||
|
result = simulate_real_estate_strategy( |
||||
|
94000, 120000, 400000, |
||||
|
total_years=20, |
||||
|
buy_interval_years=4, |
||||
|
) |
||||
|
assert result is not None |
||||
|
timeline = result.get("timeline", []) |
||||
|
assert len(timeline) >= 20 |
||||
|
assert result["final_picture"]["num_properties_owned"] >= 1 |
||||
Loading…
Reference in new issue