CoolFace
Apppublic

himanshunakrani9/decision-simulator-api

sourceHugging Faceupdated 6mo agoView on Hugging Face
0likes
test_simulation.py268 linesDownload Raw Back to tests
1"""Property-based tests for the Simulation Engine — scenario generation.2 3Property 1: Scenario probability sum invariant4Validates: Requirements 3.2, 3.35 6For any valid structured_input with N options and any risk_tolerance in [0.0, 1.0],7generate_scenarios must return exactly N option groups where:8- each group has 2–3 scenarios9- the scenario probabilities within each group sum to exactly 1.010"""11 12import math13 14from hypothesis import given, settings15from hypothesis import strategies as st16 17from backend.app.simulation import generate_scenarios18 19 20# ---------------------------------------------------------------------------21# Strategies22# ---------------------------------------------------------------------------23 24# Non-empty option strings (printable, at least 1 char)25option_strategy = st.text(26    alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Nd", "Zs")),27    min_size=1,28    max_size=30,29).filter(lambda s: s.strip())30 31# structured_input dict with at least 2 options32structured_input_strategy = st.fixed_dictionaries(33    {34        "options": st.lists(option_strategy, min_size=2, max_size=5, unique=True),35        "factors": st.just(["factor_1"]),36    }37)38 39risk_tolerance_strategy = st.floats(min_value=0.0, max_value=1.0, allow_nan=False)40 41 42# ---------------------------------------------------------------------------43# Property 1: Scenario probability sum invariant44# Validates: Requirements 3.2, 3.345# ---------------------------------------------------------------------------46 47 48@given(structured_input=structured_input_strategy, risk_tolerance=risk_tolerance_strategy)49@settings(max_examples=100)50def test_scenario_probability_sum_invariant(structured_input, risk_tolerance):51    """**Validates: Requirements 3.2, 3.3**52 53    For any valid structured_input and risk_tolerance in [0.0, 1.0]:54    - generate_scenarios returns exactly one group per option55    - each group has 2–3 scenarios56    - probabilities within each group sum to exactly 1.057    """58    result = generate_scenarios(structured_input, risk_tolerance)59 60    # One group per option (Requirement 3.1)61    assert len(result) == len(structured_input["options"])62 63    for group in result:64        scenarios = group["scenarios"]65 66        # Each group has 2–3 scenarios (Requirement 3.2)67        assert 2 <= len(scenarios) <= 3, (68            f"Expected 2–3 scenarios per option, got {len(scenarios)} "69            f"for option '{group['option']}'"70        )71 72        # Probabilities sum to exactly 1.0 (Requirement 3.3)73        total = sum(s["probability"] for s in scenarios)74        assert math.isclose(total, 1.0, abs_tol=1e-4), (75            f"Probabilities for option '{group['option']}' sum to {total}, expected 1.0"76        )77 78 79# ---------------------------------------------------------------------------80# Strategies for run_simulation tests81# ---------------------------------------------------------------------------82 83from backend.app.simulation import run_simulation84 85# Scenario name strategies for semantic salary range testing86HIGH_GROWTH_KEYWORDS = ["high growth", "promotion", "success"]87STRESSFUL_KEYWORDS = ["stressful", "risk", "struggle"]88 89high_growth_name_strategy = st.sampled_from(HIGH_GROWTH_KEYWORDS).flatmap(90    lambda kw: st.just(kw)91    | st.text(alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Zs")), min_size=0, max_size=10).map(92        lambda prefix: f"{prefix} {kw}".strip()93    )94)95 96stressful_name_strategy = st.sampled_from(STRESSFUL_KEYWORDS).flatmap(97    lambda kw: st.just(kw)98    | st.text(alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Zs")), min_size=0, max_size=10).map(99        lambda prefix: f"{prefix} {kw}".strip()100    )101)102 103# Neutral names: must not contain any high-growth or stressful keywords104_ALL_KEYWORDS = HIGH_GROWTH_KEYWORDS + STRESSFUL_KEYWORDS105 106neutral_name_strategy = st.text(107    alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Zs")),108    min_size=3,109    max_size=20,110).filter(111    lambda s: s.strip()112    and not any(kw in s.lower() for kw in _ALL_KEYWORDS)113)114 115 116def make_scenario_list(scenario_name: str, probability: float = 1.0) -> list[dict]:117    """Build a minimal valid scenario list with a single option and single scenario."""118    return [{"option": "test_option", "scenarios": [{"name": scenario_name, "probability": probability}]}]119 120 121# Valid scenario list strategy for general simulation tests122scenario_entry_strategy = st.fixed_dictionaries(123    {124        "name": st.text(125            alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Zs")),126            min_size=1,127            max_size=30,128        ).filter(lambda s: s.strip()),129        "probability": st.just(1.0),130    }131)132 133scenario_list_strategy = st.lists(134    st.fixed_dictionaries(135        {136            "option": st.text(137                alphabet=st.characters(whitelist_categories=("Lu", "Ll", "Nd", "Zs")),138                min_size=1,139                max_size=20,140            ).filter(lambda s: s.strip()),141            "scenarios": st.lists(scenario_entry_strategy, min_size=1, max_size=3),142        }143    ),144    min_size=1,145    max_size=3,146)147 148time_horizon_strategy = st.integers(min_value=1, max_value=30)149 150 151# ---------------------------------------------------------------------------152# Property 7: Simulation output value invariants153# Validates: Requirements 4.2, 4.3, 4.4154# ---------------------------------------------------------------------------155 156 157@given(scenarios=scenario_list_strategy, time_horizon=time_horizon_strategy)158@settings(max_examples=100)159def test_simulation_output_value_invariants(scenarios, time_horizon):160    """**Validates: Requirements 4.2, 4.3, 4.4**161 162    For any valid scenario list and time_horizon >= 1:163    - every result has salary > 0164    - every result has risk_score in [0.0, 1.0]165    - every result has happiness in [0.0, 1.0]166    """167    results = run_simulation(scenarios, time_horizon)168 169    for r in results:170        assert r["salary"] > 0, f"Expected salary > 0, got {r['salary']}"171        assert 0.0 <= r["risk_score"] <= 1.0, f"Expected risk_score in [0,1], got {r['risk_score']}"172        assert 0.0 <= r["happiness"] <= 1.0, f"Expected happiness in [0,1], got {r['happiness']}"173 174 175# ---------------------------------------------------------------------------176# Property 9: Simulation determinism177# Validates: Requirements 4.1178# ---------------------------------------------------------------------------179 180 181@given(scenarios=scenario_list_strategy, time_horizon=time_horizon_strategy)182@settings(max_examples=50)183def test_simulation_determinism(scenarios, time_horizon):184    """**Validates: Requirements 4.1**185 186    Calling run_simulation twice with identical inputs must produce identical results187    because np.random.seed(42) is set inside run_simulation.188    """189    results_1 = run_simulation(scenarios, time_horizon)190    results_2 = run_simulation(scenarios, time_horizon)191 192    assert results_1 == results_2, (193        f"run_simulation is not deterministic: first call returned {results_1}, "194        f"second call returned {results_2}"195    )196 197 198# ---------------------------------------------------------------------------199# Property 8: Semantic salary ranges200# Validates: Requirements 4.5, 4.6, 4.7201# ---------------------------------------------------------------------------202 203 204@given(scenario_name=high_growth_name_strategy, time_horizon=time_horizon_strategy)205@settings(max_examples=100)206def test_semantic_salary_range_high_growth(scenario_name, time_horizon):207    """**Validates: Requirements 4.5**208 209    For scenario names containing high-growth keywords, salary must fall in210    [base * 1.3 * (1 + 0.05*t), base * 2.0 * (1 + 0.05*t)] where base in [40000, 80000].211    """212    results = run_simulation(make_scenario_list(scenario_name), time_horizon)213    assert len(results) == 1214    salary = results[0]["salary"]215 216    growth_factor = 1 + 0.05 * time_horizon217    min_salary = 40000 * 1.3 * growth_factor218    max_salary = 80000 * 2.0 * growth_factor219 220    assert min_salary <= salary <= max_salary, (221        f"High-growth scenario '{scenario_name}' salary {salary} not in "222        f"[{min_salary:.2f}, {max_salary:.2f}] for time_horizon={time_horizon}"223    )224 225 226@given(scenario_name=stressful_name_strategy, time_horizon=time_horizon_strategy)227@settings(max_examples=100)228def test_semantic_salary_range_stressful(scenario_name, time_horizon):229    """**Validates: Requirements 4.6**230 231    For scenario names containing stressful keywords, salary must fall in232    [base * 0.8 * (1 + 0.05*t), base * 1.1 * (1 + 0.05*t)] where base in [40000, 80000].233    """234    results = run_simulation(make_scenario_list(scenario_name), time_horizon)235    assert len(results) == 1236    salary = results[0]["salary"]237 238    growth_factor = 1 + 0.05 * time_horizon239    min_salary = 40000 * 0.8 * growth_factor240    max_salary = 80000 * 1.1 * growth_factor241 242    assert min_salary <= salary <= max_salary, (243        f"Stressful scenario '{scenario_name}' salary {salary} not in "244        f"[{min_salary:.2f}, {max_salary:.2f}] for time_horizon={time_horizon}"245    )246 247 248@given(scenario_name=neutral_name_strategy, time_horizon=time_horizon_strategy)249@settings(max_examples=100)250def test_semantic_salary_range_neutral(scenario_name, time_horizon):251    """**Validates: Requirements 4.7**252 253    For neutral scenario names (no high-growth or stressful keywords), salary must fall in254    [base * 1.0 * (1 + 0.05*t), base * 1.4 * (1 + 0.05*t)] where base in [40000, 80000].255    """256    results = run_simulation(make_scenario_list(scenario_name), time_horizon)257    assert len(results) == 1258    salary = results[0]["salary"]259 260    growth_factor = 1 + 0.05 * time_horizon261    min_salary = 40000 * 1.0 * growth_factor262    max_salary = 80000 * 1.4 * growth_factor263 264    assert min_salary <= salary <= max_salary, (265        f"Neutral scenario '{scenario_name}' salary {salary} not in "266        f"[{min_salary:.2f}, {max_salary:.2f}] for time_horizon={time_horizon}"267    )268