xworkmate-app/test/app_controller_desktop_runtime_cleanup_test.dart
2026-04-11 12:07:34 +08:00

291 lines
9.2 KiB
Dart

import 'dart:convert';
import 'dart:io';
import 'package:flutter_test/flutter_test.dart';
import 'package:xworkmate/app/app_controller_desktop_core.dart';
import 'package:xworkmate/app/app_controller_desktop_skill_permissions.dart';
import 'package:xworkmate/app/app_controller_desktop_thread_sessions.dart';
import 'package:xworkmate/app/app_controller_desktop_workspace_execution.dart';
import 'package:xworkmate/runtime/desktop_platform_service.dart';
import 'package:xworkmate/runtime/go_task_service_client.dart';
import 'package:xworkmate/runtime/runtime_models.dart';
import 'package:xworkmate/runtime/secure_config_store.dart';
import 'package:xworkmate/runtime/skill_directory_access.dart';
void main() {
TestWidgetsFlutterBinding.ensureInitialized();
test('app-side runtime cleanup removes direct provider ACP side-channels', () {
final workspaceExecution = File(
'lib/app/app_controller_desktop_workspace_execution.dart',
).readAsStringSync();
expect(
workspaceExecution.contains("'skills.status'"),
isFalse,
reason:
'single-agent skill refresh should not query provider ACP skills.status directly',
);
expect(
workspaceExecution.contains('gatewayAcpClientInternal.request('),
isFalse,
reason: 'workspace execution should not issue direct provider ACP RPCs',
);
final runtimeCoordination = File(
'lib/app/app_controller_desktop_runtime_coordination_impl.dart',
);
if (runtimeCoordination.existsSync()) {
final source = runtimeCoordination.readAsStringSync();
expect(
source.contains('resolveSingleAgentEndpointRuntimeInternal'),
isFalse,
reason:
'single-agent endpoint probing should not remain in app-side runtime coordination',
);
expect(
source.contains('authorizationOverride'),
isFalse,
reason:
'app-side runtime coordination should not own provider auth side-channels',
);
}
});
test(
'single-agent skill refresh stays bridge-owned and does not query provider endpoints directly',
() async {
final root = Directory.systemTemp.createTempSync(
'xworkmate-runtime-cleanup-test-',
);
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
var requestCount = 0;
server.listen((request) async {
requestCount += 1;
final body = await utf8.decoder.bind(request).join();
final payload = jsonDecode(body) as Map<String, dynamic>;
final method = payload['method']?.toString().trim() ?? '';
final response = switch (method) {
'acp.capabilities' => <String, dynamic>{
'jsonrpc': '2.0',
'id': payload['id'],
'result': <String, dynamic>{
'singleAgent': true,
'multiAgent': false,
'providerCatalog': <Map<String, dynamic>>[
<String, dynamic>{'providerId': 'codex', 'label': 'Codex'},
],
},
},
'skills.status' => <String, dynamic>{
'jsonrpc': '2.0',
'id': payload['id'],
'result': <String, dynamic>{
'skills': <Map<String, dynamic>>[
<String, dynamic>{
'skillKey': 'remote-skill',
'name': 'Remote Skill',
'description': 'stale remote side-channel',
},
],
},
},
_ => <String, dynamic>{
'jsonrpc': '2.0',
'id': payload['id'],
'result': <String, dynamic>{},
},
};
request.response.headers.contentType = ContentType.json;
request.response.write(jsonEncode(response));
await request.response.close();
});
final store = SecureConfigStore(
enableSecureStorage: false,
appDataRootPathResolver: () async => root.path,
secretRootPathResolver: () async => root.path,
supportRootPathResolver: () async => root.path,
);
final controller = AppController(
store: store,
desktopPlatformService: UnsupportedDesktopPlatformService(),
skillDirectoryAccessService: _FakeSkillDirectoryAccessService(
root.path,
),
goTaskServiceClient: const _FakeGoTaskServiceClient(),
singleAgentSharedSkillScanRootOverrides: const <String>[],
availableSingleAgentProvidersOverride: const <SingleAgentProvider>[
SingleAgentProvider.codex,
],
);
addTearDown(() async {
controller.dispose();
await server.close(force: true);
if (root.existsSync()) {
await root.delete(recursive: true);
}
});
final endpoint = 'http://${server.address.address}:${server.port}';
final nextSettings = controller.settings.copyWith(
providerSyncDefinitions: <ExternalAcpEndpointProfile>[
ExternalAcpEndpointProfile.defaultsForProvider(
SingleAgentProvider.codex,
).copyWith(endpoint: endpoint),
],
);
controller.settingsController.snapshotInternal = nextSettings;
controller.lastObservedSettingsSnapshotInternal = nextSettings;
const sessionKey = 'draft:runtime-cleanup';
controller.initializeAssistantThreadContext(
sessionKey,
executionTarget: AssistantExecutionTarget.singleAgent,
singleAgentProvider: SingleAgentProvider.codex,
);
controller.upsertTaskThreadInternal(
sessionKey,
executionTarget: AssistantExecutionTarget.singleAgent,
singleAgentProvider: SingleAgentProvider.codex,
executionTargetSource: ThreadSelectionSource.explicit,
singleAgentProviderSource: ThreadSelectionSource.explicit,
);
expect(
controller.assistantExecutionTargetForSession(sessionKey),
AssistantExecutionTarget.singleAgent,
);
expect(
controller.singleAgentProviderForSession(sessionKey),
SingleAgentProvider.codex,
);
expect(
controller.singleAgentResolvedProviderForSession(sessionKey),
SingleAgentProvider.codex,
);
await controller.refreshSingleAgentSkillsForSession(sessionKey);
expect(controller.assistantImportedSkillsForSession(sessionKey), isEmpty);
expect(
requestCount,
0,
reason:
'single-agent skill refresh should not probe provider ACP endpoints directly',
);
},
);
}
class _FakeSkillDirectoryAccessService implements SkillDirectoryAccessService {
const _FakeSkillDirectoryAccessService(this.homeDirectory);
final String homeDirectory;
@override
bool get isSupported => false;
@override
Future<List<AuthorizedSkillDirectory>> authorizeDirectories({
List<String> suggestedPaths = const <String>[],
}) async {
return const <AuthorizedSkillDirectory>[];
}
@override
Future<AuthorizedSkillDirectory?> authorizeDirectory({
String suggestedPath = '',
}) async {
return null;
}
@override
Future<SkillDirectoryAccessHandle?> openDirectory(
AuthorizedSkillDirectory directory,
) async {
return null;
}
@override
Future<String> resolveUserHomeDirectory() async {
return homeDirectory;
}
}
class _FakeGoTaskServiceClient implements GoTaskServiceClient {
const _FakeGoTaskServiceClient();
@override
Future<void> cancelTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) async {}
@override
Future<void> closeTask({
required GoTaskServiceRoute route,
required AssistantExecutionTarget target,
required String sessionId,
required String threadId,
}) async {}
@override
Future<void> dispose() async {}
@override
Future<GoTaskServiceResult> executeTask(
GoTaskServiceRequest request, {
required void Function(GoTaskServiceUpdate update) onUpdate,
}) async {
return const GoTaskServiceResult(
success: true,
message: '',
turnId: '',
raw: <String, dynamic>{},
errorMessage: '',
resolvedModel: '',
route: GoTaskServiceRoute.externalAcpSingle,
);
}
@override
Future<ExternalCodeAgentAcpCapabilities> loadExternalAcpCapabilities({
required AssistantExecutionTarget target,
bool forceRefresh = false,
}) async {
return const ExternalCodeAgentAcpCapabilities(
singleAgent: true,
multiAgent: false,
providerCatalog: <SingleAgentProvider>[SingleAgentProvider.codex],
raw: <String, dynamic>{},
);
}
@override
Future<ExternalCodeAgentAcpRoutingResolution> resolveExternalAcpRouting({
required String taskPrompt,
required String workingDirectory,
required ExternalCodeAgentAcpRoutingConfig routing,
String aiGatewayBaseUrl = '',
String aiGatewayApiKey = '',
}) async {
return const ExternalCodeAgentAcpRoutingResolution(
raw: <String, dynamic>{
'resolvedExecutionTarget': 'single-agent',
'resolvedEndpointTarget': 'singleAgent',
'resolvedProviderId': 'codex',
'resolvedModel': '',
'resolvedSkills': <String>[],
'unavailable': false,
},
);
}
@override
Future<void> syncExternalProviders(
List<ExternalCodeAgentAcpSyncedProvider> providers,
) async {}
}