From 21c2a0f411c127a40e64cd134b535ae3f4ca0c45 Mon Sep 17 00:00:00 2001 From: flemming-it Date: Mon, 25 May 2026 14:05:43 +0200 Subject: [PATCH] chore(studio): RadioGroup migration + second integration test Two clean-ups: - `fai_system_ai_editor.dart` uses the new `RadioGroup` ancestor pattern Material 3 introduced after Flutter 3.32. Same UX (three radios for off/redacted/full), no deprecation warnings, `flutter analyze` is now zero-issue for the whole project. - New `install_install_planned_test.dart` asserts that installing a `planned` seed entry surfaces a clear, human-readable error class (no panic, no silent success). Locks in the failed-precondition path Studio's friendly-error mapper depends on. Integration test suite is now 3 scenarios: - capabilities_test.dart: system.approval+kind tag - install_install_planned_test.dart: planned-install error All run against a fresh `fai serve` subprocess via HubFixture, ~2 s total. Signed-off-by: flemming-it Signed-off-by: flemming-it --- lib/widgets/fai_system_ai_editor.dart | 97 ++++++++++--------- .../install_install_planned_test.dart | 42 ++++++++ 2 files changed, 94 insertions(+), 45 deletions(-) create mode 100644 test/integration/install_install_planned_test.dart diff --git a/lib/widgets/fai_system_ai_editor.dart b/lib/widgets/fai_system_ai_editor.dart index 39c262e..ce260e5 100644 --- a/lib/widgets/fai_system_ai_editor.dart +++ b/lib/widgets/fai_system_ai_editor.dart @@ -553,53 +553,60 @@ class _PrivacyModeChips extends StatelessWidget { l.systemAiPrivacyFullDesc, ), ]; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - l.systemAiPrivacyHeader, - style: theme.textTheme.labelSmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - letterSpacing: 0.6, - fontSize: 10, - ), - ), - const SizedBox(height: 4), - for (final (wire, label, desc) in modes) - InkWell( - onTap: () => onChanged(wire), - borderRadius: BorderRadius.circular(FaiRadius.sm), - child: Padding( - padding: const EdgeInsets.symmetric( - vertical: FaiSpace.sm, - horizontal: FaiSpace.sm, - ), - child: Row( - children: [ - Radio( - value: wire, - groupValue: value, - onChanged: (v) => onChanged(v ?? wire), - ), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text(label, style: theme.textTheme.bodyMedium), - Text( - desc, - style: theme.textTheme.bodySmall?.copyWith( - color: theme.colorScheme.onSurfaceVariant, - ), - ), - ], - ), - ), - ], - ), + // Material 3 deprecated per-Radio `groupValue` / `onChanged` + // after Flutter 3.32. The modern pattern wraps the radios in + // a RadioGroup ancestor that owns the selection + change + // callback; the individual Radio widgets only declare their + // value. Same UX, no deprecation warnings. + return RadioGroup( + groupValue: value, + onChanged: (v) { + if (v != null) onChanged(v); + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + l.systemAiPrivacyHeader, + style: theme.textTheme.labelSmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + letterSpacing: 0.6, + fontSize: 10, ), ), - ], + const SizedBox(height: 4), + for (final (wire, label, desc) in modes) + InkWell( + onTap: () => onChanged(wire), + borderRadius: BorderRadius.circular(FaiRadius.sm), + child: Padding( + padding: const EdgeInsets.symmetric( + vertical: FaiSpace.sm, + horizontal: FaiSpace.sm, + ), + child: Row( + children: [ + Radio(value: wire), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(label, style: theme.textTheme.bodyMedium), + Text( + desc, + style: theme.textTheme.bodySmall?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], + ), + ), + ], + ), + ), + ), + ], + ), ); } } diff --git a/test/integration/install_install_planned_test.dart b/test/integration/install_install_planned_test.dart new file mode 100644 index 0000000..cf20e7c --- /dev/null +++ b/test/integration/install_install_planned_test.dart @@ -0,0 +1,42 @@ +// Integration test: install_module against a `planned` seed +// entry must fail with a clear, human-readable error class — +// not a panic, not a silent success. +// +// Why this matters: planned modules are listed in the Store +// but have no wasm_url. The old behaviour was a server-side +// panic on resolve_install_source(); the new contract emits a +// FAILED_PRECONDITION with "no installable version" so +// Studio's friendly-error mapper can show a usable message. + +import 'package:flutter_test/flutter_test.dart'; + +import 'hub_fixture.dart'; + +void main() { + test('install_module on a planned entry surfaces a clear error', + () async { + final fixture = await HubFixture.start(); + if (fixture == null) return; + try { + // web.scrape is `planned` in the seed and has no + // wasm_url. The hub resolves the name through the store + // index, finds the entry, then fails at the + // "no installable version" check. + try { + await fixture.client.installModule(source: 'web.scrape'); + fail('install_module should not succeed for a planned entry'); + } catch (e) { + final msg = e.toString(); + expect( + msg.contains('no installable version') || + msg.contains('not yet published'), + isTrue, + reason: + 'Expected a clear planned-status error, got: $msg', + ); + } + } finally { + await fixture.dispose(); + } + }); +}