@Tags(['integration', 'postgres']) library; import 'dart:convert'; import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart'; import 'package:cyberhybridhub_server/question_dispatcher.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(); }); Future _pipeline() async { return QuestionPipeline( questionsDb: testDb!.questionsDb, questionService: testDb!.questionService(), questionSourcePreferencesDb: testDb!.questionSourcePreferencesDb, testMode: true, ); } test('maintainSpellingForUser creates spelling pipeline question', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'spelling-maintain-uid'; await testDb!.seedUser(uid); final QuestionPipeline pipeline = await _pipeline(); await pipeline.maintainSpellingForUser(uid); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT pipeline_key, pipeline_step, source_tag FROM questions WHERE assigned_user_id = @uid ''', ), parameters: {'uid': uid}, ); expect(rows, hasLength(1)); expect(rows.first[0], PipelineKeys.spelling); expect(rows.first[1], PipelineSteps.spellingWordQuiz); expect(rows.first[2], 'spelling:word_quiz'); final Result meta = await testDb!.connection.execute( Sql.named( ''' SELECT metadata->>'spelling_item_id' FROM questions WHERE assigned_user_id = @uid ''', ), parameters: {'uid': uid}, ); expect(meta.first[0], isNull); }); test('prepareSpellingQuestion selects word and creates links at tile load', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'spelling-prepare-uid'; await testDb!.seedUser(uid); final QuestionPipeline pipeline = await _pipeline(); await pipeline.maintainSpellingForUser(uid); final Result reserved = await testDb!.connection.execute( Sql.named( ''' SELECT id, metadata FROM questions WHERE assigned_user_id = @uid ''', ), parameters: {'uid': uid}, ); final String questionId = reserved.first[0]!.toString(); final Map? prepared = await pipeline.prepareSpellingQuestion( firebaseUid: uid, questionId: questionId, ); expect(prepared, isNotNull); expect(prepared!['spokenWord'], isNotEmpty); expect(prepared['audioTitle'], questionId); expect(prepared['canonicalWord'], isNotEmpty); final Result link = await testDb!.connection.execute( Sql.named( ''' SELECT spelling_item_id FROM spelling_question_links WHERE question_id = @qid::uuid ''', ), parameters: {'qid': questionId}, ); expect(link, hasLength(1)); final Result progress = await testDb!.connection.execute( Sql.named( ''' SELECT familiarity_score, attempt_count FROM user_spelling_progress WHERE firebase_uid = @uid AND spelling_item_id = @item_id::uuid ''', ), parameters: { 'uid': uid, 'item_id': link.first[0]!.toString(), }, ); expect(progress, hasLength(1)); expect(progress.first[0], 0); expect(progress.first[1], 0); }); test('maintainSpellingForUser skips when spelling source disabled', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'spelling-disabled-uid'; await testDb!.seedUser(uid); await testDb!.questionSourcePreferencesDb.save( uid, >[ {'id': 'market_history', 'enabled': true, 'ratio': 50}, {'id': 'spelling', 'enabled': false, 'ratio': 50}, ], ); final QuestionPipeline pipeline = await _pipeline(); await pipeline.maintainSpellingForUser(uid); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM questions WHERE assigned_user_id = @uid ''', ), parameters: {'uid': uid}, ); expect(rows.first[0], 0); }); test('onAnswerSubmitted branches to follow-up then another word', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'spelling-answer-uid'; await testDb!.seedUser(uid); final QuestionPipeline pipeline = await _pipeline(); await pipeline.maintainSpellingForUser(uid); final Result reserved = await testDb!.connection.execute( Sql.named( ''' SELECT id FROM questions WHERE assigned_user_id = @uid ORDER BY created_at LIMIT 1 ''', ), parameters: {'uid': uid}, ); final String reservedId = reserved.first[0]!.toString(); await pipeline.prepareSpellingQuestion( firebaseUid: uid, questionId: reservedId, ); final Result first = await testDb!.connection.execute( Sql.named( ''' SELECT id, metadata FROM questions WHERE assigned_user_id = @uid ORDER BY created_at LIMIT 1 ''', ), parameters: {'uid': uid}, ); final String questionId = first.first[0]!.toString(); final Object? metadataRaw = first.first[1]; final Map metadata = metadataRaw is Map ? metadataRaw : Map.from( jsonDecode(metadataRaw.toString()) as Map, ); final String canonicalWord = metadata['canonical_word']! as String; final Map? answered = await testDb!.questionsDb.submitAnswer( questionId: questionId, assignedUserId: uid, userResponseText: canonicalWord, ); expect(answered, isNotNull); await pipeline.onAnswerSubmitted( firebaseUid: uid, answeredQuestion: answered!, userResponse: answered['userResponse'] as num? ?? 0, ); final Result rows = await testDb!.connection.execute( Sql.named( ''' SELECT pipeline_step, source_tag FROM questions WHERE assigned_user_id = @uid ORDER BY created_at ''', ), parameters: {'uid': uid}, ); expect(rows.length, greaterThanOrEqualTo(2)); expect(rows.last[0], PipelineSteps.spellingFollowUp); expect(rows.last[1], 'spelling:follow_up'); }); test('resetSpellingProgressForUser clears links and progress then re-seeds', () async { if (testDb == null) { markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests'); return; } const String uid = 'spelling-reset-uid'; await testDb!.seedUser(uid); final QuestionDispatcher dispatcher = testDb!.questionDispatcher(); final QuestionPipeline pipeline = testDb!.questionPipeline(testMode: true); pipeline.attachQuestionDispatcher(dispatcher); await pipeline.maintainSpellingForUser(uid); final Result reserved = await testDb!.connection.execute( Sql.named( ''' SELECT id FROM questions WHERE assigned_user_id = @uid LIMIT 1 ''', ), parameters: {'uid': uid}, ); final String questionId = reserved.first[0]!.toString(); await pipeline.prepareSpellingQuestion( firebaseUid: uid, questionId: questionId, ); final Result progressBefore = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM user_spelling_progress WHERE firebase_uid = @uid ''', ), parameters: {'uid': uid}, ); final Result linksBefore = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM spelling_question_links WHERE firebase_uid = @uid ''', ), parameters: {'uid': uid}, ); expect(progressBefore.first[0], greaterThan(0)); expect(linksBefore.first[0], greaterThan(0)); final Map result = await pipeline.resetSpellingProgressForUser(uid); expect(result['unansweredCount'], greaterThanOrEqualTo(1)); final Result progressAfter = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM user_spelling_progress WHERE firebase_uid = @uid ''', ), parameters: {'uid': uid}, ); final Result linksAfter = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM spelling_question_links WHERE firebase_uid = @uid ''', ), parameters: {'uid': uid}, ); expect(progressAfter.first[0], 0); expect(linksAfter.first[0], 0); final Result spellingQuestions = await testDb!.connection.execute( Sql.named( ''' SELECT COUNT(*)::int FROM questions WHERE assigned_user_id = @uid AND pipeline_key = 'spelling' AND user_response IS NULL ''', ), parameters: {'uid': uid}, ); expect(spellingQuestions.first[0], 1); }); }