import 'package:cyberhybridhub_server/trading/guardrails.dart'; import 'package:cyberhybridhub_server/trading/trading_config.dart'; import 'package:test/test.dart'; EffectiveTradingConfig _config({ bool enabled = true, String mode = 'paper', List symbols = const ['SPY'], int maxOrdersPerDay = 3, num maxNotionalUsdPer4h = 100, bool requireQuestion = true, List blocklist = const [], }) { return EffectiveTradingConfig.fromJson({ 'version': 1, 'enabled': enabled, 'mode': mode, 'data_inputs': >[ { 'id': 'primary_watchlist', 'symbols': symbols, 'metrics': ['last_trade'], }, ], 'rules': >[], 'guardrails': { 'max_orders_per_day': maxOrdersPerDay, 'max_notional_usd_per_4h': maxNotionalUsdPer4h, 'require_question_before_order': requireQuestion, 'symbols_blocklist': blocklist, }, }); } GuardrailDecision _check( Guardrails g, { required EffectiveTradingConfig config, String symbol = 'SPY', num notionalUsd = 10, int dailyOrderCount = 0, num notionalUsdInWindow = 0, bool hasUnansweredQuestion = false, bool questionAnswered = true, }) { return g.check( config: config, symbol: symbol, notionalUsd: notionalUsd, dailyOrderCount: dailyOrderCount, notionalUsdInWindow: notionalUsdInWindow, hasUnansweredQuestion: hasUnansweredQuestion, questionAnswered: questionAnswered, ); } void main() { group('Guardrails.check', () { test('allows a paper order within all limits', () { final GuardrailDecision decision = _check(Guardrails(), config: _config()); expect(decision.allowed, isTrue); }); test('rejects when trading is disabled', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(enabled: false), ); expect(decision.reason, GuardrailRejectionReason.tradingDisabled); }); test('rejects blocklisted symbol', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(blocklist: ['SPY']), ); expect(decision.reason, GuardrailRejectionReason.blocklistedSymbol); }); test('rejects symbol not in watchlist', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(symbols: ['AAPL']), ); expect(decision.reason, GuardrailRejectionReason.symbolNotInWatchlist); }); test('rejects when daily order count reaches config max', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(maxOrdersPerDay: 3), dailyOrderCount: 3, ); expect( decision.reason, GuardrailRejectionReason.maxOrdersPerDayExceeded, ); }); test('rejects when 4h-window notional plus new order would exceed config', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(maxNotionalUsdPer4h: 100), notionalUsd: 50, notionalUsdInWindow: 60, ); expect( decision.reason, GuardrailRejectionReason.maxNotionalUsdPer4hExceeded, ); }); test('allows a later order when prior 4h window has rolled off', () { // Caller queries trade_orders with submitted_at >= now() - 4h, so older // orders aren't counted here. final GuardrailDecision decision = _check( Guardrails(), config: _config(maxNotionalUsdPer4h: 100), notionalUsd: 40, notionalUsdInWindow: 0, ); expect(decision.allowed, isTrue); }); test('Guardrails.windowDuration is 4 hours by default', () { expect(Guardrails().windowDuration, const Duration(hours: 4)); }); test('rejects when require_question_before_order and no answer on file', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(), questionAnswered: false, ); expect(decision.reason, GuardrailRejectionReason.questionRequired); }); test('rejects when an unanswered question is still open', () { final GuardrailDecision decision = _check( Guardrails(), config: _config(), hasUnansweredQuestion: true, ); expect(decision.reason, GuardrailRejectionReason.unansweredQuestion); }); test('server ceiling overrides high config max_notional_usd_per_4h', () { final Guardrails g = Guardrails(serverMaxNotionalUsd: 25); final GuardrailDecision decision = _check( g, config: _config(maxNotionalUsdPer4h: 10000), notionalUsd: 100, ); expect( decision.reason, GuardrailRejectionReason.serverMaxNotionalUsdExceeded, ); }); test('refuses live mode unless allowLive=true', () { final GuardrailDecision blocked = _check( Guardrails(), config: _config(mode: 'live'), ); expect(blocked.reason, GuardrailRejectionReason.livePaperMismatch); final GuardrailDecision allowed = _check( Guardrails(allowLive: true), config: _config(mode: 'live'), ); expect(allowed.allowed, isTrue); }); }); }