Skip to content

Commit 9323134

Browse files
author
GitLab CI
committed
feat: smart retry with auto-reconnect + assert_batch tool
1 parent 36f6211 commit 9323134

File tree

1 file changed

+138
-0
lines changed

1 file changed

+138
-0
lines changed

lib/src/cli/server.dart

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1920,6 +1920,31 @@ Detailed diagnostic report with:
19201920
},
19211921
},
19221922
},
1923+
{
1924+
"name": "assert_batch",
1925+
"description": "Run multiple assertions in a single call. Returns all results (does not fail-fast).",
1926+
"inputSchema": {
1927+
"type": "object",
1928+
"properties": {
1929+
"assertions": {
1930+
"type": "array",
1931+
"description": "List of assertions to run",
1932+
"items": {
1933+
"type": "object",
1934+
"properties": {
1935+
"type": {"type": "string", "enum": ["visible", "not_visible", "text", "element_count"], "description": "Assertion type"},
1936+
"key": {"type": "string", "description": "Element key"},
1937+
"text": {"type": "string", "description": "Text to find (for visible/not_visible) or expected text (for text assertion)"},
1938+
"expected": {"type": "string", "description": "Expected value for text assertion"},
1939+
"count": {"type": "integer", "description": "Expected count for element_count assertion"},
1940+
},
1941+
"required": ["type"],
1942+
},
1943+
},
1944+
},
1945+
"required": ["assertions"],
1946+
},
1947+
},
19231948

19241949
// === NEW: Page State ===
19251950
{
@@ -2137,7 +2162,78 @@ Detailed diagnostic report with:
21372162
return 'session_${DateTime.now().millisecondsSinceEpoch}';
21382163
}
21392164

2165+
/// Check if an error is retryable (transient connection/timeout issues)
2166+
bool _isRetryableError(dynamic error) {
2167+
final msg = error.toString().toLowerCase();
2168+
// NOT retryable
2169+
if (msg.contains('unknown tool')) return false;
2170+
if (msg.contains('required') && msg.contains('parameter')) return false;
2171+
if (msg.contains('element not found')) return false;
2172+
if (msg.contains('is required')) return false;
2173+
// Retryable
2174+
if (msg.contains('websocket')) return true;
2175+
if (msg.contains('connection closed')) return true;
2176+
if (msg.contains('connection reset')) return true;
2177+
if (msg.contains('not connected')) return true;
2178+
if (msg.contains('connection lost')) return true;
2179+
if (msg.contains('timed out') || msg.contains('timeout')) return true;
2180+
if (msg.contains('socket') && (msg.contains('closed') || msg.contains('error'))) return true;
2181+
return false;
2182+
}
2183+
2184+
/// Attempt auto-reconnect using last known connection info
2185+
Future<bool> _attemptAutoReconnect() async {
2186+
if (_lastConnectionUri != null) {
2187+
stderr.writeln('Attempting auto-reconnect to $_lastConnectionUri (port: $_lastConnectionPort)...');
2188+
try {
2189+
final client = _clients[_activeSessionId];
2190+
if (client is BridgeDriver) {
2191+
await client.connect();
2192+
stderr.writeln('Auto-reconnect successful');
2193+
return true;
2194+
}
2195+
} catch (e) {
2196+
stderr.writeln('Auto-reconnect failed: $e');
2197+
}
2198+
}
2199+
if (_cdpDriver != null && !_cdpDriver!.isConnected) {
2200+
stderr.writeln('CDP connection lost, attempting reconnect...');
2201+
try {
2202+
await _cdpDriver!.connect();
2203+
stderr.writeln('CDP auto-reconnect successful');
2204+
return true;
2205+
} catch (e) {
2206+
stderr.writeln('CDP auto-reconnect failed: $e');
2207+
}
2208+
}
2209+
return false;
2210+
}
2211+
21402212
Future<dynamic> _executeTool(String name, Map<String, dynamic> args) async {
2213+
const maxRetries = 2;
2214+
for (int attempt = 0; attempt <= maxRetries; attempt++) {
2215+
try {
2216+
final result = await _executeToolInner(name, args);
2217+
return result;
2218+
} catch (e) {
2219+
if (attempt < maxRetries && _isRetryableError(e)) {
2220+
stderr.writeln('Retryable error on attempt ${attempt + 1}: $e');
2221+
// Try auto-reconnect on connection errors
2222+
final msg = e.toString().toLowerCase();
2223+
if (msg.contains('not connected') || msg.contains('connection lost') || msg.contains('connection closed')) {
2224+
await _attemptAutoReconnect();
2225+
}
2226+
await Future.delayed(Duration(milliseconds: 500 * (attempt + 1)));
2227+
continue;
2228+
}
2229+
rethrow;
2230+
}
2231+
}
2232+
// Unreachable, but satisfies analyzer
2233+
throw StateError('Retry loop exited unexpectedly');
2234+
}
2235+
2236+
Future<dynamic> _executeToolInner(String name, Map<String, dynamic> args) async {
21412237
// Session management tools
21422238
if (name == 'list_sessions') {
21432239
return {
@@ -2268,6 +2364,10 @@ Detailed diagnostic report with:
22682364
// Always switch to the newly created session
22692365
_activeSessionId = sessionId;
22702366

2367+
// Store for auto-reconnect
2368+
_lastConnectionUri = uri;
2369+
_lastConnectionPort = int.tryParse(uri.split(':').last.split('/').first);
2370+
22712371
return {
22722372
"success": true,
22732373
"message": "Connected to $uri",
@@ -4208,6 +4308,44 @@ Detailed diagnostic report with:
42084308
final fc = _asFlutterClient(client!, 'scroll_until_visible');
42094309
return await _scrollUntilVisible(args, fc);
42104310

4311+
// === Batch Assertions ===
4312+
case 'assert_batch':
4313+
final assertions = (args['assertions'] as List<dynamic>?) ?? [];
4314+
final results = <Map<String, dynamic>>[];
4315+
int passed = 0;
4316+
int failed = 0;
4317+
for (final assertion in assertions) {
4318+
final a = assertion as Map<String, dynamic>;
4319+
final aType = a['type'] as String;
4320+
try {
4321+
final toolName = aType == 'visible' ? 'assert_visible'
4322+
: aType == 'not_visible' ? 'assert_not_visible'
4323+
: aType == 'text' ? 'assert_text'
4324+
: aType == 'element_count' ? 'assert_element_count'
4325+
: aType;
4326+
final toolArgs = <String, dynamic>{
4327+
if (a['key'] != null) 'key': a['key'],
4328+
if (a['text'] != null) 'text': a['text'],
4329+
if (a['expected'] != null) 'expected': a['expected'],
4330+
if (a['count'] != null) 'expected_count': a['count'],
4331+
};
4332+
final result = await _executeToolInner(toolName, toolArgs);
4333+
final success = result is Map && result['success'] == true;
4334+
if (success) passed++; else failed++;
4335+
results.add({'type': aType, 'success': success, 'result': result});
4336+
} catch (e) {
4337+
failed++;
4338+
results.add({'type': aType, 'success': false, 'error': e.toString()});
4339+
}
4340+
}
4341+
return {
4342+
'success': failed == 0,
4343+
'total': assertions.length,
4344+
'passed': passed,
4345+
'failed': failed,
4346+
'results': results,
4347+
};
4348+
42114349
// === NEW: Assertions ===
42124350
case 'assert_visible':
42134351
if (client is BridgeDriver) {

0 commit comments

Comments
 (0)