334 lines
11 KiB
Dart
334 lines
11 KiB
Dart
import 'package:flutter_test/flutter_test.dart';
|
|
import 'package:xworkmate/runtime/go_task_service_client.dart';
|
|
import 'package:xworkmate/runtime/runtime_models.dart';
|
|
|
|
void main() {
|
|
group('GoTaskServiceRequest routing', () {
|
|
GoTaskServiceRequest buildRequest({
|
|
required AssistantExecutionTarget target,
|
|
bool multiAgent = false,
|
|
}) {
|
|
return GoTaskServiceRequest(
|
|
sessionId: 'thread-1',
|
|
threadId: 'thread-1',
|
|
target: target,
|
|
prompt: 'hello',
|
|
workingDirectory: '/tmp/workspace',
|
|
model: '',
|
|
thinking: 'medium',
|
|
selectedSkills: const <String>[],
|
|
inlineAttachments: const <GatewayChatAttachmentPayload>[],
|
|
localAttachments: const <CollaborationAttachment>[],
|
|
aiGatewayBaseUrl: '',
|
|
aiGatewayApiKey: '',
|
|
agentId: '',
|
|
metadata: const <String, dynamic>{},
|
|
multiAgent: multiAgent,
|
|
);
|
|
}
|
|
|
|
test('routes local and remote targets to the ACP single lane', () {
|
|
expect(
|
|
buildRequest(target: AssistantExecutionTarget.local).route,
|
|
GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
expect(
|
|
buildRequest(target: AssistantExecutionTarget.remote).route,
|
|
GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
});
|
|
|
|
test('routes single-agent targets to the ACP single lane', () {
|
|
expect(
|
|
buildRequest(target: AssistantExecutionTarget.singleAgent).route,
|
|
GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
});
|
|
|
|
test('routes multi-agent requests to the ACP multi lane', () {
|
|
expect(
|
|
buildRequest(
|
|
target: AssistantExecutionTarget.remote,
|
|
multiAgent: true,
|
|
).route,
|
|
GoTaskServiceRoute.externalAcpMulti,
|
|
);
|
|
});
|
|
});
|
|
|
|
group('GoTaskService ACP mapping', () {
|
|
test('request maps skills, attachments, and provider into ACP params', () {
|
|
const request = GoTaskServiceRequest(
|
|
sessionId: 'session-1',
|
|
threadId: 'thread-1',
|
|
target: AssistantExecutionTarget.singleAgent,
|
|
prompt: 'hello world',
|
|
workingDirectory: '/tmp/workspace',
|
|
model: 'codex-sonnet',
|
|
thinking: 'medium',
|
|
selectedSkills: <String>['PPT', 'Browser Automation'],
|
|
inlineAttachments: <GatewayChatAttachmentPayload>[
|
|
GatewayChatAttachmentPayload(
|
|
type: 'inline',
|
|
fileName: 'note.txt',
|
|
mimeType: 'text/plain',
|
|
content: 'aGVsbG8=',
|
|
),
|
|
],
|
|
localAttachments: <CollaborationAttachment>[
|
|
CollaborationAttachment(
|
|
name: 'spec.md',
|
|
path: '/tmp/workspace/spec.md',
|
|
description: 'workspace spec',
|
|
),
|
|
],
|
|
aiGatewayBaseUrl: 'https://gateway.example.com',
|
|
aiGatewayApiKey: 'secret',
|
|
agentId: '',
|
|
metadata: <String, dynamic>{},
|
|
routing: ExternalCodeAgentAcpRoutingConfig.auto(
|
|
preferredGatewayTarget: 'local',
|
|
availableSkills: <ExternalCodeAgentAcpAvailableSkill>[
|
|
ExternalCodeAgentAcpAvailableSkill(
|
|
id: 'pptx',
|
|
label: 'PPTX',
|
|
description: 'deck skill',
|
|
),
|
|
],
|
|
),
|
|
provider: SingleAgentProvider.opencode,
|
|
);
|
|
|
|
final params = request.toExternalAcpParams();
|
|
|
|
expect(params['sessionId'], 'session-1');
|
|
expect(params['threadId'], 'thread-1');
|
|
expect(params['mode'], 'single-agent');
|
|
expect(params['workingDirectory'], '/tmp/workspace');
|
|
expect(params['provider'], 'opencode');
|
|
expect(params['model'], 'codex-sonnet');
|
|
expect(params['thinking'], 'medium');
|
|
expect(params['selectedSkills'], <String>['PPT', 'Browser Automation']);
|
|
expect(params['attachments'], <Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'name': 'spec.md',
|
|
'description': 'workspace spec',
|
|
'path': '/tmp/workspace/spec.md',
|
|
},
|
|
<String, dynamic>{
|
|
'name': 'note.txt',
|
|
'description': 'text/plain',
|
|
'path': '',
|
|
},
|
|
]);
|
|
expect(params['inlineAttachments'], <Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'name': 'note.txt',
|
|
'mimeType': 'text/plain',
|
|
'content': 'aGVsbG8=',
|
|
'sizeBytes': 5,
|
|
},
|
|
]);
|
|
expect(params['routing'], <String, dynamic>{
|
|
'routingMode': 'auto',
|
|
'preferredGatewayTarget': 'local',
|
|
'explicitSkills': const <String>[],
|
|
'allowSkillInstall': false,
|
|
'availableSkills': <Map<String, dynamic>>[
|
|
<String, dynamic>{
|
|
'id': 'pptx',
|
|
'label': 'PPTX',
|
|
'description': 'deck skill',
|
|
'installed': true,
|
|
},
|
|
],
|
|
});
|
|
});
|
|
|
|
test('request synthesizes routing when caller omits it', () {
|
|
const request = GoTaskServiceRequest(
|
|
sessionId: 'session-implicit-routing',
|
|
threadId: 'thread-implicit-routing',
|
|
target: AssistantExecutionTarget.singleAgent,
|
|
prompt: 'hello world',
|
|
workingDirectory: '/tmp/workspace',
|
|
model: 'codex-sonnet',
|
|
thinking: '',
|
|
selectedSkills: <String>['PPTX'],
|
|
inlineAttachments: <GatewayChatAttachmentPayload>[],
|
|
localAttachments: <CollaborationAttachment>[],
|
|
aiGatewayBaseUrl: '',
|
|
aiGatewayApiKey: '',
|
|
agentId: '',
|
|
metadata: <String, dynamic>{},
|
|
provider: SingleAgentProvider.opencode,
|
|
);
|
|
|
|
final params = request.toExternalAcpParams();
|
|
|
|
expect(params['routing'], <String, dynamic>{
|
|
'routingMode': 'explicit',
|
|
'preferredGatewayTarget': 'local',
|
|
'explicitExecutionTarget': 'singleAgent',
|
|
'explicitProviderId': 'opencode',
|
|
'explicitModel': 'codex-sonnet',
|
|
'explicitSkills': const <String>['PPTX'],
|
|
'allowSkillInstall': false,
|
|
'availableSkills': const <Map<String, dynamic>>[],
|
|
});
|
|
});
|
|
|
|
test('request maps gateway intents onto the ACP single lane', () {
|
|
const request = GoTaskServiceRequest(
|
|
sessionId: 'session-2',
|
|
threadId: 'thread-2',
|
|
target: AssistantExecutionTarget.local,
|
|
prompt: 'search latest news',
|
|
workingDirectory: '/tmp/workspace',
|
|
model: '',
|
|
thinking: '',
|
|
selectedSkills: <String>[],
|
|
inlineAttachments: <GatewayChatAttachmentPayload>[],
|
|
localAttachments: <CollaborationAttachment>[],
|
|
aiGatewayBaseUrl: '',
|
|
aiGatewayApiKey: '',
|
|
agentId: 'agent-1',
|
|
metadata: <String, dynamic>{'source': 'test'},
|
|
routingHint: 'gateway',
|
|
);
|
|
|
|
final params = request.toExternalAcpParams();
|
|
|
|
expect(request.route, GoTaskServiceRoute.externalAcpSingle);
|
|
expect(request.routingExecutionTarget, 'gateway');
|
|
expect(params['mode'], 'gateway-chat');
|
|
expect(params['executionTarget'], 'local');
|
|
expect(params['agentId'], 'agent-1');
|
|
expect(params['routingHint'], 'gateway');
|
|
expect(params['requestedExecutionTarget'], 'local');
|
|
expect(params['routing'], <String, dynamic>{
|
|
'routingMode': 'explicit',
|
|
'preferredGatewayTarget': 'local',
|
|
'explicitExecutionTarget': 'local',
|
|
'explicitSkills': const <String>[],
|
|
'allowSkillInstall': false,
|
|
'availableSkills': const <Map<String, dynamic>>[],
|
|
});
|
|
});
|
|
|
|
test(
|
|
'run result prefers completion text and preserves resolved workspace',
|
|
() {
|
|
final result = goTaskServiceResultFromAcpResponse(
|
|
<String, dynamic>{
|
|
'result': <String, dynamic>{
|
|
'success': true,
|
|
'turnId': 'turn-7',
|
|
'summary': 'summary text',
|
|
'resolvedModel': 'codex-sonnet',
|
|
'resolvedWorkingDirectory': '/tmp/thread',
|
|
'resolvedWorkspaceRefKind': 'remotePath',
|
|
},
|
|
},
|
|
route: GoTaskServiceRoute.externalAcpSingle,
|
|
streamedText: 'partial output',
|
|
completedMessage: 'final output',
|
|
);
|
|
|
|
expect(result.success, isTrue);
|
|
expect(result.turnId, 'turn-7');
|
|
expect(result.message, 'final output');
|
|
expect(result.resolvedModel, 'codex-sonnet');
|
|
expect(result.resolvedWorkingDirectory, '/tmp/thread');
|
|
expect(result.resolvedWorkspaceRefKind, WorkspaceRefKind.remotePath);
|
|
},
|
|
);
|
|
|
|
test(
|
|
'run result falls back to effective working directory when resolved path is absent',
|
|
() {
|
|
final result = goTaskServiceResultFromAcpResponse(
|
|
<String, dynamic>{
|
|
'result': <String, dynamic>{
|
|
'success': true,
|
|
'turnId': 'turn-8',
|
|
'summary': 'summary text',
|
|
'effectiveWorkingDirectory': '/tmp/effective-thread',
|
|
},
|
|
},
|
|
route: GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
|
|
expect(result.turnId, 'turn-8');
|
|
expect(result.message, 'summary text');
|
|
expect(result.resolvedWorkingDirectory, '/tmp/effective-thread');
|
|
},
|
|
);
|
|
|
|
test('run result falls back to error text when failed payload has no message', () {
|
|
final result = goTaskServiceResultFromAcpResponse(
|
|
<String, dynamic>{
|
|
'result': <String, dynamic>{
|
|
'success': false,
|
|
'turnId': 'turn-error',
|
|
'error': 'missing bearer authorization',
|
|
},
|
|
},
|
|
route: GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
|
|
expect(result.success, isFalse);
|
|
expect(result.turnId, 'turn-error');
|
|
expect(result.message, 'missing bearer authorization');
|
|
});
|
|
|
|
test(
|
|
'run result includes skill install diagnostics when failed payload requires install',
|
|
() {
|
|
final result = goTaskServiceResultFromAcpResponse(
|
|
<String, dynamic>{
|
|
'result': <String, dynamic>{
|
|
'success': false,
|
|
'turnId': 'turn-skill-install',
|
|
'error': 'missing bearer authorization',
|
|
'needsSkillInstall': true,
|
|
'skillCandidates': <Map<String, dynamic>>[
|
|
<String, dynamic>{'id': 'pptx', 'label': 'pptx'},
|
|
],
|
|
},
|
|
},
|
|
route: GoTaskServiceRoute.externalAcpSingle,
|
|
);
|
|
|
|
expect(result.success, isFalse);
|
|
expect(result.turnId, 'turn-skill-install');
|
|
expect(result.message, 'missing bearer authorization (skills: pptx)');
|
|
},
|
|
);
|
|
|
|
test('session update recognizes delta notifications', () {
|
|
final update = goTaskServiceUpdateFromAcpNotification(<String, dynamic>{
|
|
'method': 'session.update',
|
|
'params': <String, dynamic>{
|
|
'sessionId': 'session-2',
|
|
'threadId': 'thread-2',
|
|
'turnId': 'turn-2',
|
|
'type': 'delta',
|
|
'mode': 'multi-agent',
|
|
'delta': 'hello',
|
|
'pending': true,
|
|
},
|
|
});
|
|
|
|
expect(update, isNotNull);
|
|
expect(update!.sessionId, 'session-2');
|
|
expect(update.threadId, 'thread-2');
|
|
expect(update.turnId, 'turn-2');
|
|
expect(update.route, GoTaskServiceRoute.externalAcpMulti);
|
|
expect(update.isDelta, isTrue);
|
|
expect(update.text, 'hello');
|
|
expect(update.pending, isTrue);
|
|
});
|
|
});
|
|
}
|