@Tags(['integration', 'postgres']) library; import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart'; import 'package:cyberhybridhub_server/question_dispatcher.dart'; import 'package:cyberhybridhub_server/questions_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(); }); test('disabled source is never dispatched', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'dispatcher-spelling-only'; await testDb!.seedUser(uid); await testDb!.questionSourcePreferencesDb.save( uid, >[ {'id': 'market_history', 'enabled': false, 'ratio': 0}, {'id': 'spelling', 'enabled': true, 'ratio': 100}, ], ); final QuestionDispatcher dispatcher = testDb!.questionDispatcher(); final Map? payload = await dispatcher.dispatchNextQuestion(uid); expect(payload, isNotNull); expect(payload!['sourceId'], 'spelling'); expect(payload['presentation'], 'spelling_word'); }); test('spelling 100% with market enabled at 0 does not enqueue market fallback', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'dispatcher-spelling-100-no-market-fallback'; await testDb!.seedUser(uid); await testDb!.questionSourcePreferencesDb.save( uid, >[ {'id': 'market_history', 'enabled': true, 'ratio': 0}, {'id': 'spelling', 'enabled': true, 'ratio': 100}, ], ); final QuestionDispatcher dispatcher = testDb!.questionDispatcher(); await dispatcher.topUpQueue(uid); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT source_tag, pipeline_key FROM questions WHERE assigned_user_id = @uid AND user_response IS NULL ''', ), parameters: {'uid': uid}, ); expect(rows, isNotEmpty); for (final ResultRow row in rows) { expect( QuestionsDb.resolveSourceId({ 'sourceTag': row[0], 'pipelineKey': row[1], }), 'spelling', ); } }); test('70/30 mix yields spelling and respects fall-through over cycles', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'dispatcher-mix-uid'; await testDb!.seedUser(uid); await testDb!.questionSourcePreferencesDb.save( uid, >[ {'id': 'market_history', 'enabled': true, 'ratio': 70}, {'id': 'spelling', 'enabled': true, 'ratio': 30}, ], ); final QuestionDispatcher dispatcher = testDb!.questionDispatcher( testMode: true, ); final Set seenSources = {}; for (var cycle = 0; cycle < 16 && seenSources.length < 2; cycle++) { final Map? payload = await dispatcher.dispatchNextQuestion(uid); if (payload == null) { continue; } seenSources.add(payload['sourceId'] as String); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT id, pipeline_key FROM questions WHERE assigned_user_id = @uid AND user_response IS NULL ORDER BY created_at LIMIT 1 ''', ), parameters: {'uid': uid}, ); if (rows.isEmpty) { break; } final String questionId = rows.first[0]!.toString(); final String? pipelineKey = rows.first[1] as String?; await testDb!.questionsDb.submitAnswer( questionId: questionId, assignedUserId: uid, userResponse: 0, ); if (pipelineKey == PipelineKeys.spelling) { await testDb!.connection.execute( Sql.named( ''' UPDATE user_pipeline_state SET step = @idle WHERE assigned_user_id = @uid AND pipeline_key = @key ''', ), parameters: { 'uid': uid, 'key': PipelineKeys.spelling, 'idle': PipelineSteps.idle, }, ); } } expect(seenSources, contains('spelling')); expect(seenSources.length, greaterThanOrEqualTo(1)); }); }