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