"""Tests for subscription/donation DCA integration.""" import numpy as np import pytest from src.composed.subscription_dca import ( SubscriptionDCAConfig, DonationDCAConfig, SubscriptionDCAOrder, DonationDCAOrder, SubscriptionDCAState, create_subscription_dca_order, create_donation_dca_order, execute_subscription_chunk, execute_donation_chunk, pending_chunks, simulate_subscription_dca, ) from src.composed.dca_executor import DCAOrder from src.composed.myco_surface import MycoSystem, MycoSystemConfig from src.commitments.subscription import ( Subscription, SubscriptionTier, create_default_tiers, ) from src.primitives.twap_oracle import ( TWAPOracleParams, TWAPOracleState, create_oracle, record_observation, ) # --- Helpers --- def _make_system() -> MycoSystem: config = MycoSystemConfig(n_reserve_assets=3) system = MycoSystem(config) # Bootstrap system.deposit(np.array([1000.0, 1000.0, 1000.0]), 0.0) return system def _make_oracle() -> TWAPOracleState: oracle = create_oracle(TWAPOracleParams(default_window=24.0)) oracle = record_observation(oracle, 1.0, 0.0) oracle = record_observation(oracle, 1.0, 1.0) return oracle def _make_subscription() -> tuple[Subscription, SubscriptionTier]: tier = SubscriptionTier( name="Sustainer", payment_per_period=50.0, period_length=30.0, base_mint_rate=1.8, loyalty_multiplier_max=1.5, loyalty_halflife=120.0, ) sub = Subscription( subscriber="alice", tier="sustainer", start_time=0.0, last_payment_time=0.0, total_paid=0.0, total_minted=0.0, periods_paid=0, ) return sub, tier # --- TestSubscriptionDCAOrder --- class TestSubscriptionDCAOrder: def test_creates_correct_chunks(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=0.8) order = create_subscription_dca_order(sub, tier, config, 10.0, 1.2, 0) assert order.order.params.n_chunks == 5 assert abs(order.order.params.total_amount - 50.0) < 1e-10 def test_interval_respects_spread(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=0.8) order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0) expected_interval = (30.0 * 0.8) / 5 assert abs(order.order.params.interval - expected_interval) < 1e-10 def test_loyalty_multiplier_stored(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig() order = create_subscription_dca_order(sub, tier, config, 0.0, 1.35, 2) assert abs(order.loyalty_multiplier - 1.35) < 1e-10 assert order.period_index == 2 def test_strategy_propagated(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig(strategy="twap_aware") order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0) assert order.order.params.strategy == "twap_aware" # --- TestDonationDCAOrder --- class TestDonationDCAOrder: def test_dca_enabled_multi_chunk(self): config = DonationDCAConfig(enable_dca=True, n_chunks=5, interval=2.0) order = create_donation_dca_order("bob", 500.0, config, 0.0) assert order.order.params.n_chunks == 5 assert abs(order.donation_amount - 500.0) < 1e-10 def test_dca_disabled_single_chunk(self): config = DonationDCAConfig(enable_dca=False, n_chunks=5) order = create_donation_dca_order("bob", 500.0, config, 0.0) assert order.order.params.n_chunks == 1 def test_single_chunk_when_n_chunks_one(self): config = DonationDCAConfig(enable_dca=True, n_chunks=1) order = create_donation_dca_order("bob", 100.0, config, 0.0) assert order.order.params.n_chunks == 1 assert order.order.params.interval == 0.0 def test_donor_stored(self): config = DonationDCAConfig() order = create_donation_dca_order("carol", 200.0, config, 5.0) assert order.donor == "carol" assert abs(order.order.start_time - 5.0) < 1e-10 # --- TestChunkExecution --- class TestChunkExecution: def test_subscription_chunk_mints_tokens(self): system = _make_system() oracle = _make_oracle() sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=3) order = create_subscription_dca_order(sub, tier, config, 1.0, 1.0, 0) state = SubscriptionDCAState( subscription_orders={"alice": [order]}, ) state, system, oracle, tokens, bonus = execute_subscription_chunk( state, "alice", system, oracle, 1.0, ) assert tokens > 0 assert bonus == 0.0 # loyalty_multiplier == 1.0 def test_loyalty_bonus_computed(self): system = _make_system() oracle = _make_oracle() sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=3) order = create_subscription_dca_order(sub, tier, config, 1.0, 1.5, 0) state = SubscriptionDCAState( subscription_orders={"alice": [order]}, ) state, system, oracle, tokens, bonus = execute_subscription_chunk( state, "alice", system, oracle, 1.0, ) assert tokens > 0 # bonus = tokens * (1.5 - 1.0) = tokens * 0.5 assert abs(bonus - tokens * 0.5) < 1e-10 def test_donation_chunk_mints_tokens(self): system = _make_system() oracle = _make_oracle() config = DonationDCAConfig(enable_dca=True, n_chunks=3, interval=1.0) don_order = create_donation_dca_order("bob", 300.0, config, 1.0) state = SubscriptionDCAState(donation_orders=[don_order]) state, system, oracle, tokens = execute_donation_chunk( state, 0, system, oracle, 1.0, ) assert tokens > 0 def test_complete_order_returns_zero(self): system = _make_system() oracle = _make_oracle() sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=1) order = create_subscription_dca_order(sub, tier, config, 1.0, 1.0, 0) state = SubscriptionDCAState( subscription_orders={"alice": [order]}, ) # Execute the single chunk state, system, oracle, t1, b1 = execute_subscription_chunk( state, "alice", system, oracle, 1.0, ) assert t1 > 0 # Second execution should return zero state, system, oracle, t2, b2 = execute_subscription_chunk( state, "alice", system, oracle, 2.0, ) assert t2 == 0.0 assert b2 == 0.0 def test_nonexistent_subscriber_returns_zero(self): system = _make_system() oracle = _make_oracle() state = SubscriptionDCAState() state, system, oracle, tokens, bonus = execute_subscription_chunk( state, "nobody", system, oracle, 1.0, ) assert tokens == 0.0 def test_out_of_range_donation_index_returns_zero(self): system = _make_system() oracle = _make_oracle() state = SubscriptionDCAState() state, system, oracle, tokens = execute_donation_chunk( state, 99, system, oracle, 1.0, ) assert tokens == 0.0 # --- TestPendingChunks --- class TestPendingChunks: def test_lists_due_chunks(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=5, spread_fraction=1.0) order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0) state = SubscriptionDCAState( subscription_orders={"alice": [order]}, ) # interval = 30.0 / 5 = 6.0, so at t=7 chunk 0 and 1 are due result = pending_chunks(state, "alice", 7.0) assert len(result) >= 1 def test_no_pending_when_complete(self): sub, tier = _make_subscription() config = SubscriptionDCAConfig(n_chunks=1) order = create_subscription_dca_order(sub, tier, config, 0.0, 1.0, 0) order.order.is_complete = True state = SubscriptionDCAState( subscription_orders={"alice": [order]}, ) result = pending_chunks(state, "alice", 100.0) assert len(result) == 0 def test_empty_for_unknown_subscriber(self): state = SubscriptionDCAState() result = pending_chunks(state, "nobody", 0.0) assert result == [] # --- TestBackwardCompat --- class TestBackwardCompat: """Ensure the new modules don't break existing DCA or subscription imports.""" def test_dca_executor_still_works(self): from src.composed.dca_executor import DCAParams, simulate_dca config = MycoSystemConfig(n_reserve_assets=3) params = DCAParams( total_amount=100.0, n_chunks=5, interval=1.0, asset_index=0, ) result = simulate_dca(config, params) assert result.order.total_tokens_received > 0 def test_subscription_module_still_works(self): from src.commitments.subscription import ( SubscriptionSystem, create_subscription, process_payment, create_default_tiers, ) tiers = create_default_tiers() system = SubscriptionSystem(tiers=tiers) system, sub = create_subscription(system, "alice", "supporter", 0.0) system, tokens = process_payment(system, "alice", 31.0) assert tokens > 0 def test_myco_system_signal_routing(self): """Test the apply_signal_routing integration on MycoSystem.""" from src.primitives.signal_router import SignalRouterConfig config = MycoSystemConfig( n_reserve_assets=3, twap_oracle_params=TWAPOracleParams(), ) system = MycoSystem(config) system.deposit(np.array([1000.0, 1000.0, 1000.0]), 0.0) # Do a few deposits to build oracle history for i in range(1, 6): system.deposit(np.array([10.0, 10.0, 10.0]), float(i)) router_config = SignalRouterConfig() adapted = system.apply_signal_routing(router_config) assert adapted.flow_threshold > 0 assert adapted.pamm_alpha_bar > 0 def test_simulation_runs(self): tiers = create_default_tiers() config = SubscriptionDCAConfig(n_chunks=3) sys_config = MycoSystemConfig(n_reserve_assets=3) subscribers = [("alice", "supporter", 0.0)] result = simulate_subscription_dca( tiers, config, sys_config, subscribers, duration=60.0, dt=5.0, ) assert "times" in result assert len(result["times"]) == 12