fix(setup): wizard errors copyable above the dialog, CLI skew explained, exec transparency
Some checks failed
Security / Security check (push) Failing after 2s
Some checks failed
Security / Security check (push) Failing after 2s
Field test of the setup wizard surfaced three trust breaks in one run: an unexplained macOS Documents permission prompt, a perceived crash, and an error message whose copy button could not be reached. Root causes and fixes: - chain init failures were shown as a SnackBar, which lands BEHIND the wizard's modal barrier: dimmed, clipped, copy unreachable — and the click aimed at it hit the barrier, dismissing the whole wizard with all answers (the perceived crash). Errors now open a modal dialog ABOVE the wizard via showChainErrorDialog with a copyable detail block, and the wizard is no longer barrier-dismissible. - When the resolved chain binary is older than Studio and rejects --plan-json, the wizard now explains the version skew in plain language (binary path + update path) instead of leaking a raw clap usage error. A missing binary gets its own localized story. - Step 3 announces which chain binary the preview will execute; when that binary physically lives (symlinks resolved) in a TCC-protected folder, the wizard pre-explains the macOS folder prompt. Supporting changes: FriendlyError passes through friendlyError() unchanged so call sites can ship precise localized stories through the shared presentation; SystemActions gains resolvedChainBinary() plus run/resolve test seams; ChainErrorBox hugs its content instead of filling an unbounded dialog; the wizard's answers file is written synchronously (the async dart:io variants never complete under the widget-test fake-async zone). Verified: flutter analyze clean, 53 tests green (6 new wizard error- path tests incl. clipboard round-trip), plus a live GUI walk on macOS in dark + light with a stale binary (skew dialog, copy verified via clipboard) and with the real binary (TCC pre-explanation with the resolved path, full plan preview). Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
parent
4ceb5bb567
commit
c6da5025ce
11 changed files with 524 additions and 10 deletions
|
|
@ -1,12 +1,18 @@
|
|||
// Guided-setup wizard — the three explained answer steps. Verifies
|
||||
// that options render as localized cards with their one-line
|
||||
// explanation (no English enum humanization) and that Weiter/Zurück
|
||||
// walk the steps. The review step calls the CLI (a subprocess) and is
|
||||
// out of scope for a widget test.
|
||||
// walk the steps. CLI calls run through the SystemActions test seam
|
||||
// so the error paths (stale binary, generic failure) are covered
|
||||
// without a subprocess.
|
||||
|
||||
import 'dart:io' show Directory, Platform;
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
|
||||
import 'package:chain_studio/data/chain_log.dart';
|
||||
import 'package:chain_studio/data/system_actions.dart';
|
||||
import 'package:chain_studio/l10n/app_localizations.dart';
|
||||
import 'package:chain_studio/widgets/guided_setup_dialog.dart';
|
||||
|
||||
|
|
@ -247,6 +253,168 @@ void main() {
|
|||
},
|
||||
);
|
||||
|
||||
group('CLI paths (SystemActions test seam)', () {
|
||||
late Directory tmp;
|
||||
|
||||
setUp(() {
|
||||
tmp = Directory.systemTemp.createTempSync('wizard-test-');
|
||||
ChainLog.testPathOverride = '${tmp.path}/studio-errors.log';
|
||||
});
|
||||
|
||||
tearDown(() {
|
||||
ChainLog.testPathOverride = null;
|
||||
SystemActions.debugRunFaiOverride = null;
|
||||
SystemActions.debugResolveOverride = null;
|
||||
tmp.deleteSync(recursive: true);
|
||||
});
|
||||
|
||||
Future<void> walkToStep3(WidgetTester tester) async {
|
||||
await tester.pumpWidget(_host());
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Schritt 3 von 3'), findsOneWidget);
|
||||
}
|
||||
|
||||
testWidgets('a stray click outside the wizard does not dismiss it', (
|
||||
tester,
|
||||
) async {
|
||||
await tester.pumpWidget(_host());
|
||||
await tester.tap(find.text('open'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Schritt 1 von 3'), findsOneWidget);
|
||||
|
||||
// The exact click that used to end the wizard: aiming at an
|
||||
// error SnackBar dimmed below the modal barrier.
|
||||
await tester.tapAt(const Offset(4, 4));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Schritt 1 von 3'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'stale chain binary → explained, copyable error dialog above the intact wizard',
|
||||
(tester) async {
|
||||
const clapError =
|
||||
"error: unexpected argument '--plan-json' found\n\n"
|
||||
" tip: a similar argument exists: '--plan-out'\n\n"
|
||||
'Usage: chain init --answers <ANSWERS> --plan-out <PLAN_OUT>';
|
||||
SystemActions.debugResolveOverride = () => '/Users/op/.chain/bin/chain';
|
||||
SystemActions.debugRunFaiOverride = (args) async =>
|
||||
(ok: false, stdout: '', stderr: clapError);
|
||||
|
||||
final copied = <String>[];
|
||||
tester.binding.defaultBinaryMessenger.setMockMethodCallHandler(
|
||||
SystemChannels.platform,
|
||||
(call) async {
|
||||
if (call.method == 'Clipboard.setData') {
|
||||
copied.add((call.arguments as Map)['text'] as String);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
);
|
||||
|
||||
await walkToStep3(tester);
|
||||
await tester.tap(find.text('Weiter')); // triggers `chain init`
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
// Plain-language headline naming the skew — not a raw clap
|
||||
// usage wall — plus the binary that was executed.
|
||||
expect(find.textContaining('älter als Studio'), findsOneWidget);
|
||||
expect(
|
||||
find.textContaining('/Users/op/.chain/bin/chain'),
|
||||
findsWidgets,
|
||||
);
|
||||
|
||||
// The verbatim CLI output is one copy-click away (the old
|
||||
// SnackBar rendition sat behind the modal barrier where the
|
||||
// copy button could not be reached at all).
|
||||
await tester.tap(find.byIcon(Icons.content_copy));
|
||||
await tester.pump();
|
||||
expect(copied, hasLength(1));
|
||||
expect(copied.single, contains('unexpected argument'));
|
||||
// Let the button's transient "copied" checkmark reset so no
|
||||
// timer outlives the test.
|
||||
await tester.pump(const Duration(seconds: 2));
|
||||
|
||||
// Dismissing the error returns to the intact wizard — the
|
||||
// failure must not cost the operator their answers.
|
||||
await tester.tap(find.text('OK'));
|
||||
await tester.pumpAndSettle();
|
||||
expect(find.text('Schritt 3 von 3'), findsOneWidget);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('generic init failure names the executed binary', (
|
||||
tester,
|
||||
) async {
|
||||
SystemActions.debugResolveOverride = () => '/Users/op/.chain/bin/chain';
|
||||
SystemActions.debugRunFaiOverride = (args) async =>
|
||||
(ok: false, stdout: '', stderr: 'boom: config unreadable');
|
||||
|
||||
await walkToStep3(tester);
|
||||
await tester.tap(find.text('Weiter'));
|
||||
await tester.pumpAndSettle();
|
||||
|
||||
expect(
|
||||
find.textContaining('Die Plan-Vorschau konnte nicht erstellt werden'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(find.textContaining('Ausgeführt wurde:'), findsOneWidget);
|
||||
});
|
||||
|
||||
testWidgets('step 3 announces which binary the wizard will run', (
|
||||
tester,
|
||||
) async {
|
||||
SystemActions.debugResolveOverride = () => '/Users/op/.chain/bin/chain';
|
||||
SystemActions.debugRunFaiOverride = (args) async =>
|
||||
(ok: false, stdout: '', stderr: 'unused');
|
||||
|
||||
await walkToStep3(tester);
|
||||
expect(
|
||||
find.textContaining('führt Studio das Programm „chain“ aus'),
|
||||
findsOneWidget,
|
||||
);
|
||||
expect(
|
||||
find.textContaining('/Users/op/.chain/bin/chain'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
|
||||
testWidgets(
|
||||
'step 3 pre-explains the macOS folder prompt for a Documents-resident binary',
|
||||
(tester) async {
|
||||
final home = Platform.environment['HOME'];
|
||||
if (!Platform.isMacOS || home == null || home.isEmpty) {
|
||||
return; // TCC folder prompts are a macOS-only concern.
|
||||
}
|
||||
SystemActions.debugResolveOverride = () => '$home/Documents/dev/chain';
|
||||
SystemActions.debugRunFaiOverride = (args) async =>
|
||||
(ok: false, stdout: '', stderr: 'unused');
|
||||
|
||||
await walkToStep3(tester);
|
||||
expect(
|
||||
find.textContaining('nach Zugriff auf diesen Ordner'),
|
||||
findsOneWidget,
|
||||
);
|
||||
},
|
||||
);
|
||||
|
||||
testWidgets('step 3 says so when no chain binary can be found', (
|
||||
tester,
|
||||
) async {
|
||||
SystemActions.debugResolveOverride = () => null;
|
||||
|
||||
await walkToStep3(tester);
|
||||
expect(
|
||||
find.textContaining('wurde auf diesem Rechner nicht gefunden'),
|
||||
findsOneWidget,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('parseSetupSuggestion', () {
|
||||
test('accepts a valid JSON object embedded in prose', () {
|
||||
final r = parseSetupSuggestion(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue