@Tags(['integration', 'postgres']) library; import 'package:cyberhybridhub_server/alpaca/alpaca_models.dart'; import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart'; import 'package:cyberhybridhub_server/trading/market_history_config.dart'; import 'package:cyberhybridhub_server/trading/market_history_query.dart'; import 'package:cyberhybridhub_server/trading/tradable_assets_db.dart'; import 'package:cyberhybridhub_server/trading/prospective_answer_scoring.dart'; import 'package:cyberhybridhub_server/trading/trading_pipeline.dart'; import 'package:postgres/postgres.dart'; import 'package:test/test.dart'; import '../helpers/test_db.dart'; void main() { TestDb? testDb; setUpAll(() async { testDb = await TestDb.open(); ProspectiveAnswerScoring.closenessExtraPointsEnabled = true; }); tearDown(() async { if (testDb != null) { await testDb!.truncateTradingTables(); } }); tearDownAll(() async { ProspectiveAnswerScoring.closenessExtraPointsEnabled = false; await testDb?.close(); }); final DateTime _testNow = DateTime.utc(2026, 5, 26, 12); Future _seedGuessUniverse() async { await TradableAssetsDb(testDb!.connection).upsertAll( [ AlpacaAsset( symbol: 'SPY', assetClass: 'us_equity', tradable: true, fractionable: true, status: 'active', ), ], now: _testNow, ); final List closes = [500, 501, 502, 503, 504, 505, 510]; for (int i = 0; i < closes.length; i++) { await testDb!.marketDataDb.upsertSnapshot( symbol: 'SPY', metric: 'bar', timeframe: MarketHistoryConfig.barTimeframe, asOf: _testNow.subtract(Duration(hours: 4 * (closes.length - 1 - i))), price: closes[i], ); } } Future _guessPipeline() async { return TradingPipeline( questionsDb: testDb!.questionsDb, questionService: testDb!.questionService(), marketDataDb: testDb!.marketDataDb, tradingConfigDb: testDb!.tradingConfigDb, tradingStateDb: testDb!.userTradingStateDb, marketHistoryQuery: MarketHistoryQuery(connection: testDb!.connection), clock: () => _testNow, ); } Future _enableGuessRule(String uid) async { await testDb!.seedUser(uid); await testDb!.tradingConfigDb.upsertUserConfig( firebaseUid: uid, templateName: 'default_paper_watchlist', enabled: true, config: { 'rules': >[ { 'id': 'guess_weekly_move', 'type': 'guess_weekly_move', 'question_template': '{{token}} was {{ref_price}} {{ref_days_ago}} days ago. Swipe +10 if up, -10 if down.', }, { 'id': 'dip_confirm', 'threshold_pct': 0, }, ], }, ); } group('guess_weekly_move pipeline', () { test('evaluate creates obfuscated question with metadata.guess_symbol', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-eval-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); final TradingEvaluationResult result = await pipeline.evaluate(uid); expect(result.questionsCreated, 1); expect(result.rulesFired, ['guess_weekly_move']); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT pipeline_key, pipeline_step, question_text, metadata FROM questions WHERE assigned_user_id = @uid ''', ), parameters: {'uid': uid}, ); expect(rows, hasLength(1)); expect(rows.first[0], PipelineKeys.trading); expect(rows.first[1], 'guess_weekly_move:${TradingPhases.awaitAnswer}'); final String text = rows.first[2]! as String; expect(text, contains('ASSET_A')); expect(text, isNot(contains('SPY'))); final Map metadata = rows.first[3]! as Map; expect(metadata['guess_symbol'], 'SPY'); }); test('matching direction and within ±1 records full 2-point score', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-score-match-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); await pipeline.evaluate(uid); final List> open = await testDb!.questionsDb.listUnansweredQuestions(uid); final Map? updated = await testDb!.questionsDb.submitAnswer( questionId: open.single['id'] as String, assignedUserId: uid, userResponse: 9, ); await pipeline.handleAnswer( firebaseUid: uid, answeredQuestion: updated!, userResponse: 9, ); final Map? score = await testDb!.userTradingStateDb.getGuessScore(uid); expect(score, isNotNull); expect(score!['total'], 2); expect((score['last'] as Map)['score_delta'], 2); final Map? ruleState = await testDb!.userTradingStateDb.getRuleState(uid, 'guess_weekly_move'); expect(ruleState!['direction_point'], 1); expect(ruleState['closeness_point'], 1); expect(ruleState['answer_score'], 2); }); test('matching direction but farther magnitude records fractional score', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-score-miss-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); await pipeline.evaluate(uid); final List> open = await testDb!.questionsDb.listUnansweredQuestions(uid); final Map? updated = await testDb!.questionsDb.submitAnswer( questionId: open.single['id'] as String, assignedUserId: uid, userResponse: 6, ); await pipeline.handleAnswer( firebaseUid: uid, answeredQuestion: updated!, userResponse: 6, ); final Map? score = await testDb!.userTradingStateDb.getGuessScore(uid); expect(score!['total'], closeTo(1.7, 0.001)); expect((score['last'] as Map)['score_delta'], closeTo(1.7, 0.001)); }); test('non-matching direction records -2 score and ignores closeness', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-score-miss-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); await pipeline.evaluate(uid); final List> open = await testDb!.questionsDb.listUnansweredQuestions(uid); final Map? updated = await testDb!.questionsDb.submitAnswer( questionId: open.single['id'] as String, assignedUserId: uid, userResponse: -10, ); await pipeline.handleAnswer( firebaseUid: uid, answeredQuestion: updated!, userResponse: -10, ); final Map? score = await testDb!.userTradingStateDb.getGuessScore(uid); expect(score!['total'], -2); expect((score['last'] as Map)['score_delta'], -2); final Map? ruleState = await testDb!.userTradingStateDb.getRuleState(uid, 'guess_weekly_move'); expect(ruleState!['direction_point'], -2); expect(ruleState['closeness_point'], 0); expect(ruleState['answer_score'], -2); }); test('handleAnswer never stages pending orders; actuator not invoked', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-no-actuator-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); await pipeline.evaluate(uid); final List> open = await testDb!.questionsDb.listUnansweredQuestions(uid); final Map? updated = await testDb!.questionsDb.submitAnswer( questionId: open.single['id'] as String, assignedUserId: uid, userResponse: 10, ); await pipeline.handleAnswer( firebaseUid: uid, answeredQuestion: updated!, userResponse: 10, ); final List> pending = await testDb!.userTradingStateDb.listPendingOrders(uid); expect(pending, isEmpty); final Result orders = await testDb!.connection.execute( Sql.named( 'SELECT COUNT(*)::int FROM trade_orders WHERE firebase_uid = @uid', ), parameters: {'uid': uid}, ); expect(orders.first[0], 0); }); test('symbol cooldown prevents re-pick within GUESS_COOLDOWN_HOURS', () async { if (testDb == null) { markTestSkipped( 'Set DATABASE_URL or TEST_DATABASE_URL for integration tests', ); return; } const String uid = 'guess-cooldown-uid'; await _enableGuessRule(uid); await _seedGuessUniverse(); final TradingPipeline pipeline = await _guessPipeline(); await pipeline.evaluate(uid); final List> open = await testDb!.questionsDb.listUnansweredQuestions(uid); final Map? updated = await testDb!.questionsDb.submitAnswer( questionId: open.single['id'] as String, assignedUserId: uid, userResponse: 10, ); await pipeline.handleAnswer( firebaseUid: uid, answeredQuestion: updated!, userResponse: 10, ); final TradingEvaluationResult again = await pipeline.evaluate(uid); expect(again.questionsCreated, 0); expect( again.rulesSkipped, contains('guess_weekly_move(insufficientBars)'), ); }); }); }