// 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. import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:chain_studio/l10n/app_localizations.dart'; import 'package:chain_studio/widgets/guided_setup_dialog.dart'; Widget _host() => MaterialApp( locale: const Locale('de'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Builder( builder: (context) => Scaffold( body: Center( child: ElevatedButton( onPressed: () => GuidedSetupDialog.show(context), child: const Text('open'), ), ), ), ), ); /// Host that opens the dialog pre-seeded on the review / applied /// step with [plan] (the parsed `--plan-json` shape), bypassing the /// CLI subprocess. Widget _seededHost(Map plan, {bool applied = false}) => MaterialApp( locale: const Locale('de'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Builder( builder: (context) => Scaffold( body: Center( child: ElevatedButton( onPressed: () => showDialog( context: context, builder: (_) => GuidedSetupDialog(debugPlan: plan, debugApplied: applied), ), child: const Text('open'), ), ), ), ), ); Map _regulatedPlan({bool signed = true}) => { 'profile': 'enterprise', 'modules': ['text.extract', 'text.summarize'], 'starter_flow': 'extract-summarize', 'runbook': 'service', 'curated_docs': [], 'require_signatures': signed, 'worm_audit': false, 'approval_step': true, }; void main() { testWidgets('step 1 shows scenario options in German with explanations', ( tester, ) async { await tester.pumpWidget(_host()); await tester.tap(find.text('open')); await tester.pumpAndSettle(); // Step counter + localized (German) scenario labels, not the // English enum humanization. expect(find.text('Schritt 1 von 3'), findsOneWidget); expect(find.text('Regulierter Produktivbetrieb'), findsOneWidget); expect(find.text('Erst mal ausprobieren'), findsOneWidget); // Each option carries its one-line explanation. expect( find.textContaining('menschliche Freigabe vor jedem KI-Schritt'), findsOneWidget, ); // No leaked English enum labels. expect(find.text('Regulated Production'), findsNothing); expect(find.text('Trying Out'), findsNothing); }); testWidgets('Weiter/Zurück walk the three answer steps', (tester) async { await tester.pumpWidget(_host()); await tester.tap(find.text('open')); await tester.pumpAndSettle(); // Step 1 → 2 (task). await tester.tap(find.text('Weiter')); await tester.pumpAndSettle(); expect(find.text('Schritt 2 von 3'), findsOneWidget); expect(find.text('Text aus Dokumenten extrahieren'), findsOneWidget); // Step 2 → 3 (environment) shows the two plain-language toggles. await tester.tap(find.text('Weiter')); await tester.pumpAndSettle(); expect(find.text('Schritt 3 von 3'), findsOneWidget); expect(find.text('Dieser Rechner'), findsOneWidget); expect( find.text('Vor jedem KI-Schritt einen Menschen um Freigabe bitten'), findsOneWidget, ); // Zurück returns to step 2. await tester.tap(find.text('Zurück')); await tester.pumpAndSettle(); expect(find.text('Schritt 2 von 3'), findsOneWidget); }); testWidgets( 'review of a regulated plan explains the signature dead end and offers the relaxation switch', (tester) async { await tester.pumpWidget(_seededHost(_regulatedPlan())); await tester.tap(find.text('open')); await tester.pumpAndSettle(); // The plain-language notice + the deliberate one-click switch. expect( find.textContaining('Der öffentliche Store liefert zurzeit unsignierte Pakete'), findsOneWidget, ); expect( find.text('Installation aus dem öffentlichen Store erlauben'), findsOneWidget, ); // No raw CLI prose anywhere. expect(find.textContaining('chain install'), findsNothing); }, ); testWidgets( 'applied state renders clickable next steps — start hub, per-module install, open flow', (tester) async { await tester.pumpWidget( _seededHost(_regulatedPlan(signed: false), applied: true), ); await tester.tap(find.text('open')); await tester.pumpAndSettle(); expect(find.text('Fertig — Ch∆In ist eingerichtet.'), findsOneWidget); // Hub not probed up in tests → the start action + the // install-needs-hub hint show, and install buttons exist // (disabled) for each plan module. expect(find.text('Hub starten'), findsOneWidget); expect( find.textContaining('Starten Sie zuerst den Hub'), findsOneWidget, ); expect(find.text('text.extract installieren'), findsOneWidget); expect(find.text('text.summarize installieren'), findsOneWidget); expect( find.text('Beispiel-Flow „extract-summarize“ öffnen'), findsOneWidget, ); // No CLI text in the GUI path. expect(find.textContaining('chain serve'), findsNothing); expect(find.textContaining('chain install'), findsNothing); }, ); testWidgets( 'applied regulated (signed) state guides to the signed source instead of dead-end install buttons', (tester) async { await tester.pumpWidget(_seededHost(_regulatedPlan(), applied: true)); await tester.tap(find.text('open')); await tester.pumpAndSettle(); expect( find.textContaining('signierten Quelle'), findsOneWidget, ); expect(find.text('text.extract installieren'), findsNothing); }, ); testWidgets( 'step 1 offers the free-text path with an honest no-AI fallback hint', (tester) async { await tester.pumpWidget( MaterialApp( locale: const Locale('de'), localizationsDelegates: AppLocalizations.localizationsDelegates, supportedLocales: AppLocalizations.supportedLocales, home: Builder( builder: (context) => Scaffold( body: Center( child: ElevatedButton( onPressed: () => showDialog( context: context, builder: (_) => const GuidedSetupDialog(debugSkipAiProbe: true), ), child: const Text('open'), ), ), ), ), ), ); await tester.tap(find.text('open')); await tester.pumpAndSettle(); expect( find.text('Oder beschreiben Sie einfach, was Sie vorhaben'), findsOneWidget, ); // No system AI configured → honest fallback: the menu always // works; no dead end, no disabled mystery button. expect( find.textContaining('Die Auswahl oben funktioniert immer'), findsOneWidget, ); expect(find.text('Vorschlagen lassen'), findsNothing); }, ); group('parseSetupSuggestion', () { test('accepts a valid JSON object embedded in prose', () { final r = parseSetupSuggestion( 'Sure! Here is the mapping:\n' '{"scenario":"regulated-production","intent":"classify-documents",' '"target":"home-server","require_approval":true,' '"data_must_stay_local":false}\n' 'Let me know if you need anything else.', ); expect(r, isNotNull); expect(r!['scenario'], 'regulated-production'); expect(r['intent'], 'classify-documents'); expect(r['target'], 'home-server'); expect(r['require_approval'], true); expect(r['data_must_stay_local'], false); }); test('rejects hallucinated enum values', () { expect( parseSetupSuggestion( '{"scenario":"world-domination","intent":"hello-world",' '"target":"this-laptop"}', ), isNull, ); }); test('rejects non-JSON replies', () { expect(parseSetupSuggestion('I cannot help with that.'), isNull); expect(parseSetupSuggestion('{broken json'), isNull); }); test('missing booleans default to false', () { final r = parseSetupSuggestion( '{"scenario":"trying-out","intent":"hello-world",' '"target":"this-laptop"}', ); expect(r, isNotNull); expect(r!['require_approval'], false); expect(r['data_must_stay_local'], false); }); }); }