import 'dart:async'; import 'package:flutter/material.dart'; import 'package:flutter_test/flutter_test.dart'; import 'package:iop_client/control_plane_status_client.dart'; import 'package:iop_client/main.dart'; import 'support/client_test_harness.dart'; void main() { _registerPanelRenderingScenario(); _registerCommandErrorScenario(); _registerCommandGatingScenario(); _registerProviderCommandAllowlistScenario(); _registerOllamaApiPathScenario(); _registerPendingEmptyHistoryScenario(); _registerOperationsFetchStateScenario(); } void _registerPanelRenderingScenario() { testWidgets( 'Client App opens Operations panel and verifies history and provider commands', (WidgetTester tester) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); _verifyPanelHeaderAndSections(tester); _verifyOperationsHistoryItem(tester); _verifyTriggerSyncAbsent(tester); _verifySystemGatedButtons(tester); await _verifyHealthCheckSuccess(tester, fakeStatusRepo); fakeStatusRepo.edgeBOperationsCompleter = Completer(); await tester.tap(find.byType(DropdownButton)); await tester.pumpAndSettle(); await tester.tap(find.text('Edge Beta').last); await tester.pump(); _verifyEdgeSwitchNotLoaded(tester); fakeStatusRepo.edgeBOperationsCompleter!.complete( EdgeOperationsResponseView( edgeId: 'edge-b', operations: [ EdgeCommandRecordView( edgeId: 'edge-b', commandId: 'cmd-beta-active', operation: 'build-deploy.deploy', targetSelector: '', status: 'success', summary: 'Deploy queued for edge beta', error: '', ), ], ), ); await tester.pumpAndSettle(); _verifyEdgeBetaActiveCommand(tester); _verifyEdgeBetaOperationsHistory(tester); _verifyEdgeBetaOperationsAbsence(tester); await tester.tap(find.text('Health Check')); await tester.pumpAndSettle(); expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-b')); }, ); } void _registerCommandErrorScenario() { testWidgets( 'Client App handles unsupported or error command responses and shows error banner', (WidgetTester tester) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); fakeStatusRepo.nextCommandStatus = 'unsupported'; fakeStatusRepo.nextCommandError = 'Operation not supported by Edge'; fakeStatusRepo.nextCommandSummary = 'Error summary'; await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.text('Node Status')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('Get Node Status'), findsOneWidget); await tester.enterText(find.byType(TextField), 'test-node'); await tester.tap(find.text('Send')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect( find.textContaining( 'Failed: unsupported - Operation not supported by Edge', ), findsOneWidget, ); }, ); } void _registerCommandGatingScenario() { testWidgets( 'Client App gates node.status and provider.command without required inputs', (WidgetTester tester) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await _verifyNodeStatusGating(tester, fakeStatusRepo); await _verifyProviderCommandGating(tester, fakeStatusRepo); }, ); } void _registerPendingEmptyHistoryScenario() { testWidgets( 'RuntimePanel keeps loaded empty history visible while a command is pending', (WidgetTester tester) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); fakeStatusRepo.emptyOperationsEdgeId = 'edge-a'; await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('No operation executions recorded.'), findsOneWidget); fakeStatusRepo.commandResponseCompleter = Completer(); await tester.tap(find.text('Health Check')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('No operation executions recorded.'), findsOneWidget); expect(find.byType(CircularProgressIndicator), findsNothing); expect(fakeStatusRepo.lastCommandOperation, equals('health.check')); expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-a')); fakeStatusRepo.commandResponseCompleter!.complete( EdgeCommandResponseView( requestId: 'req-111', commandId: 'cmd-pending-test', edgeId: 'edge-a', status: 'accepted', summary: 'Command accepted by control plane', error: '', ), ); await tester.pumpAndSettle(); }, ); } void _registerOperationsFetchStateScenario() { testWidgets('RuntimePanel renders operations empty and fetch error states', ( WidgetTester tester, ) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); fakeStatusRepo.emptyOperationsEdgeId = 'edge-a'; fakeStatusRepo.fetchOperationsError = null; await _reFetchOperations(tester); expect(find.text('No operation executions recorded.'), findsOneWidget); fakeStatusRepo.fetchOperationsError = Exception('repository failure'); fakeStatusRepo.emptyOperationsEdgeId = null; await _reFetchOperations(tester); expect(find.textContaining('Error:'), findsOneWidget); expect(find.textContaining('repository failure'), findsOneWidget); }); } void _verifyPanelHeaderAndSections(WidgetTester tester) { expect(find.text('Operations'), findsOneWidget); expect(find.text('Operations Execution History'), findsOneWidget); expect(find.text('System Gated Operations'), findsOneWidget); } void _verifyOperationsHistoryItem(WidgetTester tester) { expect(find.text('deployer.sync'), findsOneWidget); expect(find.text('SUCCESS'), findsOneWidget); expect(find.textContaining('Synced 5 models'), findsOneWidget); } void _verifyTriggerSyncAbsent(WidgetTester tester) { expect(find.text('Trigger Sync'), findsNothing); } void _verifySystemGatedButtons(WidgetTester tester) { expect(find.text('Health Check'), findsOneWidget); expect(find.text('Node Status'), findsOneWidget); expect(find.text('Provider Command'), findsOneWidget); } Future _verifyHealthCheckSuccess( WidgetTester tester, FakeControlPlaneStatusRepository fakeStatusRepo, ) async { await tester.tap(find.text('Health Check')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.textContaining('Success: Command accepted'), findsOneWidget); expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-a')); } void _verifyEdgeSwitchNotLoaded(WidgetTester tester) { expect(find.text('deployer.sync'), findsNothing); expect(find.textContaining('Synced 5 models'), findsNothing); expect(find.byType(CircularProgressIndicator), findsOneWidget); } void _verifyEdgeBetaActiveCommand(WidgetTester tester) { expect(find.text('build-deploy.deploy'), findsOneWidget); expect(find.textContaining('Deploy queued for edge beta'), findsOneWidget); } void _verifyEdgeBetaOperationsHistory(WidgetTester tester) { expect(find.text('build-deploy.deploy'), findsOneWidget); } void _verifyEdgeBetaOperationsAbsence(WidgetTester tester) { expect(find.text('deployer.sync'), findsNothing); expect(find.textContaining('Synced 5 models'), findsNothing); } Future _verifyNodeStatusGating( WidgetTester tester, FakeControlPlaneStatusRepository fakeStatusRepo, ) async { await tester.tap(find.text('Node Status')); await tester.pump(); expect(find.text('Get Node Status'), findsOneWidget); await tester.enterText(find.byType(TextField), ''); await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Get Node Status'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); await tester.tap(find.text('Cancel')); await tester.pump(); } Future _verifyProviderCommandGating( WidgetTester tester, FakeControlPlaneStatusRepository fakeStatusRepo, ) async { await tester.tap(find.text('Provider Command')); await tester.pump(); expect(find.text('Send Provider Command'), findsOneWidget); // The dialog exposes only the selector as a free-form field; the command is // constrained to an allowlisted dropdown, so no arbitrary command can be typed. expect(find.byType(TextField), findsOneWidget); expect(find.byType(DropdownButtonFormField), findsOneWidget); // No selector and no command selected: Send is inert. await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Provider Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); // Selector present but still no command selected: Send is inert. await tester.enterText(find.byType(TextField), 'node-1'); await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Provider Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); await tester.tap(find.text('Cancel')); await tester.pump(); } void _registerProviderCommandAllowlistScenario() { testWidgets('Client App sends only allowlisted provider commands', ( WidgetTester tester, ) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); for (final command in const ['capabilities', 'transport_status']) { await _sendAllowlistedCommand(tester, command); expect(fakeStatusRepo.lastCommandOperation, equals('provider.command')); expect(fakeStatusRepo.lastCommandTargetSelector, equals('node-1')); expect( fakeStatusRepo.lastCommandParameters, equals({'command': command}), ); } // Confirm the dropdown only offers the three allowlisted commands. await tester.tap(find.text('Provider Command')); await tester.pump(); await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); expect(find.text('capabilities'), findsWidgets); expect(find.text('transport_status'), findsWidgets); expect(find.text('ollama_api'), findsWidgets); expect(find.text('node.status'), findsNothing); expect(find.text('provider.command'), findsNothing); await tester.tap(find.text('capabilities').last); await tester.pumpAndSettle(); await tester.tap(find.text('Cancel')); await tester.pump(); }); } void _registerOllamaApiPathScenario() { testWidgets('Client App requires Ollama API path', ( WidgetTester tester, ) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); await tester.pumpWidget( IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo), ); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.byTooltip('Runtime')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); await tester.tap(find.text('Provider Command')); await tester.pump(); await tester.tap(find.byType(DropdownButtonFormField).first); await tester.pumpAndSettle(); await tester.tap(find.text('ollama_api').last); await tester.pumpAndSettle(); // With ollama_api selected the method dropdown and path field appear. expect(find.byType(TextField), findsNWidgets(2)); await tester.enterText(find.byType(TextField).at(0), 'node-1'); // Empty path: Send stays gated and never reaches the repository. await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Provider Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); await tester.enterText(find.byType(TextField).at(1), '/api/tags'); await tester.tap(find.text('Send')); await tester.pumpAndSettle(); expect(fakeStatusRepo.lastCommandOperation, equals('provider.command')); expect(fakeStatusRepo.lastCommandTargetSelector, equals('node-1')); expect( fakeStatusRepo.lastCommandParameters, equals({'command': 'ollama_api', 'method': 'GET', 'path': '/api/tags'}), ); }); } Future _sendAllowlistedCommand( WidgetTester tester, String command, ) async { await tester.tap(find.text('Provider Command')); await tester.pump(); expect(find.text('Send Provider Command'), findsOneWidget); await tester.enterText(find.byType(TextField), 'node-1'); await tester.tap(find.byType(DropdownButtonFormField)); await tester.pumpAndSettle(); await tester.tap(find.text(command).last); await tester.pumpAndSettle(); await tester.tap(find.text('Send')); await tester.pumpAndSettle(); } Future _reFetchOperations(WidgetTester tester) async { await tester.tap(find.byIcon(Icons.refresh)); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); }