iop/apps/client/test/widget_test.dart

1429 lines
50 KiB
Dart

import 'dart:async';
import 'dart:io';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';
import 'package:iop_client/main.dart';
import 'package:iop_client/client_config.dart';
import 'package:iop_client/iop_wire/client_wire_client.dart';
import 'package:iop_client/gen/proto/iop/control.pb.dart';
import 'package:iop_client/control_plane_status_client.dart';
import 'package:iop_client/widgets/nodes_panel.dart';
import 'package:protobuf/protobuf.dart';
import 'package:fixnum/fixnum.dart';
class FakeWebSocket implements WebSocket {
final _controller = StreamController<dynamic>();
@override
StreamSubscription<dynamic> listen(
void Function(dynamic event)? onData, {
Function? onError,
void Function()? onDone,
bool? cancelOnError,
}) {
return _controller.stream.listen(
onData,
onError: onError,
onDone: onDone,
cancelOnError: cancelOnError,
);
}
@override
dynamic noSuchMethod(Invocation invocation) {
return null;
}
}
class FakeClientWireClient extends ClientWireClient {
final bool shouldSuccess;
final String mockMessage;
FakeClientWireClient({
this.shouldSuccess = true,
this.mockMessage = 'Welcome to Toki CP!',
}) : super(FakeWebSocket(), heartbeatIntervalTime: 0);
@override
bool get isAlive => true;
@override
Future<Res> sendRequest<
Req extends GeneratedMessage,
Res extends GeneratedMessage
>(Req data, {Duration timeout = const Duration(seconds: 30)}) async {
if (data is ClientHelloRequest) {
final response = ClientHelloResponse()
..ready = shouldSuccess
..protocol = 'iop-wire-v1'
..serverTimeUnixNano = Int64(1716584400)
..message = mockMessage;
return response as Res;
}
throw UnimplementedError();
}
}
class FakeControlPlaneStatusRepository implements ControlPlaneStatusRepository {
String nextCommandStatus = 'accepted';
String nextCommandError = '';
String nextCommandSummary = 'Command accepted by control plane';
String? lastCommandOperation;
String? lastCommandTargetSelector;
Map<String, String>? lastCommandParameters;
String? lastCommandEdgeId;
Completer<EdgeOperationsResponseView>? edgeBOperationsCompleter;
List<EdgeRegistryView> mockEdges = [
EdgeRegistryView(
edgeId: 'edge-a',
edgeName: 'Edge Alpha',
version: '1.2.0',
capabilities: ['Serving', 'Automation'],
protocol: 'iop-wire-v1',
connected: true,
lastSeen: DateTime.now(),
),
EdgeRegistryView(
edgeId: 'edge-b',
edgeName: 'Edge Beta',
version: '1.2.1',
capabilities: ['Serving'],
protocol: 'iop-wire-v1',
connected: false,
lastSeen: DateTime.now().subtract(const Duration(minutes: 5)),
),
];
@override
Future<List<EdgeRegistryView>> fetchEdges() async {
return mockEdges;
}
@override
Future<EdgeStatusResponseView> fetchEdgeStatus(String edgeId) async {
return EdgeStatusResponseView(
requestId: 'req-123',
edgeId: edgeId,
edgeName: edgeId == 'edge-a' ? 'Edge Alpha' : 'Edge Beta',
observedTimeUnixNano: DateTime.now().microsecondsSinceEpoch * 1000,
nodes: [
EdgeNodeSnapshotView(
nodeId: 'node-1',
alias: 'Node One',
label: 'GPU-T4',
connected: true,
config: NodeConfigSummaryView(
adapters: [
AdapterSummaryView(type: 'ollama', enabled: true),
AdapterSummaryView(type: 'custom', enabled: false),
],
concurrency: 4,
),
providerSnapshots: [
ProviderSnapshotView(
id: 'provider-ollama',
adapter: 'ollama',
type: 'llm',
category: 'inference',
status: 'active',
health: 'healthy',
capacity: 10,
inFlight: 3,
queued: 1,
loadRatio: 0.3,
servedModels: ['llama-3.1', 'mistral'],
lifecycleCapabilities: ['start', 'stop', 'restart'],
),
ProviderSnapshotView(
id: 'provider-vllm',
adapter: 'vllm',
type: 'llm',
category: 'inference',
status: 'active',
health: 'healthy',
capacity: 8,
inFlight: 2,
queued: 0,
loadRatio: 0.25,
servedModels: ['vicuna'],
lifecycleCapabilities: ['start', 'stop'],
),
],
),
EdgeNodeSnapshotView(
nodeId: 'node-2',
alias: 'Node Two',
label: 'CPU-only',
connected: false,
config: NodeConfigSummaryView(
adapters: [AdapterSummaryView(type: 'python-cli', enabled: true)],
concurrency: 2,
),
providerSnapshots: [
ProviderSnapshotView(
id: 'provider-openai',
adapter: 'openai',
type: 'llm',
category: 'external',
status: 'active',
health: 'degraded',
capacity: 100,
inFlight: 50,
queued: 5,
loadRatio: 0.5,
servedModels: ['gpt-4', 'gpt-3.5-turbo'],
lifecycleCapabilities: [],
),
],
),
],
capabilities: [
EdgeCapabilitySummaryView(
kind: 'Serving',
available: true,
status: 'ready',
summary: 'Active',
),
EdgeCapabilitySummaryView(
kind: 'Automation',
available: true,
status: 'ready',
summary: 'Active',
),
],
domainAgents: edgeId == 'edge-a'
? [
EdgeDomainAgentSummaryView(
agentKind: 'deployer',
available: true,
lifecycleState: 'ready',
activeCommandId: 'cmd-alpha-active',
summary: 'Active',
),
EdgeDomainAgentSummaryView(
agentKind: 'build-deploy',
available: true,
lifecycleState: 'ready',
activeCommandId: '',
summary: 'Idle',
),
]
: [
EdgeDomainAgentSummaryView(
agentKind: 'deployer',
available: true,
lifecycleState: 'busy',
activeCommandId: 'cmd-beta-active',
summary: 'Processing',
),
EdgeDomainAgentSummaryView(
agentKind: 'build-deploy',
available: true,
lifecycleState: 'ready',
activeCommandId: '',
summary: 'Idle',
),
],
metadata: {'region': 'us-west'},
error: '',
);
}
@override
Future<List<EdgeNodeEventView>> fetchEdgeEvents(String edgeId) async {
return [
EdgeNodeEventView(
edgeId: edgeId,
eventId: 'evt-1',
type: 'online',
source: 'node-register',
nodeId: 'node-1',
alias: 'Node One',
reason: 'Node connected and registered successfully',
timestamp: DateTime.now().subtract(const Duration(minutes: 2)),
receivedAt: DateTime.now().subtract(const Duration(minutes: 2)),
metadata: {},
),
EdgeNodeEventView(
edgeId: edgeId,
eventId: 'evt-2',
type: 'warn',
source: 'runtime-dispatch',
nodeId: 'node-2',
alias: 'Node Two',
reason: 'Execution concurrency limit reached',
timestamp: DateTime.now().subtract(const Duration(minutes: 1)),
receivedAt: DateTime.now().subtract(const Duration(minutes: 1)),
metadata: {},
),
];
}
@override
Future<FleetStatusResponseView> fetchFleetStatus() async {
return FleetStatusResponseView(
generatedAt: DateTime.now(),
edges: mockEdges
.map(
(e) => FleetEdgeView(
edgeId: e.edgeId,
edgeName: e.edgeName,
version: e.version,
protocol: e.protocol,
connected: e.connected,
health: e.connected ? 'healthy' : 'unhealthy',
lastSeen: e.lastSeen,
nodeCount: 2,
capabilities: e.capabilities
.map(
(c) => EdgeCapabilitySummaryView(
kind: c,
available: true,
status: 'ready',
summary: '',
),
)
.toList(),
domainAgents: e.edgeId == 'edge-a'
? [
EdgeDomainAgentSummaryView(
agentKind: 'deployer',
available: true,
lifecycleState: 'ready',
activeCommandId: 'cmd-alpha-active',
summary: 'Active',
),
EdgeDomainAgentSummaryView(
agentKind: 'build-deploy',
available: true,
lifecycleState: 'ready',
activeCommandId: '',
summary: 'Idle',
),
]
: [
EdgeDomainAgentSummaryView(
agentKind: 'deployer',
available: true,
lifecycleState: 'busy',
activeCommandId: 'cmd-beta-active',
summary: 'Processing',
),
EdgeDomainAgentSummaryView(
agentKind: 'build-deploy',
available: true,
lifecycleState: 'ready',
activeCommandId: '',
summary: 'Idle',
),
],
error: '',
),
)
.toList(),
);
}
@override
Future<EdgeOperationsResponseView> fetchEdgeOperations(String edgeId) async {
if (edgeId == 'edge-a') {
return EdgeOperationsResponseView(
edgeId: edgeId,
operations: [
EdgeCommandRecordView(
edgeId: edgeId,
commandId: 'cmd-alpha-active',
operation: 'deployer.sync',
targetSelector: '',
status: 'success',
summary: 'Synced 5 models',
error: '',
),
],
);
} else {
if (edgeBOperationsCompleter != null) {
return edgeBOperationsCompleter!.future;
}
return EdgeOperationsResponseView(
edgeId: edgeId,
operations: [
EdgeCommandRecordView(
edgeId: edgeId,
commandId: 'cmd-beta-active',
operation: 'build-deploy.deploy',
targetSelector: '',
status: 'success',
summary: 'Deploy queued for edge beta',
error: '',
),
],
);
}
}
@override
Future<EdgeCommandResponseView> sendEdgeCommand(
String edgeId,
String operation, {
String? targetSelector,
Map<String, String>? parameters,
}) async {
lastCommandOperation = operation;
lastCommandTargetSelector = targetSelector;
lastCommandParameters = parameters;
lastCommandEdgeId = edgeId;
if (operation == 'agent.status' &&
(targetSelector == null || targetSelector.isEmpty)) {
return EdgeCommandResponseView(
requestId: 'req-111',
commandId: 'cmd-failed',
edgeId: edgeId,
status: 'error',
summary: 'Failed: agent.status requires target_selector',
error: 'target_selector is required',
);
}
if (operation == 'agent.command') {
final cmd = parameters?['command'];
if (targetSelector == null ||
targetSelector.isEmpty ||
cmd == null ||
cmd.isEmpty) {
return EdgeCommandResponseView(
requestId: 'req-111',
commandId: 'cmd-failed',
edgeId: edgeId,
status: 'error',
summary:
'Failed: agent.command requires target_selector and parameters.command',
error: 'target_selector and parameters.command are required',
);
}
}
return EdgeCommandResponseView(
requestId: 'req-111',
commandId: 'cmd-new',
edgeId: edgeId,
status: nextCommandStatus,
summary: nextCommandSummary,
error: nextCommandError,
);
}
}
void main() {
testWidgets('Client App basic rendering and success handshake test', (
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));
expect(find.text('IOP CONTROL PLANE'), findsOneWidget);
expect(find.text('Operations Overview'), findsOneWidget);
expect(
find.text(ClientConfig.controlPlaneHttpUrl, findRichText: true),
findsOneWidget,
);
expect(
find.text(ClientConfig.controlPlaneWireUrl, findRichText: true),
findsOneWidget,
);
expect(find.text('CONNECTED'), findsOneWidget);
});
testWidgets('Client App connection error state test', (
WidgetTester tester,
) async {
final fakeClient = FakeClientWireClient(
shouldSuccess: false,
mockMessage: 'Invalid Version',
);
final fakeStatusRepo = FakeControlPlaneStatusRepository();
await tester.pumpWidget(
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
expect(find.text('ERROR'), findsOneWidget);
});
testWidgets('Client App opens IOP agent panel from the left rail', (
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('Agent'));
await tester.pump();
expect(find.text('Ask about IOP operations'), findsOneWidget);
expect(find.textContaining('IOP agent surface is ready'), findsOneWidget);
expect(find.textContaining('Edge Control'), findsOneWidget);
expect(find.textContaining('Node Management'), findsOneWidget);
});
testWidgets('Client App opens Edges panel and displays Edge details', (
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('Edges'));
await tester.pump();
// Verify list of edges
expect(find.text('Edge Alpha'), findsNWidgets(2));
expect(find.text('Edge Beta'), findsOneWidget);
// Verify detail pane for the first auto-selected edge (Edge Alpha)
expect(find.text('Edge ID'), findsOneWidget);
expect(find.text('edge-a'), findsOneWidget);
expect(find.text('1.2.0'), findsOneWidget);
expect(find.text('Serving'), findsOneWidget);
expect(find.text('Automation'), findsOneWidget);
// Verify selecting another Edge switches the detail pane instead of
// reusing the first Edge's status view.
await tester.tap(find.text('Edge Beta'));
await tester.pump();
expect(find.text('Edge Alpha'), findsOneWidget);
expect(find.text('Edge Beta'), findsNWidgets(2));
expect(find.text('edge-b'), findsOneWidget);
expect(find.text('1.2.1'), findsOneWidget);
expect(find.text('DISCONNECTED'), findsOneWidget);
expect(find.text('unhealthy'), findsOneWidget);
});
testWidgets(
'Client App opens Nodes panel and displays active Nodes and configurations',
(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('Nodes'));
await tester.pumpAndSettle();
// Verify Nodes view header
expect(find.text('Nodes View'), findsOneWidget);
// Verify nodes listed
expect(find.text('Node One'), findsOneWidget);
expect(find.text('Label: GPU-T4'), findsOneWidget);
expect(find.text('Concurrency Limit: 4'), findsOneWidget);
expect(find.text('ollama'), findsOneWidget);
expect(find.text('custom'), findsOneWidget);
},
);
testWidgets(
'Client App Nodes panel displays Provider Catalog for nodes with snapshots',
(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('Nodes'));
await tester.pumpAndSettle();
// Scroll to reveal second node's provider catalog before assertions
await tester.drag(
find.byType(ListView),
const Offset(0.0, -1500.0),
);
await tester.pumpAndSettle();
// Verify Provider Catalog sections are present on both nodes
expect(find.textContaining('Provider Catalog'), findsNWidgets(2));
// Verify Provider Snapshot display for node-1 (ollama provider)
expect(find.text('provider-ollama'), findsOneWidget);
expect(find.textContaining('llm / inference'), findsNWidgets(2)); // both providers on node-1
expect(find.textContaining('HEALTHY'), findsNWidgets(2)); // both ollama and vllm are healthy
expect(find.textContaining('Load: 3/10'), findsOneWidget);
expect(find.textContaining('Q: 1'), findsOneWidget);
expect(find.textContaining('30.0%'), findsOneWidget);
expect(find.textContaining('Models: llama-3.1, mistral'), findsOneWidget);
// Verify Provider Snapshot display for node-2 (degraded state)
expect(find.text('provider-openai'), findsOneWidget);
expect(find.textContaining('Load: 50/100'), findsOneWidget);
expect(find.textContaining('Q: 5'), findsOneWidget);
expect(find.textContaining('50.0%'), findsOneWidget);
},
);
testWidgets(
'Client App opens Execution/Logs panel and displays lifecycle events',
(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('Execution & Logs'));
await tester.pump();
// Verify logs header
expect(find.text('Lifecycle Events & Logs'), findsOneWidget);
// Verify captured events
expect(find.text('ONLINE'), findsOneWidget);
expect(find.text('WARN'), findsOneWidget);
expect(
find.text('Node connected and registered successfully'),
findsOneWidget,
);
expect(find.text('Execution concurrency limit reached'), findsOneWidget);
},
);
testWidgets('Client App refresh behavior when selected edge disappears', (
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));
// 1. Initial State: edge-a is selected, EdgesPanel has Edge Alpha and Edge Beta
await tester.tap(find.byTooltip('Edges'));
await tester.pump();
expect(find.text('Edge Alpha'), findsNWidgets(2)); // list & details
// 2. Change mockEdges so 'edge-a' and 'edge-b' disappear, only 'edge-c' is returned.
fakeStatusRepo.mockEdges = [
EdgeRegistryView(
edgeId: 'edge-c',
edgeName: 'Edge Gamma',
version: '1.2.2',
capabilities: ['Automation'],
protocol: 'iop-wire-v1',
connected: true,
lastSeen: DateTime.now(),
),
];
// Trigger refresh in overview
await tester.tap(find.byTooltip('Overview'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// Verify route has navigated back to Overview successfully
expect(find.text('Operations Overview'), findsOneWidget);
// Drag the ListView upward to scroll down and mount the bottom refresh button
final listViewFinder = find.byType(ListView);
await tester.drag(listViewFinder.first, const Offset(0.0, -400.0));
await tester.pump();
await tester.pump(const Duration(milliseconds: 200));
// Tap the refresh button safely by its text label
await tester.tap(find.text('Refresh Connection'));
await tester.pump();
await tester.pump(const Duration(seconds: 1));
// 3. Switch to Edges panel, verify edge-a disappears, edge-c appears and auto-selected
await tester.tap(find.byTooltip('Edges'));
await tester.pump();
expect(find.text('Edge Alpha'), findsNothing);
expect(
find.text('Edge Gamma'),
findsNWidgets(2),
); // list & details (auto-selected)
// 4. Switch to Nodes panel, verify dropdown Value works without assert crash
await tester.tap(find.byTooltip('Nodes'));
await tester.pump();
expect(find.text('Nodes View'), findsOneWidget);
// dropdown button value should be edge-c now
expect(find.text('Edge Gamma'), findsOneWidget);
// 5. Switch to Logs panel, verify dropdown Value works without assert crash
await tester.tap(find.byTooltip('Execution & Logs'));
await tester.pump();
expect(find.text('Lifecycle Events & Logs'), findsOneWidget);
expect(find.text('Edge Gamma'), findsOneWidget);
});
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<EdgeOperationsResponseView>();
// Switch to Edge Beta via Dropdown
await tester.tap(find.byType(DropdownButton<String>));
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();
},
);
// [REVIEW_API-1] Regression: health=degraded, status=active -> yellow state text/color
testWidgets(
'Provider with health=degraded and status=active shows DEGRADED text with yellow color',
(WidgetTester tester) async {
final fakeClient = FakeClientWireClient(shouldSuccess: true);
final fakeStatusRepo = FakeControlPlaneStatusRepository();
// Override the OpenAI provider to have health=degraded, status=active
// This is already set in the fake repo, but verify via the widget
await tester.pumpWidget(
IopClientApp(testClient: fakeClient, statusRepository: fakeStatusRepo),
);
await tester.pump();
await tester.pump(const Duration(milliseconds: 100));
await tester.tap(find.byTooltip('Nodes'));
await tester.pumpAndSettle();
// Scroll to reveal node-2's provider catalog
await tester.drag(
find.byType(ListView),
const Offset(0.0, -1500.0),
);
await tester.pumpAndSettle();
// The degraded provider should show DEGRADED text (not ACTIVE)
expect(find.textContaining('DEGRADED'), findsOneWidget);
// Also confirm the provider id is shown
expect(find.text('provider-openai'), findsOneWidget);
},
);
// [REVIEW_API-2] DTO JSON parsing test
testWidgets(
'EdgeStatusResponseView.fromJson parses raw JSON and validates provider_snapshots',
(WidgetTester tester) async {
// This test verifies the HTTP JSON DTO parsing path used by
// HttpControlPlaneStatusRepository.fetchEdgeStatus.
final rawJson = {
'request_id': 'test-req-001',
'edge_id': 'edge-test',
'edge_name': 'Edge Test Node',
'observed_time_unix_nano': 1716584400000000000,
'nodes': [
{
'node_id': 'node-1',
'alias': 'Test Node',
'label': 'GPU-A100',
'connected': true,
'config': {
'adapters': [
{'type': 'ollama', 'enabled': true},
],
'concurrency': 8,
},
'provider_snapshots': [
{
'id': 'provider-ollama',
'adapter': 'ollama',
'type': 'llm',
'category': 'inference',
'status': 'active',
'health': 'degraded',
'capacity': 10,
'in_flight': 3,
'queued': 1,
'load_ratio': 0.3,
'served_models': ['llama-3.1', 'mistral'],
'lifecycle_capabilities': ['start', 'stop', 'restart'],
},
{
'id': 'provider-vllm',
'adapter': 'vllm',
'type': 'llm',
'category': 'inference',
'status': 'ready',
'health': 'healthy',
'capacity': 20,
'in_flight': 0,
'queued': 0,
'load_ratio': 0.0,
'served_models': ['vicuna'],
'lifecycle_capabilities': ['start', 'stop'],
},
],
},
],
'capabilities': [
{
'kind': 'Serving',
'available': true,
'status': 'ready',
'summary': 'Active',
},
],
'domain_agents': [],
'metadata': {'region': 'us-west'},
'error': '',
};
// Parse the raw JSON using the DTO from
// apps/client/lib/control_plane_status_dto.dart
final parsed = EdgeStatusResponseView.fromJson(rawJson);
// Validate top-level fields
expect(parsed.requestId, equals('test-req-001'));
expect(parsed.edgeId, equals('edge-test'));
expect(parsed.edgeName, equals('Edge Test Node'));
expect(parsed.observedTimeUnixNano, equals(1716584400000000000));
expect(parsed.nodes.length, equals(1));
expect(parsed.error, equals(''));
// Validate node fields
final node = parsed.nodes.first;
expect(node.nodeId, equals('node-1'));
expect(node.alias, equals('Test Node'));
expect(node.label, equals('GPU-A100'));
expect(node.connected, isTrue);
expect(node.config, isNotNull);
expect(node.config!.adapters.length, equals(1));
expect(node.config!.adapters.first.type, equals('ollama'));
expect(node.config!.adapters.first.enabled, isTrue);
expect(node.config!.concurrency, equals(8));
// Validate provider_snapshots fields
final prov0 = node.providerSnapshots[0];
expect(prov0.id, equals('provider-ollama'));
expect(prov0.adapter, equals('ollama'));
expect(prov0.type, equals('llm'));
expect(prov0.category, equals('inference'));
expect(prov0.status, equals('active'));
expect(prov0.health, equals('degraded'));
expect(prov0.capacity, equals(10));
expect(prov0.inFlight, equals(3));
expect(prov0.queued, equals(1));
expect(prov0.loadRatio, equals(0.3));
expect(prov0.servedModels, equals(['llama-3.1', 'mistral']));
expect(
prov0.lifecycleCapabilities,
equals(['start', 'stop', 'restart']),
);
// Validate second provider snapshot
final prov1 = node.providerSnapshots[1];
expect(prov1.id, equals('provider-vllm'));
expect(prov1.adapter, equals('vllm'));
expect(prov1.status, equals('ready'));
expect(prov1.health, equals('healthy'));
expect(prov1.capacity, equals(20));
expect(prov1.inFlight, equals(0));
expect(prov1.queued, equals(0));
expect(prov1.loadRatio, equals(0.0));
expect(prov1.servedModels, equals(['vicuna']));
expect(prov1.lifecycleCapabilities, equals(['start', 'stop']));
// Validate capabilities and metadata
expect(parsed.capabilities.length, equals(1));
expect(parsed.capabilities.first.kind, equals('Serving'));
expect(parsed.capabilities.first.available, isTrue);
expect(parsed.metadata, equals({'region': 'us-west'}));
// Verify that the same raw JSON can also be rendered in the widget.
// This connects the DTO parsing path to the UI path.
final statusView = EdgeStatusResponseView.fromJson(rawJson);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NodesPanel(
edges: [
FleetEdgeView(
edgeId: 'edge-test',
edgeName: 'Edge Test Node',
version: '1.0.0',
protocol: 'iop-wire-v1',
connected: true,
health: 'healthy',
lastSeen: DateTime.now(),
nodeCount: 1,
capabilities: [],
domainAgents: [],
error: '',
),
],
selectedEdgeId: 'edge-test',
selectedEdgeStatus: statusView,
isLoading: false,
error: null,
onSelectEdge: (_) {},
onRefresh: () {},
),
),
),
);
await tester.pump();
// Verify degraded provider shows correctly via _stateColor semantics
expect(find.textContaining('DEGRADED'), findsOneWidget);
expect(find.textContaining('HEALTHY'), findsOneWidget);
expect(find.textContaining('provider-ollama'), findsOneWidget);
expect(find.textContaining('provider-vllm'), findsOneWidget);
},
);
// [G03] Control Plane predecessor values: health=available/status=available
testWidgets(
'Provider with health=available and status=available shows AVAILABLE text with green color',
(WidgetTester tester) async {
// Test uses standalone NodesPanel with available providers.
// The DTO test below covers JSON parsing for all predecessor values.
final statusView = EdgeStatusResponseView.fromJson({
'request_id': 'test-req-g03-solo',
'edge_id': 'edge-g03',
'edge_name': 'Edge G03 Test',
'observed_time_unix_nano': 1716584400000000000,
'nodes': [
{
'node_id': 'node-g03',
'alias': 'G03 Test Node',
'label': 'GPU-Test',
'connected': true,
'config': {
'adapters': [
{'type': 'ollama', 'enabled': true},
],
'concurrency': 4,
},
'provider_snapshots': [
{
'id': 'provider-available',
'adapter': 'ollama',
'type': 'llm',
'category': 'inference',
'status': 'available',
'health': 'available',
'capacity': 10,
'in_flight': 2,
'queued': 0,
'load_ratio': 0.2,
'served_models': ['llama-3.1'],
'lifecycle_capabilities': ['start', 'stop'],
},
],
},
],
'capabilities': [],
'domain_agents': [],
'metadata': {},
'error': '',
});
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NodesPanel(
edges: [
FleetEdgeView(
edgeId: 'edge-g03',
edgeName: 'Edge G03 Test',
version: '1.0.0',
protocol: 'iop-wire-v1',
connected: true,
health: 'healthy',
lastSeen: DateTime.now(),
nodeCount: 1,
capabilities: [],
domainAgents: [],
error: '',
),
],
selectedEdgeId: 'edge-g03',
selectedEdgeStatus: statusView,
isLoading: false,
error: null,
onSelectEdge: (_) {},
onRefresh: () {},
),
),
),
);
await tester.pump();
// health=available/status=available should show AVAILABLE text in green (#10B981)
expect(find.text('AVAILABLE'), findsOneWidget);
expect(find.text('provider-available'), findsOneWidget);
// [G02] Status label color assertions
// Find the AVAILABLE status label Text widget (inside Container) and verify its color
final availableLabelFinder = find.byWidgetPredicate((Widget widget) {
if (widget is Text) {
return widget.data == 'AVAILABLE' && widget.style != null;
}
return false;
});
expect(availableLabelFinder, findsOneWidget);
final availableLabel = availableLabelFinder.evaluate().first.widget as Text;
expect(availableLabel.style?.color, equals(const Color(0xFF10B981)));
},
);
// [G03] DTO JSON parsing test for Control Plane predecessor values
testWidgets(
'EdgeStatusResponseView parses health=available/status=available and health=unavailable/status=backlog',
(WidgetTester tester) async {
final rawJson = {
'request_id': 'test-req-g03',
'edge_id': 'edge-g03',
'edge_name': 'Edge G03 Test',
'observed_time_unix_nano': 1716584400000000000,
'nodes': [
{
'node_id': 'node-g03',
'alias': 'G03 Test Node',
'label': 'GPU-Test',
'connected': true,
'config': {
'adapters': [
{'type': 'ollama', 'enabled': true},
],
'concurrency': 4,
},
'provider_snapshots': [
{
// health=available, status=available -> green
'id': 'provider-available',
'adapter': 'ollama',
'type': 'llm',
'category': 'inference',
'status': 'available',
'health': 'available',
'capacity': 10,
'in_flight': 2,
'queued': 0,
'load_ratio': 0.2,
'served_models': ['llama-3.1'],
'lifecycle_capabilities': ['start', 'stop'],
},
{
// health=unavailable, status=backlog -> red
'id': 'provider-unavailable',
'adapter': 'vllm',
'type': 'llm',
'category': 'inference',
'status': 'backlog',
'health': 'unavailable',
'capacity': 20,
'in_flight': 0,
'queued': 0,
'load_ratio': 0.0,
'served_models': [],
'lifecycle_capabilities': ['start', 'stop'],
},
{
// health=unknown, status=unknown -> neutral grey
'id': 'provider-unknown',
'adapter': 'openai',
'type': 'llm',
'category': 'external',
'status': 'unknown',
'health': '',
'capacity': 100,
'in_flight': 0,
'queued': 0,
'load_ratio': 0.0,
'served_models': ['gpt-4'],
'lifecycle_capabilities': [],
},
],
},
],
'capabilities': [],
'domain_agents': [],
'metadata': {},
'error': '',
};
// Parse the raw JSON
final parsed = EdgeStatusResponseView.fromJson(rawJson);
expect(parsed.requestId, equals('test-req-g03'));
expect(parsed.edgeId, equals('edge-g03'));
expect(parsed.nodes.length, equals(1));
// Validate provider snapshots
final prov0 = parsed.nodes[0].providerSnapshots[0];
expect(prov0.id, equals('provider-available'));
expect(prov0.health, equals('available'));
expect(prov0.status, equals('available'));
final prov1 = parsed.nodes[0].providerSnapshots[1];
expect(prov1.id, equals('provider-unavailable'));
expect(prov1.health, equals('unavailable'));
expect(prov1.status, equals('backlog'));
final prov2 = parsed.nodes[0].providerSnapshots[2];
expect(prov2.id, equals('provider-unknown'));
expect(prov2.health, equals(''));
expect(prov2.status, equals('unknown'));
// Verify that the same raw JSON can be rendered in the widget.
final statusView = EdgeStatusResponseView.fromJson(rawJson);
await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: NodesPanel(
edges: [
FleetEdgeView(
edgeId: 'edge-g03',
edgeName: 'Edge G03 Test',
version: '1.0.0',
protocol: 'iop-wire-v1',
connected: true,
health: 'healthy',
lastSeen: DateTime.now(),
nodeCount: 1,
capabilities: [],
domainAgents: [],
error: '',
),
],
selectedEdgeId: 'edge-g03',
selectedEdgeStatus: statusView,
isLoading: false,
error: null,
onSelectEdge: (_) {},
onRefresh: () {},
),
),
),
);
await tester.pump();
// Verify available provider shows AVAILABLE text
expect(find.text('AVAILABLE'), findsWidgets);
expect(find.textContaining('provider-available'), findsOneWidget);
// [G02] UNAVAILABLE status label color assertion (#EF4444 red)
final unavailableLabelFinder = find.byWidgetPredicate((Widget widget) {
if (widget is Text) {
return widget.data == 'UNAVAILABLE' && widget.style != null;
}
return false;
});
expect(unavailableLabelFinder, findsOneWidget);
final unavailableLabel = unavailableLabelFinder.evaluate().first.widget as Text;
expect(unavailableLabel.style?.color, equals(const Color(0xFFEF4444)));
// [G02] UNKNOWN status label color assertion (#64748B grey)
// For provider-unknown, health='' so status='unknown' -> _stateColor returns Color(0xFF64748B)
// displayState = p.health.isNotEmpty ? p.health : p.status = 'unknown'
// displayState.toUpperCase() = 'UNKNOWN'
final unknownLabelFinder = find.byWidgetPredicate((Widget widget) {
if (widget is Text) {
return widget.data == 'UNKNOWN' && widget.style != null;
}
return false;
});
expect(unknownLabelFinder, findsOneWidget);
final unknownLabel = unknownLabelFinder.evaluate().first.widget as Text;
expect(unknownLabel.style?.color, equals(const Color(0xFF64748B)));
// Verify unavailable provider shows UNAVAILABLE text
expect(find.text('UNAVAILABLE'), findsWidgets);
expect(find.textContaining('provider-unavailable'), findsOneWidget);
// Verify unknown provider shows UNKNOWN text (from status)
expect(find.text('UNKNOWN'), findsWidgets);
expect(find.textContaining('provider-unknown'), findsOneWidget);
},
);
testWidgets(
'Client App mobile screen layout verification for layout and overflow',
(WidgetTester tester) async {
// Set a small mobile screen size
tester.view.physicalSize = const Size(360, 640);
tester.view.devicePixelRatio = 1.0;
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));
// Overview
expect(find.text('Operations Overview'), findsOneWidget);
// Switch to Edges panel
await tester.tap(find.byTooltip('Edges'));
await tester.pump();
expect(find.text('Edge Alpha'), findsNWidgets(2));
// Switch to Runtime panel
await tester.tap(find.byTooltip('Runtime'));
await tester.pump();
expect(find.text('Operations & Domain Agents'), findsOneWidget);
// Reset view size after test
addTearDown(() {
tester.view.resetPhysicalSize();
tester.view.resetDevicePixelRatio();
});
},
);
}