chore(studio): RadioGroup migration + second integration test

Two clean-ups:

- `fai_system_ai_editor.dart` uses the new `RadioGroup<T>`
  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 <sf@flemming.it>
Signed-off-by: flemming-it <stefan.a.flemming@googlemail.com>
This commit is contained in:
flemming-it 2026-05-25 14:05:43 +02:00
parent 69b54629d3
commit 21c2a0f411
2 changed files with 94 additions and 45 deletions

View file

@ -553,53 +553,60 @@ class _PrivacyModeChips extends StatelessWidget {
l.systemAiPrivacyFullDesc, l.systemAiPrivacyFullDesc,
), ),
]; ];
return Column( // Material 3 deprecated per-Radio `groupValue` / `onChanged`
crossAxisAlignment: CrossAxisAlignment.start, // after Flutter 3.32. The modern pattern wraps the radios in
children: [ // a RadioGroup<T> ancestor that owns the selection + change
Text( // callback; the individual Radio widgets only declare their
l.systemAiPrivacyHeader, // value. Same UX, no deprecation warnings.
style: theme.textTheme.labelSmall?.copyWith( return RadioGroup<String>(
color: theme.colorScheme.onSurfaceVariant, groupValue: value,
letterSpacing: 0.6, onChanged: (v) {
fontSize: 10, if (v != null) onChanged(v);
), },
), child: Column(
const SizedBox(height: 4), crossAxisAlignment: CrossAxisAlignment.start,
for (final (wire, label, desc) in modes) children: [
InkWell( Text(
onTap: () => onChanged(wire), l.systemAiPrivacyHeader,
borderRadius: BorderRadius.circular(FaiRadius.sm), style: theme.textTheme.labelSmall?.copyWith(
child: Padding( color: theme.colorScheme.onSurfaceVariant,
padding: const EdgeInsets.symmetric( letterSpacing: 0.6,
vertical: FaiSpace.sm, fontSize: 10,
horizontal: FaiSpace.sm,
),
child: Row(
children: [
Radio<String>(
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,
),
),
],
),
),
],
),
), ),
), ),
], 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<String>(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,
),
),
],
),
),
],
),
),
),
],
),
); );
} }
} }

View file

@ -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();
}
});
}