// Version-skew smoke — the DEV Studio against an OLDER, released // hub binary. The two worst live bugs of the project were skew // bugs (ABI 1.0/1.1, a 0.21 hub without the ListInvocations RPC // rendered as "hub not reachable"), and no test ever ran the // combination operators actually have: a Studio that is newer // than its hub. // // Run through the platform repo's scripts/skew-smoke.sh, which // resolves the old binary and isolates $HOME: // // CHAIN_BIN= \ // flutter test integration_test/skew_smoke_test.dart -d macos // // Invariants per page (the hub IS running and healthy): // * never "nicht erreichbar" — a reachable hub must never be // rendered as unreachable, whatever RPCs it is missing // * never raw error text (gRPC walls, UnimplementedError, ...) // — missing RPCs must surface as classified, localised states // ("braucht eine neuere Hub-Version", feature-off, ...) import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:integration_test/integration_test.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:chain_client_sdk/chain_client_sdk.dart' show HubEndpoint; import 'package:chain_studio/data/hub.dart'; import 'package:chain_studio/main.dart'; import '../test/integration/hub_fixture.dart'; const _destinations = [ 'welcome', 'store', 'doctor', 'flows', 'audit', 'approvals', 'runs', 'federation', ]; const _rawErrorMarkers = [ 'gRPC Error', 'UnimplementedError', 'SocketException', 'Instance of', 'StackTrace', ]; Future _pumpFrames(WidgetTester tester, [int frames = 20]) async { for (var i = 0; i < frames; i++) { await tester.pump(const Duration(milliseconds: 100)); } } String _renderedText(WidgetTester tester) { final buf = StringBuffer(); for (final w in tester.widgetList(find.byType(Text))) { buf.writeln(w.data ?? w.textSpan?.toPlainText() ?? ''); } for (final w in tester.widgetList( find.byType(SelectableText), )) { buf.writeln(w.data ?? ''); } return buf.toString(); } void main() { IntegrationTestWidgetsFlutterBinding.ensureInitialized(); testWidgets('dev Studio against the released hub: reachable is never ' 'rendered unreachable, no raw errors', (tester) async { final fixture = await HubFixture.start(skipIfBinaryMissing: false); addTearDown(fixture!.dispose); final binary = (await HubFixture.binaryPath())!; final version = await Process.run(binary, ['--version']); // Surfaced in the runner output so the log states which skew // pair was actually exercised. // ignore: avoid_print print('skew-smoke: hub binary = ${(version.stdout as String).trim()}'); SharedPreferences.setMockInitialValues({}); await tester.pumpWidget( const StudioApp( initialThemeMode: ThemeModeValue.dark, initialLocale: Locale('de'), ), ); await HubService.instance.reconnect( HubEndpoint(host: '127.0.0.1', port: fixture.port), authToken: null, persist: false, ); await _pumpFrames(tester, 30); final shell = tester.state(find.byType(StudioShell)); final violations = []; for (final id in _destinations) { shell.navigateTo(id); await _pumpFrames(tester, 25); // Let in-flight gRPC frames land in REAL async before the next // navigation disposes the page: grpc-dart 4.2 adds incoming // data to an already-closed controller when a cancel races the // response (call.dart:395, unguarded `_responses.add`) — an // upstream bug that would fail the walk spuriously. await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 150)), ); final text = _renderedText(tester); if (text.contains('nicht erreichbar')) { violations.add( 'page "$id" claims the hub is unreachable although the ' 'fixture hub is healthy', ); } for (final marker in _rawErrorMarkers) { if (text.contains(marker)) { violations.add('page "$id" renders raw error text "$marker"'); } } } // Tear the app down BEFORE the fixture teardown closes the hub: // page subscriptions cancel while the channel is still alive, so // the shutdown cannot race live streams into add-after-close. await tester.pumpWidget(const SizedBox.shrink()); await tester.runAsync( () => Future.delayed(const Duration(milliseconds: 200)), ); await tester.pump(const Duration(minutes: 6)); expect(violations, isEmpty, reason: violations.join('\n')); }); }