cyberhybridhub/server/test/integration/trading_pipeline_guess_weekly_move_test.dart
2026-05-31 11:17:12 -05:00

293 lines
8.8 KiB
Dart

@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/trading_pipeline.dart';
import 'package:cyberhybridhub_server/trading/user_trading_state_db.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();
});
tearDown(() async {
if (testDb != null) {
await testDb!.truncateTradingTables();
}
});
tearDownAll(() async {
await testDb?.close();
});
final DateTime _testNow = DateTime.utc(2026, 5, 26, 12);
Future<void> _seedGuessUniverse() async {
await TradableAssetsDb(testDb!.connection).upsertAll(
<AlpacaAsset>[
AlpacaAsset(
symbol: 'SPY',
assetClass: 'us_equity',
tradable: true,
fractionable: true,
status: 'active',
),
],
now: _testNow,
);
final List<num> closes = <num>[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<TradingPipeline> _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<void> _enableGuessRule(String uid) async {
await testDb!.seedUser(uid);
await testDb!.tradingConfigDb.upsertUserConfig(
firebaseUid: uid,
templateName: 'default_paper_watchlist',
enabled: true,
config: <String, dynamic>{
'rules': <Map<String, dynamic>>[
<String, dynamic>{
'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.',
},
<String, dynamic>{
'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, <String>['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: <String, dynamic>{'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<String, dynamic> metadata =
rows.first[3]! as Map<String, dynamic>;
expect(metadata['guess_symbol'], 'SPY');
});
test('matching direction records score_delta +1', () 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<Map<String, dynamic>> open =
await testDb!.questionsDb.listUnansweredQuestions(uid);
final Map<String, dynamic>? 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<String, dynamic>? score =
await testDb!.userTradingStateDb.getGuessScore(uid);
expect(score, isNotNull);
expect(score!['total'], 1);
expect((score['last'] as Map)['score_delta'], 1);
});
test('non-matching direction records score_delta -1', () 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<Map<String, dynamic>> open =
await testDb!.questionsDb.listUnansweredQuestions(uid);
final Map<String, dynamic>? 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<String, dynamic>? score =
await testDb!.userTradingStateDb.getGuessScore(uid);
expect(score!['total'], -1);
expect((score['last'] as Map)['score_delta'], -1);
});
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<Map<String, dynamic>> open =
await testDb!.questionsDb.listUnansweredQuestions(uid);
final Map<String, dynamic>? 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<Map<String, dynamic>> 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: <String, dynamic>{'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<Map<String, dynamic>> open =
await testDb!.questionsDb.listUnansweredQuestions(uid);
final Map<String, dynamic>? 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)'),
);
});
});
}