- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
473 lines
17 KiB
Dart
473 lines
17 KiB
Dart
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 'package:iop_client/widgets/nodes_panel.dart';
|
|
import 'support/client_test_harness.dart';
|
|
|
|
void main() {
|
|
// [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);
|
|
},
|
|
);
|
|
}
|