fix(setup): wizard errors copyable above the dialog, CLI skew explained, exec transparency
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:
flemming-it 2026-07-15 00:10:01 +02:00
parent 4ceb5bb567
commit c6da5025ce
11 changed files with 524 additions and 10 deletions

View file

@ -26,12 +26,27 @@ class FriendlyError {
required this.detail,
this.hint,
});
// The on-disk error log serialises thrown objects via toString
// keep the whole story (headline, hint, verbatim detail) in one
// readable record.
@override
String toString() => [
headline,
?hint,
if (detail.isNotEmpty && detail != headline) detail,
].join('\n');
}
/// Map an arbitrary thrown object to a [FriendlyError]. Always
/// returns a value never throws so callers can drop the
/// result straight into UI without try/catch ceremony.
FriendlyError friendlyError(Object error, AppLocalizations l) {
// A pre-built FriendlyError passes through unchanged call
// sites that already know the precise story (e.g. the setup
// wizard's CLI-version-skew case) construct one directly and
// still route through the shared presentation helpers.
if (error is FriendlyError) return error;
// We deliberately don't import package:grpc here so Studio
// doesn't have to add it to its own pubspec — the dependency
// lives one layer down in chain_client_sdk. `GrpcError` has a

View file

@ -18,6 +18,7 @@
import 'dart:io';
import 'package:meta/meta.dart';
import 'package:shared_preferences/shared_preferences.dart';
/// Sentinel returned in `_runFai(...).stderr` when no `fai`
@ -73,6 +74,28 @@ class SystemActions {
/// binary / read the install guide" recovery path.
static bool chainBinaryExists() => _faiExecutable() != null;
/// Test seam: when set, [_runFai] returns this function's result
/// instead of spawning a real process, and [resolvedChainBinary] /
/// [chainBinaryExists] answer from [debugResolveOverride]. Lets
/// widget tests drive the CLI error paths deterministically.
@visibleForTesting
static Future<({bool ok, String stdout, String stderr})> Function(
List<String> args,
)?
debugRunFaiOverride;
/// Test seam companion to [debugRunFaiOverride]: overrides binary
/// resolution (may return null to simulate "no binary found").
@visibleForTesting
static String? Function()? debugResolveOverride;
/// Absolute path of the `chain` binary Studio would execute right
/// now, or null when none can be located. Public so surfaces that
/// are about to spawn the binary (the guided-setup wizard) can say
/// WHAT they will run before macOS asks the operator for folder
/// permission because of where that binary happens to live.
static String? resolvedChainBinary() => _faiExecutable();
/// Ask the OS to open [path] in the default handler. On macOS
/// this opens text files in TextEdit, configs in the registered
/// editor, etc. Returns true on a clean spawn (process exited
@ -219,6 +242,8 @@ class SystemActions {
static Future<({bool ok, String stdout, String stderr})> _runFai(
List<String> args,
) async {
final runOverride = debugRunFaiOverride;
if (runOverride != null) return runOverride(args);
final exe = _faiExecutable();
if (exe == null) {
// Sentinel, not a user-facing string. Callers detect this
@ -249,6 +274,8 @@ class SystemActions {
/// the file picker), $CHAIN_BIN, PATH, fallback to the canonical
/// install location under the user's home dir.
static String? _faiExecutable() {
final resolveOverride = debugResolveOverride;
if (resolveOverride != null) return resolveOverride();
final override = _faiBinaryOverride;
if (override != null &&
override.isNotEmpty &&