import 'dart:async'; import 'package:flutter_test/flutter_test.dart'; import 'package:ikar/backend/api.dart'; import 'package:ikar/core/calls/call_controller.dart'; import 'package:ikar/core/protocol/packet.dart'; import 'package:kusoft/src/rust/api.dart' show CallParams; import 'package:kusoft/src/rust/frb_generated.dart' show RustLib, RustLibApi; class _PushApi extends Api { final packets = StreamController.broadcast(); @override Stream get pushStream => packets.stream; } class _CallDecoder implements RustLibApi { @override CallParams? crateApiDecodeVcp({ required String vcp, required String conversationId, }) => const CallParams( token: 'synthetic', wsEndpoint: 'wss://calls.invalid/ws2', turn: [], isVideo: false, ); @override dynamic noSuchMethod(Invocation invocation) => super.noSuchMethod(invocation); } void main() { TestWidgetsFlutterBinding.ensureInitialized(); setUpAll(() => RustLib.initMock(api: _CallDecoder())); late _PushApi api; late CallController controller; setUp(() { api = _PushApi(); controller = CallController.forTesting()..appResumed = true; controller.init(api); }); tearDown(() async { controller.dispose(); await api.packets.close(); await api.dispose(); }); Future push(Map data) async { api.packets.add(Packet(opcode: 137, payload: data)); await Future.delayed(Duration.zero); } Map invite(String id) => { 'conversationId': id, 'callerId': 2, 'vcp': 'synthetic-incoming', 'type': 'AUDIO', }; test( 'caller cancellation dismisses a ringing recipient without WS2', () async { var canceled = 0; final sub = controller.incomingCanceled.listen((_) => canceled++); await push(invite('first')); expect(controller.pendingIncoming?.conversationId, 'first'); await push({'conversationId': 'first', 'event': 'ended'}); expect(controller.pendingIncoming, isNull); expect(canceled, 1); await sub.cancel(); }, ); test('late invite after cancellation does not start ringing', () async { await push({'conversationId': 'first', 'event': 'ended'}); await push(invite('first')); controller.injectFromNative(invite('first'), autoAccept: true); expect(controller.pendingIncoming, isNull); }); test('old cancellation cannot dismiss a newer incoming call', () async { await push(invite('new')); await push({'conversationId': 'old', 'event': 'ended'}); expect(controller.pendingIncoming?.conversationId, 'new'); }); test( 'expired invite is ignored and committed history refreshes listeners', () async { await push({ ...invite('expired'), 'expiresAt': DateTime.now().millisecondsSinceEpoch - 1000, }); expect(controller.pendingIncoming, isNull); var refreshed = 0; final sub = controller.callEnded.listen((_) => refreshed++); await push({'conversationId': 'expired', 'event': 'history'}); expect(refreshed, 1); await sub.cancel(); }, ); }