concevent-ai-agent-sdk 3.10.2 → 3.10.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/tests/agent.test.js +418 -2
- package/dist/tests/agent.test.js.map +1 -1
- package/dist/tests/builtin-tools.test.js +162 -0
- package/dist/tests/builtin-tools.test.js.map +1 -1
- package/dist/tests/mocks/tools.d.ts +23 -0
- package/dist/tests/mocks/tools.d.ts.map +1 -1
- package/dist/tests/mocks/tools.js +92 -0
- package/dist/tests/mocks/tools.js.map +1 -1
- package/dist/tests/orchestrator.test.js +140 -0
- package/dist/tests/orchestrator.test.js.map +1 -1
- package/dist/tests/result-storage.test.js +87 -0
- package/dist/tests/result-storage.test.js.map +1 -1
- package/dist/tests/retry.test.js +43 -0
- package/dist/tests/retry.test.js.map +1 -1
- package/package.json +1 -1
package/dist/tests/agent.test.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
2
|
import OpenAI from 'openai';
|
|
3
3
|
import { z } from 'zod';
|
|
4
|
-
import { createAgent, createEvent, ENCRYPTED_REASONING_MARKER } from '../src/index.js';
|
|
5
|
-
import { createMockTools, createFailingTool, createParallelTools, createTimedParallelTools, } from './mocks/tools.js';
|
|
4
|
+
import { createAgent, createEvent, ENCRYPTED_REASONING_MARKER, AgentError, formatErrorTrace, isAbortError, formatAIError, AI_ERROR_CATEGORIES, ToolExecutionTimeoutError, } from '../src/index.js';
|
|
5
|
+
import { createMockTools, createFailingTool, createParallelTools, createTimedParallelTools, createLargeResultTool, createBashLikeResultTool, createGrepLikeResultTool, createGlobLikeResultTool, createReadLikeResultTool, } from './mocks/tools.js';
|
|
6
6
|
vi.mock('openai');
|
|
7
7
|
function createMockOpenAIClient() {
|
|
8
8
|
return {
|
|
@@ -4009,4 +4009,420 @@ describe('createAgent - image input', () => {
|
|
|
4009
4009
|
}
|
|
4010
4010
|
});
|
|
4011
4011
|
});
|
|
4012
|
+
// ============================================================================
|
|
4013
|
+
// 1A. Result storage integration (tool-executor.ts coverage)
|
|
4014
|
+
// ============================================================================
|
|
4015
|
+
describe('createAgent - result storage integration', () => {
|
|
4016
|
+
let mockClient;
|
|
4017
|
+
beforeEach(() => {
|
|
4018
|
+
mockClient = createMockOpenAIClient();
|
|
4019
|
+
vi.mocked(OpenAI).mockImplementation(() => mockClient);
|
|
4020
|
+
});
|
|
4021
|
+
afterEach(() => {
|
|
4022
|
+
vi.clearAllMocks();
|
|
4023
|
+
});
|
|
4024
|
+
it('should store large tool results and include storage message in history', async () => {
|
|
4025
|
+
const agent = createAgent(createTestConfig({
|
|
4026
|
+
tools: [createLargeResultTool(200)],
|
|
4027
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4028
|
+
stream: false,
|
|
4029
|
+
}));
|
|
4030
|
+
// First call: LLM calls the tool
|
|
4031
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4032
|
+
{
|
|
4033
|
+
id: 'call-large-1',
|
|
4034
|
+
type: 'function',
|
|
4035
|
+
function: { name: 'largeResultTool', arguments: '{}' },
|
|
4036
|
+
},
|
|
4037
|
+
]));
|
|
4038
|
+
// Second call: LLM final response
|
|
4039
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Here is the result summary'));
|
|
4040
|
+
const result = await agent.chat('Run the tool', createTestContext());
|
|
4041
|
+
expect(result.message).toBe('Here is the result summary');
|
|
4042
|
+
// Check that the conversation history contains the storage message
|
|
4043
|
+
const history = result.conversationHistory;
|
|
4044
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of largeResultTool stored'));
|
|
4045
|
+
expect(storageMessage).toBeDefined();
|
|
4046
|
+
expect(storageMessage.content).toContain('ID:');
|
|
4047
|
+
expect(storageMessage.content).toContain('ReadResult');
|
|
4048
|
+
});
|
|
4049
|
+
it('should include Bash exit code summary in stored result message', async () => {
|
|
4050
|
+
const agent = createAgent(createTestConfig({
|
|
4051
|
+
tools: [createBashLikeResultTool()],
|
|
4052
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4053
|
+
stream: false,
|
|
4054
|
+
}));
|
|
4055
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4056
|
+
{
|
|
4057
|
+
id: 'call-bash-1',
|
|
4058
|
+
type: 'function',
|
|
4059
|
+
function: { name: 'Bash', arguments: '{}' },
|
|
4060
|
+
},
|
|
4061
|
+
]));
|
|
4062
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Command completed'));
|
|
4063
|
+
const result = await agent.chat('Run bash', createTestContext());
|
|
4064
|
+
const history = result.conversationHistory;
|
|
4065
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of Bash stored'));
|
|
4066
|
+
expect(storageMessage).toBeDefined();
|
|
4067
|
+
expect(storageMessage.content).toContain('Exit code: 0');
|
|
4068
|
+
expect(storageMessage.content).toContain('(success)');
|
|
4069
|
+
});
|
|
4070
|
+
it('should include Grep match count summary in stored result message', async () => {
|
|
4071
|
+
const agent = createAgent(createTestConfig({
|
|
4072
|
+
tools: [createGrepLikeResultTool()],
|
|
4073
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4074
|
+
stream: false,
|
|
4075
|
+
}));
|
|
4076
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4077
|
+
{
|
|
4078
|
+
id: 'call-grep-1',
|
|
4079
|
+
type: 'function',
|
|
4080
|
+
function: { name: 'Grep', arguments: '{}' },
|
|
4081
|
+
},
|
|
4082
|
+
]));
|
|
4083
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Found matches'));
|
|
4084
|
+
const result = await agent.chat('Search files', createTestContext());
|
|
4085
|
+
const history = result.conversationHistory;
|
|
4086
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of Grep stored'));
|
|
4087
|
+
expect(storageMessage).toBeDefined();
|
|
4088
|
+
expect(storageMessage.content).toContain('Found 5 match(es)');
|
|
4089
|
+
expect(storageMessage.content).toContain('in 3 file(s)');
|
|
4090
|
+
});
|
|
4091
|
+
it('should include Glob count summary in stored result message', async () => {
|
|
4092
|
+
const agent = createAgent(createTestConfig({
|
|
4093
|
+
tools: [createGlobLikeResultTool()],
|
|
4094
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4095
|
+
stream: false,
|
|
4096
|
+
}));
|
|
4097
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4098
|
+
{
|
|
4099
|
+
id: 'call-glob-1',
|
|
4100
|
+
type: 'function',
|
|
4101
|
+
function: { name: 'Glob', arguments: '{}' },
|
|
4102
|
+
},
|
|
4103
|
+
]));
|
|
4104
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Found files'));
|
|
4105
|
+
const result = await agent.chat('Find files', createTestContext());
|
|
4106
|
+
const history = result.conversationHistory;
|
|
4107
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of Glob stored'));
|
|
4108
|
+
expect(storageMessage).toBeDefined();
|
|
4109
|
+
expect(storageMessage.content).toContain('Found 42 file(s)');
|
|
4110
|
+
});
|
|
4111
|
+
it('should include Read total lines summary in stored result message', async () => {
|
|
4112
|
+
const agent = createAgent(createTestConfig({
|
|
4113
|
+
tools: [createReadLikeResultTool()],
|
|
4114
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4115
|
+
stream: false,
|
|
4116
|
+
}));
|
|
4117
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4118
|
+
{
|
|
4119
|
+
id: 'call-read-1',
|
|
4120
|
+
type: 'function',
|
|
4121
|
+
function: { name: 'Read', arguments: '{}' },
|
|
4122
|
+
},
|
|
4123
|
+
]));
|
|
4124
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('File content'));
|
|
4125
|
+
const result = await agent.chat('Read file', createTestContext());
|
|
4126
|
+
const history = result.conversationHistory;
|
|
4127
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of Read stored'));
|
|
4128
|
+
expect(storageMessage).toBeDefined();
|
|
4129
|
+
expect(storageMessage.content).toContain('File has 500 total lines');
|
|
4130
|
+
});
|
|
4131
|
+
it('should not store small tool results', async () => {
|
|
4132
|
+
const smallTool = {
|
|
4133
|
+
declaration: {
|
|
4134
|
+
name: 'smallTool',
|
|
4135
|
+
description: 'Returns small result',
|
|
4136
|
+
parametersJsonSchema: { type: 'object', properties: {} },
|
|
4137
|
+
},
|
|
4138
|
+
executor: async () => ({ ok: true }),
|
|
4139
|
+
};
|
|
4140
|
+
const agent = createAgent(createTestConfig({
|
|
4141
|
+
tools: [smallTool],
|
|
4142
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50000 },
|
|
4143
|
+
stream: false,
|
|
4144
|
+
}));
|
|
4145
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4146
|
+
{
|
|
4147
|
+
id: 'call-small-1',
|
|
4148
|
+
type: 'function',
|
|
4149
|
+
function: { name: 'smallTool', arguments: '{}' },
|
|
4150
|
+
},
|
|
4151
|
+
]));
|
|
4152
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Done'));
|
|
4153
|
+
const result = await agent.chat('Do small thing', createTestContext());
|
|
4154
|
+
const history = result.conversationHistory;
|
|
4155
|
+
const resultMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of smallTool'));
|
|
4156
|
+
expect(resultMessage).toBeDefined();
|
|
4157
|
+
// Should be inline, not stored
|
|
4158
|
+
expect(resultMessage.content).not.toContain('stored (ID:');
|
|
4159
|
+
expect(resultMessage.content).toContain('Result of smallTool:');
|
|
4160
|
+
});
|
|
4161
|
+
it('should show large result grep tip for results with 500+ lines', async () => {
|
|
4162
|
+
// Create a tool that returns a very large result (500+ lines worth)
|
|
4163
|
+
const hugeResultTool = {
|
|
4164
|
+
declaration: {
|
|
4165
|
+
name: 'hugeResultTool',
|
|
4166
|
+
description: 'Returns a huge result',
|
|
4167
|
+
parametersJsonSchema: { type: 'object', properties: {} },
|
|
4168
|
+
},
|
|
4169
|
+
executor: async () => {
|
|
4170
|
+
// Return a result that will produce 500+ lines when JSON.stringified
|
|
4171
|
+
return {
|
|
4172
|
+
lines: Array.from({ length: 600 }, (_, i) => `output line ${i + 1}`),
|
|
4173
|
+
};
|
|
4174
|
+
},
|
|
4175
|
+
};
|
|
4176
|
+
const agent = createAgent(createTestConfig({
|
|
4177
|
+
tools: [hugeResultTool],
|
|
4178
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4179
|
+
stream: false,
|
|
4180
|
+
}));
|
|
4181
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4182
|
+
{
|
|
4183
|
+
id: 'call-huge-1',
|
|
4184
|
+
type: 'function',
|
|
4185
|
+
function: { name: 'hugeResultTool', arguments: '{}' },
|
|
4186
|
+
},
|
|
4187
|
+
]));
|
|
4188
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Huge result processed'));
|
|
4189
|
+
const result = await agent.chat('Run huge tool', createTestContext());
|
|
4190
|
+
const history = result.conversationHistory;
|
|
4191
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of hugeResultTool stored'));
|
|
4192
|
+
expect(storageMessage).toBeDefined();
|
|
4193
|
+
// Should show the grep tip for large results
|
|
4194
|
+
expect(storageMessage.content).toContain('grep=');
|
|
4195
|
+
expect(storageMessage.content).toContain('Large result');
|
|
4196
|
+
});
|
|
4197
|
+
it('should include Bash failed exit code summary', async () => {
|
|
4198
|
+
const failedBashTool = {
|
|
4199
|
+
declaration: {
|
|
4200
|
+
name: 'Bash',
|
|
4201
|
+
description: 'Failed bash',
|
|
4202
|
+
parametersJsonSchema: { type: 'object', properties: {} },
|
|
4203
|
+
},
|
|
4204
|
+
executor: async () => ({
|
|
4205
|
+
exit_code: 1,
|
|
4206
|
+
success: false,
|
|
4207
|
+
stderr: 'x'.repeat(300),
|
|
4208
|
+
}),
|
|
4209
|
+
};
|
|
4210
|
+
const agent = createAgent(createTestConfig({
|
|
4211
|
+
tools: [failedBashTool],
|
|
4212
|
+
resultStorage: { enabled: true, sizeThresholdBytes: 50 },
|
|
4213
|
+
stream: false,
|
|
4214
|
+
}));
|
|
4215
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4216
|
+
{
|
|
4217
|
+
id: 'call-bash-fail',
|
|
4218
|
+
type: 'function',
|
|
4219
|
+
function: { name: 'Bash', arguments: '{}' },
|
|
4220
|
+
},
|
|
4221
|
+
]));
|
|
4222
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Command failed'));
|
|
4223
|
+
const result = await agent.chat('Run bash', createTestContext());
|
|
4224
|
+
const history = result.conversationHistory;
|
|
4225
|
+
const storageMessage = history.find((m) => m.role === 'tool-call-results' && m.content?.includes('Result of Bash stored'));
|
|
4226
|
+
expect(storageMessage).toBeDefined();
|
|
4227
|
+
expect(storageMessage.content).toContain('Exit code: 1');
|
|
4228
|
+
expect(storageMessage.content).toContain('(failed)');
|
|
4229
|
+
});
|
|
4230
|
+
it('should handle result storage disabled', async () => {
|
|
4231
|
+
const agent = createAgent(createTestConfig({
|
|
4232
|
+
resultStorage: { enabled: false },
|
|
4233
|
+
stream: false,
|
|
4234
|
+
}));
|
|
4235
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse(null, [
|
|
4236
|
+
{
|
|
4237
|
+
id: 'call-1',
|
|
4238
|
+
type: 'function',
|
|
4239
|
+
function: { name: 'getWeather', arguments: JSON.stringify({ city: 'London' }) },
|
|
4240
|
+
},
|
|
4241
|
+
]));
|
|
4242
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Weather is sunny'));
|
|
4243
|
+
const result = await agent.chat('Weather?', createTestContext());
|
|
4244
|
+
expect(result.message).toBe('Weather is sunny');
|
|
4245
|
+
// Tool result should be inline
|
|
4246
|
+
const toolResult = result.conversationHistory.find((m) => m.role === 'tool-call-results');
|
|
4247
|
+
expect(toolResult).toBeDefined();
|
|
4248
|
+
expect(toolResult.content).toContain('Result of getWeather:');
|
|
4249
|
+
});
|
|
4250
|
+
});
|
|
4251
|
+
// ============================================================================
|
|
4252
|
+
// 1C. AgentError.toJSON() (errors.ts coverage)
|
|
4253
|
+
// ============================================================================
|
|
4254
|
+
describe('AgentError', () => {
|
|
4255
|
+
it('should serialize to JSON with toJSON()', () => {
|
|
4256
|
+
const error = new AgentError('EMPTY_RESPONSE', 'Custom empty message');
|
|
4257
|
+
const json = error.toJSON();
|
|
4258
|
+
expect(json).toEqual({
|
|
4259
|
+
code: 'EMPTY_RESPONSE',
|
|
4260
|
+
message: 'Custom empty message',
|
|
4261
|
+
recoverable: false,
|
|
4262
|
+
});
|
|
4263
|
+
});
|
|
4264
|
+
it('should use default message when no custom message provided', () => {
|
|
4265
|
+
const error = new AgentError('GENERIC_ERROR');
|
|
4266
|
+
expect(error.message).toBe('An internal error occurred, please try again later');
|
|
4267
|
+
const json = error.toJSON();
|
|
4268
|
+
expect(json.code).toBe('GENERIC_ERROR');
|
|
4269
|
+
expect(json.recoverable).toBe(false);
|
|
4270
|
+
});
|
|
4271
|
+
it('should support recoverable flag', () => {
|
|
4272
|
+
const error = new AgentError('MAX_ITERATIONS', undefined, true);
|
|
4273
|
+
const json = error.toJSON();
|
|
4274
|
+
expect(json).toEqual({
|
|
4275
|
+
code: 'MAX_ITERATIONS',
|
|
4276
|
+
message: 'Maximum iterations reached. Please break down your request into smaller parts.',
|
|
4277
|
+
recoverable: true,
|
|
4278
|
+
});
|
|
4279
|
+
});
|
|
4280
|
+
it('should be an instance of Error', () => {
|
|
4281
|
+
const error = new AgentError('API_KEY_REQUIRED');
|
|
4282
|
+
expect(error).toBeInstanceOf(Error);
|
|
4283
|
+
expect(error.name).toBe('AgentError');
|
|
4284
|
+
});
|
|
4285
|
+
});
|
|
4286
|
+
// ============================================================================
|
|
4287
|
+
// 1D. Error utilities (errors.ts coverage)
|
|
4288
|
+
// ============================================================================
|
|
4289
|
+
describe('Error utilities', () => {
|
|
4290
|
+
describe('formatErrorTrace', () => {
|
|
4291
|
+
it('should wrap error in INTERNAL_ERROR_TRACE tags', () => {
|
|
4292
|
+
const error = new Error('Something went wrong');
|
|
4293
|
+
const trace = formatErrorTrace(error);
|
|
4294
|
+
expect(trace).toContain('<INTERNAL_ERROR_TRACE>');
|
|
4295
|
+
expect(trace).toContain('</INTERNAL_ERROR_TRACE>');
|
|
4296
|
+
expect(trace).toContain('Error: Something went wrong');
|
|
4297
|
+
});
|
|
4298
|
+
it('should include the error name', () => {
|
|
4299
|
+
const error = new TypeError('Invalid type');
|
|
4300
|
+
const trace = formatErrorTrace(error);
|
|
4301
|
+
expect(trace).toContain('TypeError: Invalid type');
|
|
4302
|
+
});
|
|
4303
|
+
});
|
|
4304
|
+
describe('isAbortError', () => {
|
|
4305
|
+
it('should return true for AbortError name', () => {
|
|
4306
|
+
const error = new Error('Operation aborted');
|
|
4307
|
+
error.name = 'AbortError';
|
|
4308
|
+
expect(isAbortError(error)).toBe(true);
|
|
4309
|
+
});
|
|
4310
|
+
it('should return true for abort message', () => {
|
|
4311
|
+
const error = new Error('Request aborted before processing');
|
|
4312
|
+
expect(isAbortError(error)).toBe(true);
|
|
4313
|
+
});
|
|
4314
|
+
it('should return false for non-abort error', () => {
|
|
4315
|
+
const error = new Error('Something else');
|
|
4316
|
+
expect(isAbortError(error)).toBe(false);
|
|
4317
|
+
});
|
|
4318
|
+
it('should return false for non-Error objects', () => {
|
|
4319
|
+
expect(isAbortError('not an error')).toBe(false);
|
|
4320
|
+
expect(isAbortError(null)).toBe(false);
|
|
4321
|
+
expect(isAbortError(undefined)).toBe(false);
|
|
4322
|
+
});
|
|
4323
|
+
});
|
|
4324
|
+
describe('formatAIError', () => {
|
|
4325
|
+
it('should format error with tool name and suggestion', () => {
|
|
4326
|
+
const message = formatAIError(AI_ERROR_CATEGORIES.TOOL_EXECUTION_ERROR, 'File not found', {
|
|
4327
|
+
toolName: 'Read',
|
|
4328
|
+
suggestion: 'Check the file path',
|
|
4329
|
+
});
|
|
4330
|
+
expect(message).toBe('[TOOL_EXECUTION_ERROR] for tool "Read": File not found\n\nSuggestion: Check the file path');
|
|
4331
|
+
});
|
|
4332
|
+
it('should format error without tool name', () => {
|
|
4333
|
+
const message = formatAIError(AI_ERROR_CATEGORIES.RESPONSE_VALIDATION, 'Invalid JSON');
|
|
4334
|
+
expect(message).toBe('[RESPONSE_VALIDATION]: Invalid JSON');
|
|
4335
|
+
});
|
|
4336
|
+
it('should format error without suggestion', () => {
|
|
4337
|
+
const message = formatAIError(AI_ERROR_CATEGORIES.TOOL_NOT_FOUND, 'Tool "xyz" not found', {
|
|
4338
|
+
toolName: 'xyz',
|
|
4339
|
+
});
|
|
4340
|
+
expect(message).toBe('[TOOL_NOT_FOUND] for tool "xyz": Tool "xyz" not found');
|
|
4341
|
+
});
|
|
4342
|
+
});
|
|
4343
|
+
describe('ToolExecutionTimeoutError', () => {
|
|
4344
|
+
it('should contain tool name and timeout', () => {
|
|
4345
|
+
const error = new ToolExecutionTimeoutError('myTool', 5000);
|
|
4346
|
+
expect(error.message).toContain('myTool');
|
|
4347
|
+
expect(error.message).toContain('5000');
|
|
4348
|
+
expect(error.toolName).toBe('myTool');
|
|
4349
|
+
expect(error.timeoutMs).toBe(5000);
|
|
4350
|
+
expect(error).toBeInstanceOf(Error);
|
|
4351
|
+
});
|
|
4352
|
+
});
|
|
4353
|
+
});
|
|
4354
|
+
// ============================================================================
|
|
4355
|
+
// 1E. API key format warning (openrouter-client.ts coverage)
|
|
4356
|
+
// ============================================================================
|
|
4357
|
+
describe('createAgent - API key format warning', () => {
|
|
4358
|
+
let mockClient;
|
|
4359
|
+
beforeEach(() => {
|
|
4360
|
+
mockClient = createMockOpenAIClient();
|
|
4361
|
+
vi.mocked(OpenAI).mockImplementation(() => mockClient);
|
|
4362
|
+
});
|
|
4363
|
+
afterEach(() => {
|
|
4364
|
+
vi.clearAllMocks();
|
|
4365
|
+
});
|
|
4366
|
+
it('should warn for non-OpenRouter API key format', () => {
|
|
4367
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
4368
|
+
createAgent(createTestConfig({ apiKey: 'some-other-key' }));
|
|
4369
|
+
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('does not appear to be a valid OpenRouter API key'));
|
|
4370
|
+
consoleSpy.mockRestore();
|
|
4371
|
+
});
|
|
4372
|
+
it('should not warn for valid OpenRouter API key format', () => {
|
|
4373
|
+
const consoleSpy = vi.spyOn(console, 'warn').mockImplementation(() => { });
|
|
4374
|
+
createAgent(createTestConfig({ apiKey: 'sk-or-v1-test-key-12345' }));
|
|
4375
|
+
expect(consoleSpy).not.toHaveBeenCalled();
|
|
4376
|
+
consoleSpy.mockRestore();
|
|
4377
|
+
});
|
|
4378
|
+
});
|
|
4379
|
+
// ============================================================================
|
|
4380
|
+
// 1F. Session abort lifecycle (session.ts coverage)
|
|
4381
|
+
// ============================================================================
|
|
4382
|
+
describe('createAgent - session abort lifecycle', () => {
|
|
4383
|
+
let mockClient;
|
|
4384
|
+
beforeEach(() => {
|
|
4385
|
+
mockClient = createMockOpenAIClient();
|
|
4386
|
+
vi.mocked(OpenAI).mockImplementation(() => mockClient);
|
|
4387
|
+
});
|
|
4388
|
+
afterEach(() => {
|
|
4389
|
+
vi.clearAllMocks();
|
|
4390
|
+
});
|
|
4391
|
+
it('should allow chat after aborting when no active chat', async () => {
|
|
4392
|
+
const agent = createAgent(createTestConfig());
|
|
4393
|
+
// Abort before any chat (no active AbortController)
|
|
4394
|
+
agent.abort();
|
|
4395
|
+
// Subsequent chat should still work
|
|
4396
|
+
mockClient.chat.completions.create.mockResolvedValueOnce(createMockResponse('Hello after abort'));
|
|
4397
|
+
const result = await agent.chat('Hello', createTestContext());
|
|
4398
|
+
expect(result.message).toBe('Hello after abort');
|
|
4399
|
+
});
|
|
4400
|
+
it('should fire onAborted callback during active chat abort', async () => {
|
|
4401
|
+
// Use real timers for this test
|
|
4402
|
+
vi.useRealTimers();
|
|
4403
|
+
const agent = createAgent(createTestConfig());
|
|
4404
|
+
// Make the API call hang long enough for us to abort
|
|
4405
|
+
mockClient.chat.completions.create.mockImplementation(() => new Promise((_, reject) => {
|
|
4406
|
+
setTimeout(() => {
|
|
4407
|
+
const error = new Error('Request aborted before processing');
|
|
4408
|
+
error.name = 'AbortError';
|
|
4409
|
+
reject(error);
|
|
4410
|
+
}, 50);
|
|
4411
|
+
}));
|
|
4412
|
+
const onAborted = vi.fn();
|
|
4413
|
+
// Start chat and abort after a short delay
|
|
4414
|
+
const chatPromise = agent.chat('Hello', createTestContext(), { onAborted });
|
|
4415
|
+
// Abort mid-execution
|
|
4416
|
+
setTimeout(() => agent.abort(), 10);
|
|
4417
|
+
try {
|
|
4418
|
+
await chatPromise;
|
|
4419
|
+
}
|
|
4420
|
+
catch {
|
|
4421
|
+
// Expected to throw
|
|
4422
|
+
}
|
|
4423
|
+
expect(onAborted).toHaveBeenCalled();
|
|
4424
|
+
// Restore fake timers for other tests
|
|
4425
|
+
vi.useFakeTimers();
|
|
4426
|
+
});
|
|
4427
|
+
});
|
|
4012
4428
|
//# sourceMappingURL=agent.test.js.map
|