Refresh agent providers on agent selection
This commit is contained in:
parent
e7a5a7a5c6
commit
fc3c06e605
@ -53,6 +53,23 @@ extension AppControllerDesktopWorkspaceExecution on AppController {
|
||||
final currentTarget = assistantExecutionTargetForSession(
|
||||
sessionsControllerInternal.currentSessionKey,
|
||||
);
|
||||
final shouldRefreshAgentProviders =
|
||||
resolvedTarget.isAgent && assistantProviderCatalog.isEmpty;
|
||||
if (shouldRefreshAgentProviders) {
|
||||
try {
|
||||
await refreshSingleAgentCapabilitiesInternal(forceRefresh: true);
|
||||
} catch (_) {
|
||||
// Keep target selection interactive even when a just-in-time
|
||||
// capabilities refresh fails. The provider picker will remain hidden
|
||||
// until the next successful refresh.
|
||||
}
|
||||
if (currentTarget == resolvedTarget &&
|
||||
settings.assistantExecutionTarget == resolvedTarget) {
|
||||
recomputeTasksInternal();
|
||||
notifyIfActiveInternal();
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (currentTarget == resolvedTarget &&
|
||||
settings.assistantExecutionTarget == resolvedTarget) {
|
||||
return;
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
import 'dart:convert';
|
||||
import 'dart:io';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:xworkmate/app/app_controller.dart';
|
||||
import 'package:xworkmate/runtime/runtime_models.dart';
|
||||
import 'package:xworkmate/runtime/secure_config_store.dart';
|
||||
|
||||
void main() {
|
||||
group('AssistantExecutionTarget', () {
|
||||
@ -45,5 +50,130 @@ void main() {
|
||||
expect(provider.providerId, kCanonicalGatewayProviderId);
|
||||
expect(provider.label, kCanonicalGatewayProviderLabel);
|
||||
});
|
||||
|
||||
test(
|
||||
'refreshes agent provider catalog when agent mode is selected with an empty catalog',
|
||||
() async {
|
||||
final capture = await _startCapabilityServer();
|
||||
addTearDown(capture.close);
|
||||
|
||||
final storeRoot = await Directory.systemTemp.createTemp(
|
||||
'xworkmate-agent-provider-refresh-',
|
||||
);
|
||||
addTearDown(() async {
|
||||
if (await storeRoot.exists()) {
|
||||
await storeRoot.delete(recursive: true);
|
||||
}
|
||||
});
|
||||
|
||||
final store = SecureConfigStore(
|
||||
secretRootPathResolver: () async => '${storeRoot.path}/secrets',
|
||||
appDataRootPathResolver: () async => '${storeRoot.path}/app-data',
|
||||
supportRootPathResolver: () async => '${storeRoot.path}/support',
|
||||
enableSecureStorage: false,
|
||||
);
|
||||
await store.initialize();
|
||||
await store.saveAccountSyncState(
|
||||
AccountSyncState.defaults().copyWith(
|
||||
syncedDefaults: AccountRemoteProfile.defaults().copyWith(
|
||||
bridgeServerUrl: capture.baseEndpoint.toString(),
|
||||
),
|
||||
syncState: 'ready',
|
||||
),
|
||||
);
|
||||
|
||||
final controller = AppController(
|
||||
store: store,
|
||||
environmentOverride: const <String, String>{
|
||||
'BRIDGE_AUTH_TOKEN': 'bridge-token',
|
||||
},
|
||||
);
|
||||
addTearDown(controller.dispose);
|
||||
|
||||
await controller.sessionsController.switchSession('session-1');
|
||||
await _waitForRequest(capture, minimumCount: 1);
|
||||
|
||||
expect(controller.assistantProviderCatalog, isEmpty);
|
||||
|
||||
await controller.setAssistantExecutionTarget(
|
||||
AssistantExecutionTarget.agent,
|
||||
);
|
||||
|
||||
expect(
|
||||
controller.assistantProviderCatalog.map((item) => item.providerId),
|
||||
containsAll(<String>['codex', 'opencode', 'gemini']),
|
||||
);
|
||||
expect(capture.requestCount, greaterThanOrEqualTo(2));
|
||||
expect(capture.lastAuthorizationHeader, 'Bearer bridge-token');
|
||||
},
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _waitForRequest(
|
||||
_CapabilityServerCapture capture, {
|
||||
required int minimumCount,
|
||||
}) async {
|
||||
for (var index = 0; index < 20; index += 1) {
|
||||
if (capture.requestCount >= minimumCount) {
|
||||
return;
|
||||
}
|
||||
await Future<void>.delayed(const Duration(milliseconds: 100));
|
||||
}
|
||||
fail('Timed out waiting for $minimumCount capability requests');
|
||||
}
|
||||
|
||||
Future<_CapabilityServerCapture> _startCapabilityServer() async {
|
||||
final server = await HttpServer.bind(InternetAddress.loopbackIPv4, 0);
|
||||
final capture = _CapabilityServerCapture._(
|
||||
server,
|
||||
Uri.parse('http://127.0.0.1:${server.port}'),
|
||||
);
|
||||
server.listen((request) async {
|
||||
capture.requestCount += 1;
|
||||
capture.lastAuthorizationHeader =
|
||||
request.headers.value(HttpHeaders.authorizationHeader) ?? '';
|
||||
await utf8.decoder.bind(request).join();
|
||||
if (capture.requestCount == 1) {
|
||||
request.response.statusCode = HttpStatus.internalServerError;
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'error': <String, dynamic>{'message': 'startup refresh failed'},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
return;
|
||||
}
|
||||
|
||||
request.response.headers.contentType = ContentType.json;
|
||||
request.response.write(
|
||||
jsonEncode(<String, dynamic>{
|
||||
'jsonrpc': '2.0',
|
||||
'id': 'capabilities',
|
||||
'result': <String, dynamic>{
|
||||
'singleAgent': true,
|
||||
'multiAgent': true,
|
||||
'providerCatalog': <Map<String, dynamic>>[
|
||||
<String, dynamic>{'providerId': 'codex', 'label': 'Codex'},
|
||||
<String, dynamic>{'providerId': 'opencode', 'label': 'OpenCode'},
|
||||
<String, dynamic>{'providerId': 'gemini', 'label': 'Gemini'},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
await request.response.close();
|
||||
});
|
||||
return capture;
|
||||
}
|
||||
|
||||
class _CapabilityServerCapture {
|
||||
_CapabilityServerCapture._(this._server, this.baseEndpoint);
|
||||
|
||||
final HttpServer _server;
|
||||
final Uri baseEndpoint;
|
||||
int requestCount = 0;
|
||||
String lastAuthorizationHeader = '';
|
||||
|
||||
Future<void> close() => _server.close(force: true);
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user