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() { testWidgets( 'Client App opens Operations & Domain Agents panel and verifies agents, operations history, and command triggering', (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)); // Verify panel header and sections expect(find.text('Operations & Domain Agents'), findsOneWidget); expect(find.text('Domain Agents'), findsOneWidget); expect(find.text('Operations Execution History'), findsOneWidget); expect(find.text('System Gated Operations'), findsOneWidget); // Verify domain agents summary listed expect(find.text('DEPLOYER'), findsOneWidget); expect(find.text('BUILD-DEPLOY'), findsOneWidget); expect( find.text('READY'), findsNWidgets(2), ); // deployer and build-deploy are both ready // [REVIEW_API-2] Verify domain agents active command summary for Edge Alpha (presence) expect(find.text('Active Command: cmd-alpha-active'), findsOneWidget); // [REVIEW_API-2] Verify beta active command summary is NOT showing for Edge Alpha expect(find.text('Active Command: cmd-beta-active'), findsNothing); expect(find.text('build-deploy.deploy'), findsNothing); expect(find.textContaining('Deploy queued for edge beta'), findsNothing); // Verify operations history item expect(find.text('deployer.sync'), findsOneWidget); expect(find.text('SUCCESS'), findsOneWidget); expect(find.textContaining('Synced 5 models'), findsOneWidget); // Verify Trigger Sync button is not present expect(find.text('Trigger Sync'), findsNothing); // Verify System Gated buttons are present expect(find.text('Health Check'), findsOneWidget); expect(find.text('Agent Status'), findsOneWidget); expect(find.text('Agent Command'), findsOneWidget); // Verify Health Check button works and shows success banner await tester.tap(find.text('Health Check')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); // Check for success status banner message expect(find.textContaining('Success: Command accepted'), findsOneWidget); // Verify command target edge ID was edge-a expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-a')); // Setup delayed fetch for edge-b fakeStatusRepo.edgeBOperationsCompleter = Completer(); // Switch to Edge Beta via Dropdown await tester.tap(find.byType(DropdownButton)); await tester.pumpAndSettle(); await tester.tap(find.text('Edge Beta').last); // Pump one frame to trigger edge selection but wait before operations fetch resolves await tester.pump(); // [REVIEW_API-1] Verify that before the operations fetch completes: // 1. The domain agents section has updated immediately (so cmd-alpha-active is gone, cmd-beta-active is present) expect(find.text('Active Command: cmd-alpha-active'), findsNothing); expect(find.text('Active Command: cmd-beta-active'), findsOneWidget); // 2. The operations history of the previous edge (edge-a) is cleared immediately upon fetch start. expect(find.text('deployer.sync'), findsNothing); expect(find.textContaining('Synced 5 models'), findsNothing); // 3. The loading indicator is shown since _operations is null and _isLoading is true expect(find.byType(CircularProgressIndicator), findsOneWidget); // Now complete the operations fetch for edge-b 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(); // Verify domain agents for Edge Beta (deployer shows BUSY, build-deploy shows READY) expect(find.text('BUSY'), findsOneWidget); expect(find.text('READY'), findsOneWidget); // [REVIEW_API-2] Verify presence of beta active command and operations history expect(find.text('Active Command: cmd-beta-active'), findsOneWidget); expect(find.text('build-deploy.deploy'), findsOneWidget); expect( find.textContaining('Deploy queued for edge beta'), findsOneWidget, ); // [REVIEW_API-2] Verify absence of alpha-only active command and operations history expect(find.text('Active Command: cmd-alpha-active'), findsNothing); expect(find.text('deployer.sync'), findsNothing); expect(find.textContaining('Synced 5 models'), findsNothing); // Verify command triggering on Edge Beta sends command to edge-b await tester.tap(find.text('Health Check')); await tester.pumpAndSettle(); expect(fakeStatusRepo.lastCommandEdgeId, equals('edge-b')); }, ); testWidgets( 'Client App handles unsupported or error command responses and shows error banner', (WidgetTester tester) async { final fakeClient = FakeClientWireClient(shouldSuccess: true); final fakeStatusRepo = FakeControlPlaneStatusRepository(); // Configure repository to simulate a failed/unsupported command response 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)); // Verify Agent Status button works, opens dialog, and then shows error banner await tester.tap(find.text('Agent Status')); await tester.pump(); await tester.pump(const Duration(milliseconds: 100)); expect(find.text('Get Agent 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)); // Check for failure status banner message expect( find.textContaining( 'Failed: unsupported - Operation not supported by Edge', ), findsOneWidget, ); }, ); testWidgets( 'Client App gates agent.status and agent.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)); // 1. Verify agent.status gating await tester.tap(find.text('Agent Status')); await tester.pump(); expect(find.text('Get Agent Status'), findsOneWidget); // Try to send with empty target selector await tester.enterText(find.byType(TextField), ''); await tester.tap(find.text('Send')); await tester.pump(); // The dialog should still be open (Send was gated/no-op) expect(find.text('Get Agent Status'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); // Cancel dialog await tester.tap(find.text('Cancel')); await tester.pump(); // 2. Verify agent.command gating await tester.tap(find.text('Agent Command')); await tester.pump(); expect(find.text('Send Agent Command'), findsOneWidget); // Try to send with empty command name and empty selector await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Agent Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); // Enter only selector await tester.enterText(find.byType(TextField).at(0), 'node-1'); await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Agent Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); // Enter only command name (clear selector) await tester.enterText(find.byType(TextField).at(0), ''); await tester.enterText(find.byType(TextField).at(1), 'sync'); await tester.tap(find.text('Send')); await tester.pump(); expect(find.text('Send Agent Command'), findsOneWidget); expect(fakeStatusRepo.lastCommandOperation, isNull); // Cancel await tester.tap(find.text('Cancel')); await tester.pump(); }, ); }