cyberhybridhub/server/test/integration/spelling_catalog_test.dart
nathanAndrinoid 4459852f83
Some checks failed
Admin portal tests / test (push) Has been cancelled
spelling
2026-06-05 03:22:35 +00:00

210 lines
5.9 KiB
Dart

@Tags(<String>['integration', 'postgres'])
library;
import 'dart:convert';
import 'dart:math';
import 'package:cyberhybridhub_server/pipeline/question_pipeline.dart';
import 'package:cyberhybridhub_server/spelling/spelling_item_selector.dart';
import 'package:cyberhybridhub_server/spelling/spelling_items_db.dart';
import 'package:cyberhybridhub_server/spelling/user_spelling_progress_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('catalog seeds two items per curated word', () async {
if (testDb == null) {
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
return;
}
final Result words = await testDb!.connection.execute(
'SELECT COUNT(*)::int FROM spelling_words',
);
final Result items = await testDb!.connection.execute(
'SELECT COUNT(*)::int FROM spelling_items',
);
expect((words.first[0]! as num).toInt(), 10);
expect((items.first[0]! as num).toInt(), 20);
});
test('wrong answer increases familiarity weight for next pick', () async {
if (testDb == null) {
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
return;
}
const String uid = 'spelling-weight-uid';
await testDb!.seedUser(uid);
final SpellingItemsDb itemsDb = testDb!.spellingItemsDb;
final UserSpellingProgressDb progressDb =
UserSpellingProgressDb(testDb!.connection);
final SpellingItemSelector selector = SpellingItemSelector(
itemsDb: itemsDb,
random: Random(1),
);
final picked = await selector.pickForUser(uid);
expect(picked, isNotNull);
await progressDb.recordQuizOutcome(
firebaseUid: uid,
spellingItemId: picked!.id,
correct: false,
);
final int? score = await progressDb.familiarityScore(
firebaseUid: uid,
spellingItemId: picked.id,
);
expect(score, -1);
final QuestionPipeline pipeline = testDb!.questionPipeline();
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 DESC
LIMIT 1
''',
),
parameters: <String, dynamic>{'uid': uid},
);
await pipeline.prepareSpellingQuestion(
firebaseUid: uid,
questionId: reserved.first[0]!.toString(),
);
final Result rows = await testDb!.connection.execute(
Sql.named(
'''
SELECT metadata
FROM questions
WHERE assigned_user_id = @uid
ORDER BY created_at DESC
LIMIT 1
''',
),
parameters: <String, dynamic>{'uid': uid},
);
final Object? metadataRaw = rows.first[0];
final Map<String, dynamic> metadata = metadataRaw is Map<String, dynamic>
? metadataRaw
: Map<String, dynamic>.from(
jsonDecode(metadataRaw.toString()) as Map<String, dynamic>,
);
expect(metadata['spelling_item_id'], isNotEmpty);
});
test('quiz answer updates user_spelling_progress', () async {
if (testDb == null) {
markTestSkipped('Set DATABASE_URL or TEST_DATABASE_URL for integration tests');
return;
}
const String uid = 'spelling-progress-uid';
await testDb!.seedUser(uid);
final QuestionPipeline pipeline = testDb!.questionPipeline();
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: <String, dynamic>{'uid': uid},
);
await pipeline.prepareSpellingQuestion(
firebaseUid: uid,
questionId: reserved.first[0]!.toString(),
);
final Result first = await testDb!.connection.execute(
Sql.named(
'''
SELECT id, correct_answer, metadata
FROM questions
WHERE assigned_user_id = @uid
ORDER BY created_at
LIMIT 1
''',
),
parameters: <String, dynamic>{'uid': uid},
);
final String questionId = first.first[0]!.toString();
final Map<String, dynamic> metadata = _readMetadata(first.first[2]);
final String itemId = metadata['spelling_item_id']! as String;
final String canonicalWord = metadata['canonical_word']! as String;
final Map<String, dynamic>? 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 progress = await testDb!.connection.execute(
Sql.named(
'''
SELECT familiarity_score, attempt_count, correct_count, last_outcome
FROM user_spelling_progress
WHERE firebase_uid = @uid AND spelling_item_id = @item_id::uuid
''',
),
parameters: <String, dynamic>{'uid': uid, 'item_id': itemId},
);
expect(progress, hasLength(1));
expect(progress.first[0], 1);
expect(progress.first[1], 1);
expect(progress.first[2], 1);
expect(progress.first[3], 'correct');
});
}
Map<String, dynamic> _readMetadata(Object? raw) {
if (raw is Map<String, dynamic>) {
return raw;
}
if (raw is Map) {
return Map<String, dynamic>.from(raw);
}
return Map<String, dynamic>.from(
jsonDecode(raw.toString()) as Map<String, dynamic>,
);
}