iykyka/mobile/test/message_sync_test.dart

110 lines
3.9 KiB
Dart

import 'package:flutter_test/flutter_test.dart';
import 'package:ykavu_mobile/models/models.dart';
import 'package:ykavu_mobile/services/message_sync.dart';
ChatMessage _msg(int id, {bool retracted = false, bool? naturalnessRating}) => ChatMessage(
id: id,
conversationId: 1,
senderId: 1,
senderMode: SenderMode.human,
text: 'msg-$id',
retracted: retracted,
createdAt: DateTime(2026, 8, 3),
naturalnessRating: naturalnessRating,
);
void main() {
group('nextReconnectDelay', () {
test('first attempt (0) is the initial 1s delay', () {
expect(nextReconnectDelay(0), const Duration(seconds: 1));
});
test('doubles each attempt: 1s, 2s, 4s, 8s, 16s', () {
expect(nextReconnectDelay(1), const Duration(seconds: 2));
expect(nextReconnectDelay(2), const Duration(seconds: 4));
expect(nextReconnectDelay(3), const Duration(seconds: 8));
expect(nextReconnectDelay(4), const Duration(seconds: 16));
});
test('caps at 30s once doubling would exceed it', () {
expect(nextReconnectDelay(5), const Duration(seconds: 30));
expect(nextReconnectDelay(6), const Duration(seconds: 30));
});
test('stays capped even for a very large attempt count (no overflow)', () {
expect(nextReconnectDelay(1000000), const Duration(seconds: 30));
});
test('negative attempt is treated like the first attempt', () {
expect(nextReconnectDelay(-1), const Duration(seconds: 1));
});
});
group('highestMessageId', () {
test('empty list -> null', () {
expect(highestMessageId(const []), isNull);
});
test('returns the max id regardless of list order', () {
expect(highestMessageId([_msg(3), _msg(1), _msg(5), _msg(2)]), 5);
});
});
group('mergeNewMessages', () {
test('appends fresh messages not already present, sorted by id', () {
final existing = [_msg(1), _msg(2)];
final fresh = [_msg(4), _msg(3)];
final merged = mergeNewMessages(existing, fresh);
expect(merged.map((m) => m.id).toList(), [1, 2, 3, 4]);
});
test('drops fresh messages whose id is already loaded (WS/REST race)', () {
final existing = [_msg(1), _msg(2), _msg(3)];
final fresh = [_msg(2), _msg(3), _msg(4)];
final merged = mergeNewMessages(existing, fresh);
expect(merged.map((m) => m.id).toList(), [1, 2, 3, 4]);
});
test('fresh entirely a subset of existing -> unchanged, same order', () {
final existing = [_msg(1), _msg(2)];
final merged = mergeNewMessages(existing, [_msg(1), _msg(2)]);
expect(merged.map((m) => m.id).toList(), [1, 2]);
});
test('existing empty -> result is just fresh, sorted', () {
final merged = mergeNewMessages(const [], [_msg(2), _msg(1)]);
expect(merged.map((m) => m.id).toList(), [1, 2]);
});
test('does not mutate the existing list instance', () {
final existing = [_msg(1)];
final merged = mergeNewMessages(existing, [_msg(2)]);
expect(existing.length, 1);
expect(merged.length, 2);
});
});
// "이 답장 나답아요?" 피드백(vision.md 지표, deploy-checklist.md N4-12):
// chat_screen.dart는 이 순수 함수의 결과를 세션 상태의 "이미 평가됨" id
// 집합에 합쳐서, 히스토리를 다시 불러와도(화면 재진입, 캐치업 등) 이미
// 평가된 메시지 위에 뱃지/버튼을 다시 보여주지 않는다.
group('ratedMessageIdsFrom', () {
test('picks up ids with a non-null naturalness rating (true or false)', () {
final messages = [
_msg(1, naturalnessRating: true),
_msg(2, naturalnessRating: false),
_msg(3),
];
expect(ratedMessageIdsFrom(messages), {1, 2});
});
test('empty list -> empty set', () {
expect(ratedMessageIdsFrom(const []), isEmpty);
});
test('no rated messages -> empty set', () {
expect(ratedMessageIdsFrom([_msg(1), _msg(2)]), isEmpty);
});
});
}