From 9db79e9d4034e6b07aa0fe5f826e2637a64fcf2b Mon Sep 17 00:00:00 2001 From: just-stuff-tm Date: Tue, 10 Mar 2026 20:06:05 -0400 Subject: [PATCH] test(tcp): harden cancel-race handling and add coverage - tighten late TCP connect error suppression to manual-cancel disconnecting/disconnected windows - keep TCP handshake failures surfaced outside explicit cancel flow - allow TcpScreen connect action when connector is scanning - add connector-level tests for late-error suppression classifier - add TcpScreen test covering connect from scanning state --- lib/connector/meshcore_connector.dart | 25 ++++++-- lib/screens/tcp_screen.dart | 6 +- ...hcore_connector_tcp_error_filter_test.dart | 64 +++++++++++++++++++ test/screens/tcp_flow_test.dart | 23 +++++++ 4 files changed, 113 insertions(+), 5 deletions(-) create mode 100644 test/connector/meshcore_connector_tcp_error_filter_test.dart diff --git a/lib/connector/meshcore_connector.dart b/lib/connector/meshcore_connector.dart index aad2c71..0de1a90 100644 --- a/lib/connector/meshcore_connector.dart +++ b/lib/connector/meshcore_connector.dart @@ -1059,10 +1059,14 @@ class MeshCoreConnector extends ChangeNotifier { await syncTime(); } catch (error) { _appDebugLogService?.error('TCP connection error: $error', tag: 'TCP'); - final tcpConnectNoLongerActive = - _activeTransport != MeshCoreTransportType.tcp || - _state != MeshCoreConnectionState.connecting; - if (tcpConnectNoLongerActive) { + final tcpConnectCancelledBeforeHandshake = + shouldIgnoreLateTcpConnectError( + manualDisconnect: _manualDisconnect, + state: _state, + activeTransport: _activeTransport, + tcpManagerConnected: _tcpManager.isConnected, + ); + if (tcpConnectCancelledBeforeHandshake) { _appDebugLogService?.info( 'Ignoring late TCP connect error after cancellation/switch: state=$_state transport=$_activeTransport', tag: 'TCP', @@ -1074,6 +1078,19 @@ class MeshCoreConnector extends ChangeNotifier { } } + @visibleForTesting + static bool shouldIgnoreLateTcpConnectError({ + required bool manualDisconnect, + required MeshCoreConnectionState state, + required MeshCoreTransportType activeTransport, + required bool tcpManagerConnected, + }) { + return manualDisconnect && + (state == MeshCoreConnectionState.disconnected || + state == MeshCoreConnectionState.disconnecting) && + (activeTransport != MeshCoreTransportType.tcp || !tcpManagerConnected); + } + Future connect(BluetoothDevice device, {String? displayName}) async { if (_state == MeshCoreConnectionState.connecting || _state == MeshCoreConnectionState.connected) { diff --git a/lib/screens/tcp_screen.dart b/lib/screens/tcp_screen.dart index f3702c3..55bec20 100644 --- a/lib/screens/tcp_screen.dart +++ b/lib/screens/tcp_screen.dart @@ -224,7 +224,11 @@ class _TcpScreenState extends State { } Future _connectTcp() async { - if (_connector.state != MeshCoreConnectionState.disconnected) return; + if (_connector.state == MeshCoreConnectionState.connecting || + _connector.state == MeshCoreConnectionState.connected || + _connector.state == MeshCoreConnectionState.disconnecting) { + return; + } final host = _hostController.text.trim(); final parsedPort = int.tryParse(_portController.text.trim()); diff --git a/test/connector/meshcore_connector_tcp_error_filter_test.dart b/test/connector/meshcore_connector_tcp_error_filter_test.dart new file mode 100644 index 0000000..ee6a382 --- /dev/null +++ b/test/connector/meshcore_connector_tcp_error_filter_test.dart @@ -0,0 +1,64 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:meshcore_open/connector/meshcore_connector.dart'; + +void main() { + group('shouldIgnoreLateTcpConnectError', () { + test('returns true for manual cancel during disconnecting state', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isTrue); + }); + + test( + 'returns true for manual cancel after reaching disconnected state', + () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnected, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isTrue); + }, + ); + + test('returns false when not a manual disconnect', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: false, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.bluetooth, + tcpManagerConnected: false, + ); + + expect(result, isFalse); + }); + + test('returns false for connected state handshake failures', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.connected, + activeTransport: MeshCoreTransportType.tcp, + tcpManagerConnected: true, + ); + + expect(result, isFalse); + }); + + test('returns false when TCP is still active while disconnecting', () { + final result = MeshCoreConnector.shouldIgnoreLateTcpConnectError( + manualDisconnect: true, + state: MeshCoreConnectionState.disconnecting, + activeTransport: MeshCoreTransportType.tcp, + tcpManagerConnected: true, + ); + + expect(result, isFalse); + }); + }); +} diff --git a/test/screens/tcp_flow_test.dart b/test/screens/tcp_flow_test.dart index 501046d..5c240f4 100644 --- a/test/screens/tcp_flow_test.dart +++ b/test/screens/tcp_flow_test.dart @@ -135,6 +135,29 @@ void main() { await tester.pump(const Duration(milliseconds: 60)); }); + testWidgets('TcpScreen allows connect while connector is scanning', ( + tester, + ) async { + final connector = _FakeMeshCoreConnector() + ..initialState = MeshCoreConnectionState.scanning; + + await tester.pumpWidget( + _buildTestApp( + connector: connector, + child: const TcpScreen(), + locale: const Locale('en'), + ), + ); + await tester.pumpAndSettle(); + + await tester.tap(find.widgetWithText(FilledButton, 'Connect')); + await tester.pumpAndSettle(); + + expect(connector.connectTcpCalls, 1); + expect(connector.lastHost, '192.168.40.10'); + expect(connector.lastPort, 5000); + }); + testWidgets('TcpScreen narrow width long status text does not overflow', ( tester, ) async {