Some checks failed
Admin portal tests / test (push) Has been cancelled
- Replaced single Postgres Connection with Pool across the server to prevent runTx locking - Added _filterForEnabledSources to client's _fetchUnansweredForPanel to strictly enforce local configuration Co-authored-by: Cursor <cursoragent@cursor.com>
170 lines
4.9 KiB
Dart
170 lines
4.9 KiB
Dart
@Tags(<String>['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,
|
|
<Map<String, dynamic>>[
|
|
<String, dynamic>{'id': 'market_history', 'enabled': false, 'ratio': 0},
|
|
<String, dynamic>{'id': 'spelling', 'enabled': true, 'ratio': 100},
|
|
],
|
|
);
|
|
|
|
final QuestionDispatcher dispatcher = testDb!.questionDispatcher();
|
|
final Map<String, dynamic>? 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,
|
|
<Map<String, dynamic>>[
|
|
<String, dynamic>{'id': 'market_history', 'enabled': true, 'ratio': 0},
|
|
<String, dynamic>{'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: <String, dynamic>{'uid': uid},
|
|
);
|
|
|
|
expect(rows, isNotEmpty);
|
|
for (final ResultRow row in rows) {
|
|
expect(
|
|
QuestionsDb.resolveSourceId(<String, dynamic>{
|
|
'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,
|
|
<Map<String, dynamic>>[
|
|
<String, dynamic>{'id': 'market_history', 'enabled': true, 'ratio': 70},
|
|
<String, dynamic>{'id': 'spelling', 'enabled': true, 'ratio': 30},
|
|
],
|
|
);
|
|
|
|
final QuestionDispatcher dispatcher = testDb!.questionDispatcher(
|
|
testMode: true,
|
|
);
|
|
|
|
final Set<String> seenSources = <String>{};
|
|
for (var cycle = 0; cycle < 16 && seenSources.length < 2; cycle++) {
|
|
final Map<String, dynamic>? 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: <String, dynamic>{'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: <String, dynamic>{
|
|
'uid': uid,
|
|
'key': PipelineKeys.spelling,
|
|
'idle': PipelineSteps.idle,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
|
|
expect(seenSources, contains('spelling'));
|
|
expect(seenSources.length, greaterThanOrEqualTo(1));
|
|
});
|
|
}
|