- agent-readable-repository-refactor 완료: 기존 테스트 파일 아카이브 이동 - 새 테스트 파일 추가 (edge, node, client, config, readability) - readability_audit 스크립트 및 baseline 추가 - roadmap/SDD 문서 갱신 - agent-client/pi/extensions/openai-sampling-parameters 추가
286 lines
8.3 KiB
Dart
286 lines
8.3 KiB
Dart
import 'dart:async';
|
|
import 'dart:convert';
|
|
import 'package:http/http.dart' as http;
|
|
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:iop_client/control_plane_status_controller.dart';
|
|
import 'package:iop_client/control_plane_status_client.dart';
|
|
import 'support/client_test_harness.dart';
|
|
|
|
class TrackingHttpClient extends http.BaseClient {
|
|
int requestCount = 0;
|
|
int closeCount = 0;
|
|
|
|
@override
|
|
Future<http.StreamedResponse> send(http.BaseRequest request) async {
|
|
requestCount += 1;
|
|
final response = http.Response(
|
|
json.encode({'edges': <Map<String, dynamic>>[]}),
|
|
200,
|
|
);
|
|
return http.StreamedResponse(
|
|
Stream<List<int>>.fromIterable([response.bodyBytes]),
|
|
response.statusCode,
|
|
headers: response.headers,
|
|
request: request,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void close() {
|
|
closeCount += 1;
|
|
super.close();
|
|
}
|
|
}
|
|
|
|
class ControllableStatusRepository implements ControlPlaneStatusRepository {
|
|
final fleetCompleter = Completer<FleetStatusResponseView>();
|
|
final statusCompleter = Completer<EdgeStatusResponseView>();
|
|
final eventsCompleter = Completer<List<EdgeNodeEventView>>();
|
|
|
|
int fleetCallCount = 0;
|
|
int statusCallCount = 0;
|
|
int eventsCallCount = 0;
|
|
|
|
@override
|
|
Future<FleetStatusResponseView> fetchFleetStatus() {
|
|
fleetCallCount += 1;
|
|
return fleetCompleter.future;
|
|
}
|
|
|
|
@override
|
|
Future<EdgeStatusResponseView> fetchEdgeStatus(String edgeId) {
|
|
statusCallCount += 1;
|
|
return statusCompleter.future;
|
|
}
|
|
|
|
@override
|
|
Future<List<EdgeNodeEventView>> fetchEdgeEvents(String edgeId) {
|
|
eventsCallCount += 1;
|
|
return eventsCompleter.future;
|
|
}
|
|
|
|
@override
|
|
Future<List<EdgeRegistryView>> fetchEdges() async => [];
|
|
|
|
@override
|
|
Future<EdgeOperationsResponseView> fetchEdgeOperations(String edgeId) async {
|
|
return EdgeOperationsResponseView(edgeId: edgeId, operations: []);
|
|
}
|
|
|
|
@override
|
|
Future<EdgeCommandResponseView> sendEdgeCommand(
|
|
String edgeId,
|
|
String operation, {
|
|
String? targetSelector,
|
|
Map<String, String>? parameters,
|
|
}) async {
|
|
return EdgeCommandResponseView(
|
|
requestId: 'req-1',
|
|
commandId: 'cmd-1',
|
|
edgeId: edgeId,
|
|
status: 'success',
|
|
summary: '',
|
|
error: '',
|
|
);
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
test('HTTP repository keeps injected client caller-owned', () async {
|
|
final client = TrackingHttpClient();
|
|
final repository = HttpControlPlaneStatusRepository(
|
|
baseUrl: 'http://control-plane.test',
|
|
client: client,
|
|
);
|
|
|
|
await repository.fetchEdges();
|
|
repository.close();
|
|
|
|
expect(client.requestCount, 1);
|
|
expect(client.closeCount, 0);
|
|
});
|
|
|
|
test(
|
|
'removes stale selected edge caches when fleet refresh drops edge',
|
|
() async {
|
|
final fakeStatusRepo = FakeControlPlaneStatusRepository();
|
|
final controller = ControlPlaneStatusController(
|
|
statusRepository: fakeStatusRepo,
|
|
);
|
|
|
|
// Initial fetch (edge-a and edge-b)
|
|
await controller.fetchEdges();
|
|
expect(controller.edges.length, 2);
|
|
expect(controller.selectedEdgeId, 'edge-a');
|
|
|
|
// Select edge-b and fetch details
|
|
controller.selectEdge('edge-b');
|
|
await controller.fetchEdgeDetails('edge-b');
|
|
|
|
expect(controller.selectedEdgeId, 'edge-b');
|
|
expect(controller.edgeStatuses.containsKey('edge-b'), true);
|
|
expect(controller.edgeEvents.containsKey('edge-b'), true);
|
|
|
|
// Drop edge-b from mock repository
|
|
fakeStatusRepo.mockEdges = [
|
|
EdgeRegistryView(
|
|
edgeId: 'edge-a',
|
|
edgeName: 'Edge Alpha',
|
|
version: '1.2.0',
|
|
capabilities: ['Serving', 'Automation'],
|
|
protocol: 'iop-wire-v1',
|
|
connected: true,
|
|
lastSeen: DateTime.now(),
|
|
),
|
|
];
|
|
|
|
// Refresh fleet status
|
|
await controller.fetchEdges();
|
|
|
|
// Verify selection falls back to edge-a and edge-b's cache is removed
|
|
expect(controller.selectedEdgeId, 'edge-a');
|
|
expect(controller.edgeStatuses.containsKey('edge-b'), false);
|
|
expect(controller.edgeEvents.containsKey('edge-b'), false);
|
|
},
|
|
);
|
|
|
|
test('ignores in-flight fetch completions after dispose', () async {
|
|
final fakeRepo = ControllableStatusRepository();
|
|
final controller = ControlPlaneStatusController(statusRepository: fakeRepo);
|
|
|
|
// Start fetching edges
|
|
final fetchFuture = controller.fetchEdges();
|
|
|
|
// Verify loading state is true
|
|
expect(controller.loadingEdges, true);
|
|
|
|
// Dispose controller
|
|
controller.dispose();
|
|
|
|
// Complete the future
|
|
fakeRepo.fleetCompleter.complete(
|
|
FleetStatusResponseView(
|
|
generatedAt: DateTime.now(),
|
|
edges: [
|
|
FleetEdgeView(
|
|
edgeId: 'edge-a',
|
|
edgeName: 'Edge Alpha',
|
|
version: '1.2.0',
|
|
protocol: 'iop-wire-v1',
|
|
connected: true,
|
|
health: 'healthy',
|
|
lastSeen: DateTime.now(),
|
|
nodeCount: 1,
|
|
capabilities: [],
|
|
domainAgents: [],
|
|
error: '',
|
|
),
|
|
],
|
|
),
|
|
);
|
|
|
|
// Await the future completion
|
|
await fetchFuture;
|
|
|
|
// Verify state was NOT updated (should remain default/initial values)
|
|
expect(controller.edges.isEmpty, true);
|
|
expect(
|
|
controller.loadingEdges,
|
|
true,
|
|
); // Loading edges is still true because mutation didn't happen after dispose
|
|
});
|
|
|
|
test('ignores in-flight fetch errors after dispose', () async {
|
|
final fakeRepo = ControllableStatusRepository();
|
|
final controller = ControlPlaneStatusController(statusRepository: fakeRepo);
|
|
|
|
final fetchFuture = controller.fetchEdges();
|
|
|
|
expect(controller.loadingEdges, true);
|
|
|
|
controller.dispose();
|
|
|
|
fakeRepo.fleetCompleter.completeError(StateError('fleet failed'));
|
|
|
|
await expectLater(fetchFuture, completes);
|
|
|
|
expect(controller.edges.isEmpty, true);
|
|
expect(controller.edgesError, isNull);
|
|
expect(controller.loadingEdges, true);
|
|
});
|
|
|
|
test('ignores status and events fetch completions after dispose', () async {
|
|
final fakeRepo = ControllableStatusRepository();
|
|
final controller = ControlPlaneStatusController(statusRepository: fakeRepo);
|
|
|
|
// Start fetching details
|
|
final detailsFuture = controller.fetchEdgeDetails('edge-a');
|
|
|
|
// Verify loading status is set to true
|
|
expect(controller.loadingStatuses['edge-a'], true);
|
|
expect(controller.loadingEvents['edge-a'], true);
|
|
|
|
// Dispose controller
|
|
controller.dispose();
|
|
|
|
// Complete the status and events
|
|
fakeRepo.statusCompleter.complete(
|
|
EdgeStatusResponseView(
|
|
requestId: 'req-1',
|
|
edgeId: 'edge-a',
|
|
edgeName: 'Edge Alpha',
|
|
observedTimeUnixNano: 0,
|
|
nodes: [],
|
|
capabilities: [],
|
|
domainAgents: [],
|
|
metadata: {},
|
|
error: '',
|
|
),
|
|
);
|
|
|
|
fakeRepo.eventsCompleter.complete([]);
|
|
|
|
// Await the future completion
|
|
await detailsFuture;
|
|
|
|
// Verify state was NOT updated
|
|
expect(controller.edgeStatuses.containsKey('edge-a'), false);
|
|
expect(controller.edgeEvents.containsKey('edge-a'), false);
|
|
});
|
|
|
|
test('ignores status and events fetch errors after dispose', () async {
|
|
final fakeRepo = ControllableStatusRepository();
|
|
final controller = ControlPlaneStatusController(statusRepository: fakeRepo);
|
|
|
|
final detailsFuture = controller.fetchEdgeDetails('edge-a');
|
|
|
|
expect(controller.loadingStatuses['edge-a'], true);
|
|
expect(controller.loadingEvents['edge-a'], true);
|
|
|
|
controller.dispose();
|
|
|
|
fakeRepo.statusCompleter.completeError(StateError('status failed'));
|
|
fakeRepo.eventsCompleter.completeError(StateError('events failed'));
|
|
|
|
await expectLater(detailsFuture, completes);
|
|
|
|
expect(controller.edgeStatuses.containsKey('edge-a'), false);
|
|
expect(controller.edgeEvents.containsKey('edge-a'), false);
|
|
expect(controller.statusErrors['edge-a'], isNull);
|
|
expect(controller.eventErrors['edge-a'], isNull);
|
|
expect(controller.loadingStatuses['edge-a'], true);
|
|
expect(controller.loadingEvents['edge-a'], true);
|
|
});
|
|
|
|
test('ignores selectEdge after dispose', () {
|
|
final fakeRepo = ControllableStatusRepository();
|
|
final controller = ControlPlaneStatusController(statusRepository: fakeRepo);
|
|
|
|
controller.dispose();
|
|
controller.selectEdge('edge-a');
|
|
|
|
expect(controller.selectedEdgeId, isNull);
|
|
expect(fakeRepo.statusCallCount, 0);
|
|
expect(fakeRepo.eventsCallCount, 0);
|
|
});
|
|
}
|