chain-studio/integration_test/skew_smoke_test.dart
flemming-it bfd58baa75
Some checks failed
Security / Security check (push) Failing after 2s
test(skew): version-skew smoke — dev Studio against the released hub
Walks every sidebar page against an older, released chain binary
(hermetic fixture hub) and fails when any page claims 'unreachable'
although the hub answers, or renders raw error text. Run via the
platform repo's scripts/skew-smoke.sh as a Studio release gate.

Verified against the real pair hub 0.21.0 vs workspace 0.22.0: all
invariants hold. Two timing notes are handled explicitly: real-async
settles between navigations and an app teardown before the fixture
teardown keep grpc-dart's data-after-cancel race (unguarded
_responses.add, call.dart:395) from failing the walk spuriously;
the final fake-time pump drains the channel's 5-minute idle timer.

Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
2026-07-19 02:08:48 +02:00

137 lines
4.7 KiB
Dart

// 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=<old released chain> \
// 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 = <String>[
'welcome',
'store',
'doctor',
'flows',
'audit',
'approvals',
'runs',
'federation',
];
const _rawErrorMarkers = <String>[
'gRPC Error',
'UnimplementedError',
'SocketException',
'Instance of',
'StackTrace',
];
Future<void> _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<Text>(find.byType(Text))) {
buf.writeln(w.data ?? w.textSpan?.toPlainText() ?? '');
}
for (final w in tester.widgetList<SelectableText>(
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<StudioShellState>(find.byType(StudioShell));
final violations = <String>[];
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<void>.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<void>.delayed(const Duration(milliseconds: 200)),
);
await tester.pump(const Duration(minutes: 6));
expect(violations, isEmpty, reason: violations.join('\n'));
});
}