import random import pytest from store import OrderRequest, PriceUpdate, StoreAction, StoreEnv from store.models import dump_model from store.server.store_environment import StoreEnvironment def test_reset_returns_full_plan_like_observation() -> None: env = StoreEnvironment() observation = env.reset() assert observation.store.day == 1 assert len(observation.stock) == 30 assert len(observation.market) == 100 assert len(observation.marketing_actions) == 15 assert len(observation.seller_profiles) == 4 assert observation.pending_orders == [] assert observation.available_action.pass_action == StoreAction() assert observation.available_action.example_action.action_ids == [7] def test_pass_step_advances_three_days_but_reward_uses_six_day_forecast() -> None: env = StoreEnvironment() env.reset() observation = env.step(StoreAction()) assert observation.store.day == 4 assert observation.metadata["reward_breakdown"]["commit_window_days"] == 3 assert observation.metadata["reward_breakdown"]["reward_horizon_days"] == 6 def test_eta_sampling_is_deterministic_for_a_seed() -> None: env_a = StoreEnvironment() env_b = StoreEnvironment() env_a.reset() env_b.reset() env_a._rng = random.Random(7) env_b._rng = random.Random(7) assert env_a._sample_actual_eta("Seller C", 5) == env_b._sample_actual_eta("Seller C", 5) def test_commit_state_only_keeps_three_days_of_progress() -> None: env = StoreEnvironment() env.reset() env._sample_actual_eta = lambda seller, quoted: 4 # type: ignore[method-assign] observation = env.step( StoreAction( orders=[OrderRequest(product="Rice", seller="Seller B", quantity=10)], ) ) assert observation.store.day == 4 assert len(observation.pending_orders) == 1 assert observation.pending_orders[0].days_remaining == 1 def test_price_update_changes_customer_satisfaction() -> None: env = StoreEnvironment() reset_observation = env.reset() milk_row = next(row for row in reset_observation.stock if row.product_name == "Milk") observation = env.step( StoreAction( prices=[ PriceUpdate( product="Milk", price=max(1, int(milk_row.selling_price_inr) - 5), ) ] ) ) assert observation.store.customer_satisfaction > reset_observation.store.customer_satisfaction def test_marketing_actions_are_durable_and_cannot_be_reactivated() -> None: env = StoreEnvironment() env.reset() observation = env.step(StoreAction(action_ids=[7])) assert observation.store.active_action_ids == [7] assert observation.store.active_recurring_cost_inr > 0 assert any( action.action_id == 7 and action.is_active for action in observation.marketing_actions ) with pytest.raises(ValueError): env.step(StoreAction(action_ids=[7])) def test_recurring_costs_are_deducted_each_simulated_day() -> None: env = StoreEnvironment() env.reset() runtime = env._build_initial_runtime() runtime["store"]["bank_balance_inr"] = 10_000.0 runtime["store"]["cumulative_profit_inr"] = 0.0 runtime["store"]["current_daily_expense_inr"] = 0.0 runtime["store"]["customer_satisfaction"] = 0.0 runtime["active_action_ids"] = {7} metrics = env._simulate_days(runtime, 3) assert metrics["recurring_costs_paid_inr"] == 1_800.0 assert runtime["store"]["bank_balance_inr"] == 8_200.0 assert runtime["store"]["cumulative_profit_inr"] == -1_800.0 def test_large_capital_loss_reduces_reward_and_ends_episode() -> None: baseline = StoreEnvironment() baseline.reset() baseline_reward = baseline.step(StoreAction()).reward env = StoreEnvironment() env.reset() observation = env.step(StoreAction(action_ids=[11])) assert observation.reward < baseline_reward assert observation.done is True assert observation.metadata["done_reason"] == "bank_balance_below_zero" def test_episode_ends_after_tenth_decision_window() -> None: env = StoreEnvironment() env.reset() observation = None for _ in range(9): observation = env.step(StoreAction()) assert observation.done is False observation = env.step(StoreAction()) assert observation is not None assert observation.done is True assert observation.store.day == 31 assert observation.metadata["done_reason"] == "episode_horizon_reached" def test_client_payload_and_result_parsing_support_nested_models() -> None: client = StoreEnv(base_url="http://localhost:8000") action = StoreAction( orders=[OrderRequest(product="Rice", seller="Seller A", quantity=5)], prices=[PriceUpdate(product="Milk", price=33)], action_ids=[7], ) payload = client._step_payload(action) assert payload == { "orders": [{"product": "Rice", "seller": "Seller A", "quantity": 5}], "prices": [{"product": "Milk", "price": 33}], "action_ids": [7], } env = StoreEnvironment() observation = env.reset() result = client._parse_result( { "observation": dump_model(observation, exclude_none=True), "reward": 0.0, "done": False, } ) assert result.observation.store.day == 1 assert len(result.observation.market) == 100