@elizaos/core 1.5.4 → 1.5.5
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/README.md +0 -15
- package/dist/index.d.ts +4886 -3
- package/dist/index.js +5287 -4
- package/package.json +21 -39
- package/dist/browser/index.browser.js +0 -1109
- package/dist/browser/index.browser.js.map +0 -707
- package/dist/browser/index.d.ts +0 -3
- package/dist/node/index.d.ts +0 -3
- package/dist/node/index.node.js +0 -74530
- package/dist/node/index.node.js.map +0 -1027
- package/src/__tests__/action-chaining-simple.test.ts +0 -203
- package/src/__tests__/actions.test.ts +0 -218
- package/src/__tests__/buffer.test.ts +0 -337
- package/src/__tests__/character-validation.test.ts +0 -309
- package/src/__tests__/database.test.ts +0 -750
- package/src/__tests__/entities.test.ts +0 -727
- package/src/__tests__/env.test.ts +0 -23
- package/src/__tests__/environment.test.ts +0 -285
- package/src/__tests__/logger-browser-node.test.ts +0 -716
- package/src/__tests__/logger.test.ts +0 -403
- package/src/__tests__/messages.test.ts +0 -196
- package/src/__tests__/mockCharacter.ts +0 -544
- package/src/__tests__/parsing.test.ts +0 -58
- package/src/__tests__/prompts.test.ts +0 -159
- package/src/__tests__/roles.test.ts +0 -331
- package/src/__tests__/runtime-embedding.test.ts +0 -343
- package/src/__tests__/runtime.test.ts +0 -978
- package/src/__tests__/search.test.ts +0 -15
- package/src/__tests__/services-by-type.test.ts +0 -204
- package/src/__tests__/services.test.ts +0 -136
- package/src/__tests__/settings.test.ts +0 -810
- package/src/__tests__/utils.test.ts +0 -1105
- package/src/__tests__/uuid.test.ts +0 -94
- package/src/actions.ts +0 -122
- package/src/database.ts +0 -579
- package/src/entities.ts +0 -406
- package/src/index.browser.ts +0 -48
- package/src/index.node.ts +0 -39
- package/src/index.ts +0 -50
- package/src/logger.ts +0 -527
- package/src/prompts.ts +0 -243
- package/src/roles.ts +0 -85
- package/src/runtime.ts +0 -2514
- package/src/schemas/character.ts +0 -149
- package/src/search.ts +0 -1543
- package/src/sentry/instrument.browser.ts +0 -65
- package/src/sentry/instrument.node.ts +0 -57
- package/src/sentry/instrument.ts +0 -82
- package/src/services.ts +0 -105
- package/src/settings.ts +0 -409
- package/src/test_resources/constants.ts +0 -12
- package/src/test_resources/testSetup.ts +0 -21
- package/src/test_resources/types.ts +0 -22
- package/src/types/agent.ts +0 -112
- package/src/types/browser.ts +0 -145
- package/src/types/components.ts +0 -184
- package/src/types/database.ts +0 -348
- package/src/types/email.ts +0 -162
- package/src/types/environment.ts +0 -129
- package/src/types/events.ts +0 -249
- package/src/types/index.ts +0 -29
- package/src/types/knowledge.ts +0 -65
- package/src/types/lp.ts +0 -124
- package/src/types/memory.ts +0 -228
- package/src/types/message.ts +0 -233
- package/src/types/messaging.ts +0 -57
- package/src/types/model.ts +0 -359
- package/src/types/pdf.ts +0 -77
- package/src/types/plugin.ts +0 -78
- package/src/types/post.ts +0 -271
- package/src/types/primitives.ts +0 -97
- package/src/types/runtime.ts +0 -190
- package/src/types/service.ts +0 -198
- package/src/types/settings.ts +0 -30
- package/src/types/state.ts +0 -60
- package/src/types/task.ts +0 -72
- package/src/types/tee.ts +0 -107
- package/src/types/testing.ts +0 -30
- package/src/types/token.ts +0 -96
- package/src/types/transcription.ts +0 -133
- package/src/types/video.ts +0 -108
- package/src/types/wallet.ts +0 -56
- package/src/types/web-search.ts +0 -146
- package/src/utils/__tests__/buffer.test.ts +0 -80
- package/src/utils/__tests__/environment.test.ts +0 -58
- package/src/utils/__tests__/stringToUuid.test.ts +0 -88
- package/src/utils/buffer.ts +0 -312
- package/src/utils/environment.ts +0 -316
- package/src/utils/server-health.ts +0 -117
- package/src/utils.ts +0 -1076
|
@@ -1,1105 +0,0 @@
|
|
|
1
|
-
import { afterEach, beforeEach, describe, expect, it, setSystemTime, mock } from 'bun:test';
|
|
2
|
-
import { Content, Entity, IAgentRuntime, Memory, ModelType, State } from '../types';
|
|
3
|
-
import * as utils from '../utils';
|
|
4
|
-
import {
|
|
5
|
-
addHeader,
|
|
6
|
-
composePrompt,
|
|
7
|
-
composePromptFromState,
|
|
8
|
-
formatMessages,
|
|
9
|
-
formatPosts,
|
|
10
|
-
formatTimestamp,
|
|
11
|
-
normalizeJsonString,
|
|
12
|
-
parseBooleanFromText,
|
|
13
|
-
parseJSONObjectFromText,
|
|
14
|
-
parseKeyValueXml,
|
|
15
|
-
safeReplacer,
|
|
16
|
-
splitChunks,
|
|
17
|
-
stringToUuid,
|
|
18
|
-
trimTokens,
|
|
19
|
-
truncateToCompleteSentence,
|
|
20
|
-
validateUuid,
|
|
21
|
-
} from '../utils';
|
|
22
|
-
|
|
23
|
-
describe('Utils Comprehensive Tests', () => {
|
|
24
|
-
describe('parseBooleanFromText', () => {
|
|
25
|
-
it('should return true for affirmative values', () => {
|
|
26
|
-
const affirmativeValues = [
|
|
27
|
-
'YES',
|
|
28
|
-
'Y',
|
|
29
|
-
'TRUE',
|
|
30
|
-
'T',
|
|
31
|
-
'1',
|
|
32
|
-
'ON',
|
|
33
|
-
'ENABLE',
|
|
34
|
-
'yes',
|
|
35
|
-
'y',
|
|
36
|
-
'true',
|
|
37
|
-
't',
|
|
38
|
-
' YES ',
|
|
39
|
-
' true ',
|
|
40
|
-
];
|
|
41
|
-
|
|
42
|
-
affirmativeValues.forEach((value) => {
|
|
43
|
-
expect(parseBooleanFromText(value)).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should return false for negative values', () => {
|
|
48
|
-
const negativeValues = [
|
|
49
|
-
'NO',
|
|
50
|
-
'N',
|
|
51
|
-
'FALSE',
|
|
52
|
-
'F',
|
|
53
|
-
'0',
|
|
54
|
-
'OFF',
|
|
55
|
-
'DISABLE',
|
|
56
|
-
'no',
|
|
57
|
-
'n',
|
|
58
|
-
'false',
|
|
59
|
-
'f',
|
|
60
|
-
' NO ',
|
|
61
|
-
' false ',
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
negativeValues.forEach((value) => {
|
|
65
|
-
expect(parseBooleanFromText(value)).toBe(false);
|
|
66
|
-
});
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('should return false for null, undefined, or empty values', () => {
|
|
70
|
-
expect(parseBooleanFromText(null)).toBe(false);
|
|
71
|
-
expect(parseBooleanFromText(undefined)).toBe(false);
|
|
72
|
-
expect(parseBooleanFromText('')).toBe(false);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('should return false for unrecognized values', () => {
|
|
76
|
-
expect(parseBooleanFromText('maybe')).toBe(false);
|
|
77
|
-
expect(parseBooleanFromText('123')).toBe(false);
|
|
78
|
-
expect(parseBooleanFromText('YESNO')).toBe(false);
|
|
79
|
-
expect(parseBooleanFromText('random')).toBe(false);
|
|
80
|
-
});
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
describe('formatTimestamp', () => {
|
|
84
|
-
beforeEach(() => {
|
|
85
|
-
// Mock Date.now() to ensure consistent tests
|
|
86
|
-
setSystemTime(new Date('2024-01-15T12:00:00Z'));
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
afterEach(() => {
|
|
90
|
-
setSystemTime(); // Reset to real time
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it("should return 'just now' for recent timestamps", () => {
|
|
94
|
-
const now = Date.now();
|
|
95
|
-
expect(formatTimestamp(now)).toBe('just now');
|
|
96
|
-
expect(formatTimestamp(now - 30000)).toBe('just now'); // 30 seconds ago
|
|
97
|
-
expect(formatTimestamp(now - 59999)).toBe('just now'); // Just under 1 minute
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('should return minutes ago for timestamps within an hour', () => {
|
|
101
|
-
const now = Date.now();
|
|
102
|
-
expect(formatTimestamp(now - 60000)).toBe('1 minute ago');
|
|
103
|
-
expect(formatTimestamp(now - 120000)).toBe('2 minutes ago');
|
|
104
|
-
expect(formatTimestamp(now - 1800000)).toBe('30 minutes ago');
|
|
105
|
-
expect(formatTimestamp(now - 3599000)).toBe('59 minutes ago');
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should return hours ago for timestamps within 24 hours', () => {
|
|
109
|
-
const now = Date.now();
|
|
110
|
-
expect(formatTimestamp(now - 3600000)).toBe('1 hour ago');
|
|
111
|
-
expect(formatTimestamp(now - 7200000)).toBe('2 hours ago');
|
|
112
|
-
expect(formatTimestamp(now - 43200000)).toBe('12 hours ago');
|
|
113
|
-
expect(formatTimestamp(now - 86399000)).toBe('23 hours ago');
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
it('should return days ago for older timestamps', () => {
|
|
117
|
-
const now = Date.now();
|
|
118
|
-
expect(formatTimestamp(now - 86400000)).toBe('1 day ago');
|
|
119
|
-
expect(formatTimestamp(now - 172800000)).toBe('2 days ago');
|
|
120
|
-
expect(formatTimestamp(now - 604800000)).toBe('7 days ago');
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('parseJSONObjectFromText', () => {
|
|
125
|
-
it('should parse JSON from code blocks', () => {
|
|
126
|
-
const text = 'Here is some JSON:\n```json\n{"key": "value", "number": 42}\n```';
|
|
127
|
-
const result = parseJSONObjectFromText(text);
|
|
128
|
-
// Note: normalizeJsonString converts numbers to strings
|
|
129
|
-
expect(result).toEqual({ key: 'value', number: '42' });
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('should parse direct JSON without code blocks', () => {
|
|
133
|
-
const text = '{"name": "Alice", "age": 30}';
|
|
134
|
-
const result = parseJSONObjectFromText(text);
|
|
135
|
-
// Note: normalizeJsonString converts numbers to strings
|
|
136
|
-
expect(result).toEqual({ name: 'Alice', age: '30' });
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should return null for invalid JSON', () => {
|
|
140
|
-
expect(parseJSONObjectFromText('not json')).toBeNull();
|
|
141
|
-
expect(parseJSONObjectFromText('{invalid json}')).toBeNull();
|
|
142
|
-
expect(parseJSONObjectFromText('')).toBeNull();
|
|
143
|
-
});
|
|
144
|
-
|
|
145
|
-
it('should return null for arrays', () => {
|
|
146
|
-
expect(parseJSONObjectFromText('["item1", "item2"]')).toBeNull();
|
|
147
|
-
expect(parseJSONObjectFromText('```json\n[1, 2, 3]\n```')).toBeNull();
|
|
148
|
-
});
|
|
149
|
-
|
|
150
|
-
it('should return null for non-object values', () => {
|
|
151
|
-
expect(parseJSONObjectFromText('"string"')).toBeNull();
|
|
152
|
-
expect(parseJSONObjectFromText('42')).toBeNull();
|
|
153
|
-
expect(parseJSONObjectFromText('true')).toBeNull();
|
|
154
|
-
expect(parseJSONObjectFromText('null')).toBeNull();
|
|
155
|
-
});
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
describe('normalizeJsonString', () => {
|
|
159
|
-
it('should remove extra spaces after { and before }', () => {
|
|
160
|
-
// Note: normalizeJsonString doesn't handle unquoted keys
|
|
161
|
-
expect(normalizeJsonString('{ "key": "value" }')).toBe('{"key": "value"}');
|
|
162
|
-
expect(normalizeJsonString('{\n "key": "value"\n}')).toBe('{"key": "value"}');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
it('should wrap unquoted values in double quotes', () => {
|
|
166
|
-
expect(normalizeJsonString('{"key": value}')).toBe('{"key": "value"}');
|
|
167
|
-
expect(normalizeJsonString('{"key": someWord}')).toBe('{"key": "someWord"}');
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it('should convert single quotes to double quotes', () => {
|
|
171
|
-
// The function only converts when format is exactly "key": 'value'
|
|
172
|
-
expect(normalizeJsonString('"key": \'value\'')).toBe('"key": "value"');
|
|
173
|
-
// When already inside JSON braces, single quotes inside are preserved
|
|
174
|
-
const result = normalizeJsonString('{"key": \'value\', "key2": \'value2\'}');
|
|
175
|
-
// The function converts the quotes but may not work perfectly for nested quotes
|
|
176
|
-
expect(result).toContain('key');
|
|
177
|
-
expect(result).toContain('value');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('should handle mixed formatting', () => {
|
|
181
|
-
const input = '{ "key1": value1, "key2": \'value2\', "key3": "value3" }';
|
|
182
|
-
const result = normalizeJsonString(input);
|
|
183
|
-
// Check that the function at least wraps unquoted values
|
|
184
|
-
expect(result).toContain('"key1": "value1"');
|
|
185
|
-
expect(result).toContain('"key3": "value3"');
|
|
186
|
-
});
|
|
187
|
-
|
|
188
|
-
it('should wrap numbers in quotes', () => {
|
|
189
|
-
// normalizeJsonString wraps ALL unquoted values including numbers
|
|
190
|
-
const input = '{"key": "value", "number": 42}';
|
|
191
|
-
const result = normalizeJsonString(input);
|
|
192
|
-
expect(result).toBe('{"key": "value", "number": "42"}');
|
|
193
|
-
});
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
describe('truncateToCompleteSentence', () => {
|
|
197
|
-
it('should return text unchanged if within limit', () => {
|
|
198
|
-
const text = 'Short text.';
|
|
199
|
-
expect(truncateToCompleteSentence(text, 50)).toBe(text);
|
|
200
|
-
});
|
|
201
|
-
|
|
202
|
-
it('should truncate at last period within limit', () => {
|
|
203
|
-
const text = 'First sentence. Second sentence. Third sentence that is very long.';
|
|
204
|
-
expect(truncateToCompleteSentence(text, 35)).toBe('First sentence. Second sentence.');
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should truncate at last space if no period found', () => {
|
|
208
|
-
const text = 'This is a very long sentence without any periods that needs truncation';
|
|
209
|
-
expect(truncateToCompleteSentence(text, 30)).toBe('This is a very long sentence...');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
it('should hard truncate if no space found', () => {
|
|
213
|
-
const text = 'Verylongwordwithoutanyspacesorperiods';
|
|
214
|
-
// maxLength 10 - 3 = 7 chars for the text part
|
|
215
|
-
expect(truncateToCompleteSentence(text, 10)).toBe('Verylon...');
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('should handle edge cases', () => {
|
|
219
|
-
expect(truncateToCompleteSentence('', 10)).toBe('');
|
|
220
|
-
expect(truncateToCompleteSentence('No period', 5)).toBe('No...');
|
|
221
|
-
});
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
describe('splitChunks', () => {
|
|
225
|
-
it('should split text into chunks', async () => {
|
|
226
|
-
const text = 'a'.repeat(2000); // Long text
|
|
227
|
-
const chunks = await splitChunks(text, 512, 20);
|
|
228
|
-
|
|
229
|
-
expect(chunks).toBeInstanceOf(Array);
|
|
230
|
-
expect(chunks.length).toBeGreaterThan(0);
|
|
231
|
-
chunks.forEach((chunk) => {
|
|
232
|
-
expect(chunk.length).toBeLessThanOrEqual(512 * 4); // Accounting for character to token ratio
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
it('should handle short text', async () => {
|
|
237
|
-
const text = 'Short text';
|
|
238
|
-
const chunks = await splitChunks(text);
|
|
239
|
-
|
|
240
|
-
expect(chunks).toHaveLength(1);
|
|
241
|
-
expect(chunks[0]).toBe(text);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('should respect chunk overlap', async () => {
|
|
245
|
-
const text = 'Word1 Word2 Word3 Word4 Word5 ' + 'Word6 '.repeat(100);
|
|
246
|
-
const chunks = await splitChunks(text, 50, 10);
|
|
247
|
-
|
|
248
|
-
expect(chunks.length).toBeGreaterThan(1);
|
|
249
|
-
});
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
describe('trimTokens', () => {
|
|
253
|
-
let mockRuntime: IAgentRuntime;
|
|
254
|
-
|
|
255
|
-
beforeEach(() => {
|
|
256
|
-
mockRuntime = {
|
|
257
|
-
useModel: mock(async (type, params) => {
|
|
258
|
-
if (type === 'TEXT_TOKENIZER_ENCODE') {
|
|
259
|
-
// Simple mock: each word is a token
|
|
260
|
-
return params.prompt.split(' ');
|
|
261
|
-
}
|
|
262
|
-
if (type === 'TEXT_TOKENIZER_DECODE') {
|
|
263
|
-
// Simple mock: join tokens back
|
|
264
|
-
return params.tokens.join(' ');
|
|
265
|
-
}
|
|
266
|
-
return null;
|
|
267
|
-
}),
|
|
268
|
-
} as unknown as IAgentRuntime;
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('should trim tokens to specified limit', async () => {
|
|
272
|
-
const prompt = 'one two three four five six seven eight nine ten';
|
|
273
|
-
const result = await trimTokens(prompt, 5, mockRuntime);
|
|
274
|
-
|
|
275
|
-
expect(result).toBe('six seven eight nine ten');
|
|
276
|
-
expect(mockRuntime.useModel).toHaveBeenCalledTimes(2);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
it('should return unchanged if within limit', async () => {
|
|
280
|
-
const prompt = 'short text';
|
|
281
|
-
const result = await trimTokens(prompt, 10, mockRuntime);
|
|
282
|
-
|
|
283
|
-
expect(result).toBe(prompt);
|
|
284
|
-
});
|
|
285
|
-
|
|
286
|
-
it('should throw error for invalid inputs', async () => {
|
|
287
|
-
await expect(trimTokens('', 10, mockRuntime)).rejects.toThrow();
|
|
288
|
-
await expect(trimTokens('text', 0, mockRuntime)).rejects.toThrow();
|
|
289
|
-
await expect(trimTokens('text', -1, mockRuntime)).rejects.toThrow();
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
it('should skip tokenization for very short prompts', async () => {
|
|
293
|
-
const prompt = 'hi';
|
|
294
|
-
const result = await trimTokens(prompt, 100, mockRuntime);
|
|
295
|
-
|
|
296
|
-
expect(result).toBe(prompt);
|
|
297
|
-
expect(mockRuntime.useModel).not.toHaveBeenCalled();
|
|
298
|
-
});
|
|
299
|
-
});
|
|
300
|
-
|
|
301
|
-
describe('parseKeyValueXml', () => {
|
|
302
|
-
it('should parse response XML blocks', () => {
|
|
303
|
-
const xml = `<response>
|
|
304
|
-
<name>TestAgent</name>
|
|
305
|
-
<reasoning>Test reasoning</reasoning>
|
|
306
|
-
<action>RESPOND</action>
|
|
307
|
-
</response>`;
|
|
308
|
-
|
|
309
|
-
const result = parseKeyValueXml(xml);
|
|
310
|
-
expect(result).toEqual({
|
|
311
|
-
name: 'TestAgent',
|
|
312
|
-
reasoning: 'Test reasoning',
|
|
313
|
-
action: 'RESPOND',
|
|
314
|
-
});
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it('should handle comma-separated lists for specific keys', () => {
|
|
318
|
-
const xml = `<response>
|
|
319
|
-
<actions>ACTION1,ACTION2,ACTION3</actions>
|
|
320
|
-
<providers>PROVIDER1, PROVIDER2</providers>
|
|
321
|
-
<evaluators>EVAL1, EVAL2</evaluators>
|
|
322
|
-
</response>`;
|
|
323
|
-
|
|
324
|
-
const result = parseKeyValueXml(xml);
|
|
325
|
-
expect(result).toEqual({
|
|
326
|
-
actions: ['ACTION1', 'ACTION2', 'ACTION3'],
|
|
327
|
-
providers: ['PROVIDER1', 'PROVIDER2'],
|
|
328
|
-
evaluators: ['EVAL1', 'EVAL2'],
|
|
329
|
-
});
|
|
330
|
-
});
|
|
331
|
-
|
|
332
|
-
it('should parse boolean values', () => {
|
|
333
|
-
const xml = '<response><simple>true</simple><complex>false</complex></response>';
|
|
334
|
-
|
|
335
|
-
const result = parseKeyValueXml(xml);
|
|
336
|
-
expect(result).toEqual({
|
|
337
|
-
simple: true,
|
|
338
|
-
complex: 'false', // Only 'simple' key is treated as boolean
|
|
339
|
-
});
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('should handle XML entities', () => {
|
|
343
|
-
const xml = '<response><text>Value with <tags> & entities</text></response>';
|
|
344
|
-
|
|
345
|
-
const result = parseKeyValueXml(xml);
|
|
346
|
-
expect(result).toEqual({
|
|
347
|
-
text: 'Value with <tags> & entities',
|
|
348
|
-
});
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('should return null for invalid XML', () => {
|
|
352
|
-
expect(parseKeyValueXml('')).toBeNull();
|
|
353
|
-
expect(parseKeyValueXml('not xml')).toBeNull();
|
|
354
|
-
expect(parseKeyValueXml('<unclosed>')).toBeNull();
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
it('should handle any root tag name', () => {
|
|
358
|
-
const xml = '<custom><key>value</key></custom>';
|
|
359
|
-
const result = parseKeyValueXml(xml);
|
|
360
|
-
expect(result).toEqual({ key: 'value' });
|
|
361
|
-
});
|
|
362
|
-
|
|
363
|
-
it('should handle mismatched XML tags', () => {
|
|
364
|
-
// This tests lines 393-395 - warning about mismatched tags
|
|
365
|
-
const xml = '<response><start>value</end></response>';
|
|
366
|
-
const result = parseKeyValueXml(xml);
|
|
367
|
-
// Should still try to parse what it can
|
|
368
|
-
expect(result).toBeNull();
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('should return null when no key-value pairs are found', () => {
|
|
372
|
-
// This tests lines 400-403 - no key-value pairs extracted
|
|
373
|
-
const xml = '<response></response>'; // Empty response
|
|
374
|
-
const result = parseKeyValueXml(xml);
|
|
375
|
-
expect(result).toBeNull();
|
|
376
|
-
|
|
377
|
-
// Also test with content that doesn't match the pattern
|
|
378
|
-
const xml2 = '<response>Just plain text</response>';
|
|
379
|
-
const result2 = parseKeyValueXml(xml2);
|
|
380
|
-
expect(result2).toBeNull();
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
it('should handle empty comma-separated lists', () => {
|
|
384
|
-
const xml = `<response>
|
|
385
|
-
<actions></actions>
|
|
386
|
-
<providers> </providers>
|
|
387
|
-
</response>`;
|
|
388
|
-
|
|
389
|
-
const result = parseKeyValueXml(xml);
|
|
390
|
-
expect(result).toEqual({
|
|
391
|
-
actions: [],
|
|
392
|
-
providers: [],
|
|
393
|
-
});
|
|
394
|
-
});
|
|
395
|
-
|
|
396
|
-
it('should correctly handle nested same-name tags in fallback scan', () => {
|
|
397
|
-
// No <response> tag so fallback scanner is used
|
|
398
|
-
const xml = '<container><text><text>inner</text>outer</text></container>';
|
|
399
|
-
const result = parseKeyValueXml(xml);
|
|
400
|
-
// Ensure we did not truncate at the inner </text>
|
|
401
|
-
expect(result).not.toBeNull();
|
|
402
|
-
expect(result!.text).toContain('outer');
|
|
403
|
-
expect(result!.text).toContain('<text>inner</text>');
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('should treat self-closing tags with whitespace as self-closing and skip them', () => {
|
|
407
|
-
// First tag is self-closing with whitespace before '/>'
|
|
408
|
-
const xml = '<sc /> <container><key>value</key></container>';
|
|
409
|
-
const result = parseKeyValueXml(xml);
|
|
410
|
-
expect(result).toEqual({ key: 'value' });
|
|
411
|
-
});
|
|
412
|
-
});
|
|
413
|
-
|
|
414
|
-
describe('formatMessages', () => {
|
|
415
|
-
const mockEntities: Entity[] = [
|
|
416
|
-
{
|
|
417
|
-
id: 'entity-1' as any,
|
|
418
|
-
names: ['Alice'],
|
|
419
|
-
agentId: 'agent-1' as any,
|
|
420
|
-
},
|
|
421
|
-
{
|
|
422
|
-
id: 'entity-2' as any,
|
|
423
|
-
names: ['Bob'],
|
|
424
|
-
agentId: 'agent-1' as any,
|
|
425
|
-
},
|
|
426
|
-
];
|
|
427
|
-
|
|
428
|
-
it('should format messages with basic content', () => {
|
|
429
|
-
const messages: Memory[] = [
|
|
430
|
-
{
|
|
431
|
-
id: 'msg-1' as any,
|
|
432
|
-
entityId: 'entity-1' as any,
|
|
433
|
-
roomId: 'room-1' as any,
|
|
434
|
-
createdAt: Date.now() - 60000, // 1 minute ago
|
|
435
|
-
content: {
|
|
436
|
-
text: 'Hello world',
|
|
437
|
-
source: 'chat',
|
|
438
|
-
} as Content,
|
|
439
|
-
},
|
|
440
|
-
];
|
|
441
|
-
|
|
442
|
-
const result = formatMessages({ messages, entities: mockEntities });
|
|
443
|
-
|
|
444
|
-
expect(result).toContain('Alice');
|
|
445
|
-
expect(result).toContain('Hello world');
|
|
446
|
-
expect(result).toContain('1 minute ago');
|
|
447
|
-
expect(result).toContain('[entity-1]');
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('should format messages with actions and thoughts', () => {
|
|
451
|
-
const messages: Memory[] = [
|
|
452
|
-
{
|
|
453
|
-
id: 'msg-1' as any,
|
|
454
|
-
entityId: 'entity-2' as any,
|
|
455
|
-
roomId: 'room-1' as any,
|
|
456
|
-
createdAt: Date.now(),
|
|
457
|
-
content: {
|
|
458
|
-
text: 'Doing something',
|
|
459
|
-
thought: 'I should help',
|
|
460
|
-
actions: ['SEARCH', 'REPLY'],
|
|
461
|
-
source: 'chat',
|
|
462
|
-
} as Content,
|
|
463
|
-
},
|
|
464
|
-
];
|
|
465
|
-
|
|
466
|
-
const result = formatMessages({ messages, entities: mockEntities });
|
|
467
|
-
|
|
468
|
-
expect(result).toContain('Bob');
|
|
469
|
-
expect(result).toContain('Doing something');
|
|
470
|
-
expect(result).toContain("(Bob's internal thought: I should help)");
|
|
471
|
-
expect(result).toContain("(Bob's actions: SEARCH, REPLY)");
|
|
472
|
-
});
|
|
473
|
-
|
|
474
|
-
it('should handle attachments', () => {
|
|
475
|
-
const messages: Memory[] = [
|
|
476
|
-
{
|
|
477
|
-
id: 'msg-1' as any,
|
|
478
|
-
entityId: 'entity-1' as any,
|
|
479
|
-
roomId: 'room-1' as any,
|
|
480
|
-
createdAt: Date.now(),
|
|
481
|
-
content: {
|
|
482
|
-
text: 'Check this out',
|
|
483
|
-
attachments: [
|
|
484
|
-
{ id: 'att-1', title: 'Image', url: 'http://example.com/img.jpg' },
|
|
485
|
-
{ id: 'att-2', title: 'Document', url: 'http://example.com/doc.pdf' },
|
|
486
|
-
],
|
|
487
|
-
source: 'chat',
|
|
488
|
-
} as Content,
|
|
489
|
-
},
|
|
490
|
-
];
|
|
491
|
-
|
|
492
|
-
const result = formatMessages({ messages, entities: mockEntities });
|
|
493
|
-
|
|
494
|
-
expect(result).toContain(
|
|
495
|
-
'(Attachments: [att-1 - Image (http://example.com/img.jpg)], [att-2 - Document (http://example.com/doc.pdf)])'
|
|
496
|
-
);
|
|
497
|
-
});
|
|
498
|
-
|
|
499
|
-
it('should filter out messages without entityId', () => {
|
|
500
|
-
const messages: Memory[] = [
|
|
501
|
-
{
|
|
502
|
-
id: 'msg-1' as any,
|
|
503
|
-
entityId: null as any,
|
|
504
|
-
roomId: 'room-1' as any,
|
|
505
|
-
createdAt: Date.now(),
|
|
506
|
-
content: { text: 'Should be filtered', source: 'chat' } as Content,
|
|
507
|
-
},
|
|
508
|
-
{
|
|
509
|
-
id: 'msg-2' as any,
|
|
510
|
-
entityId: 'entity-1' as any,
|
|
511
|
-
roomId: 'room-1' as any,
|
|
512
|
-
createdAt: Date.now(),
|
|
513
|
-
content: { text: 'Should appear', source: 'chat' } as Content,
|
|
514
|
-
},
|
|
515
|
-
];
|
|
516
|
-
|
|
517
|
-
const result = formatMessages({ messages, entities: mockEntities });
|
|
518
|
-
|
|
519
|
-
expect(result).not.toContain('Should be filtered');
|
|
520
|
-
expect(result).toContain('Should appear');
|
|
521
|
-
});
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
describe('formatPosts', () => {
|
|
525
|
-
const mockEntities: Entity[] = [
|
|
526
|
-
{
|
|
527
|
-
id: 'entity-1' as any,
|
|
528
|
-
names: ['Alice'],
|
|
529
|
-
agentId: 'agent-1' as any,
|
|
530
|
-
},
|
|
531
|
-
];
|
|
532
|
-
|
|
533
|
-
it('should format posts grouped by room', () => {
|
|
534
|
-
const messages: Memory[] = [
|
|
535
|
-
{
|
|
536
|
-
id: 'msg-1' as any,
|
|
537
|
-
entityId: 'entity-1' as any,
|
|
538
|
-
roomId: 'room-1' as any,
|
|
539
|
-
createdAt: Date.now() - 3600000,
|
|
540
|
-
content: {
|
|
541
|
-
text: 'First message',
|
|
542
|
-
source: 'twitter',
|
|
543
|
-
} as Content,
|
|
544
|
-
},
|
|
545
|
-
{
|
|
546
|
-
id: 'msg-2' as any,
|
|
547
|
-
entityId: 'entity-1' as any,
|
|
548
|
-
roomId: 'room-1' as any,
|
|
549
|
-
createdAt: Date.now(),
|
|
550
|
-
content: {
|
|
551
|
-
text: 'Second message',
|
|
552
|
-
source: 'twitter',
|
|
553
|
-
} as Content,
|
|
554
|
-
},
|
|
555
|
-
];
|
|
556
|
-
|
|
557
|
-
const result = formatPosts({ messages, entities: mockEntities });
|
|
558
|
-
|
|
559
|
-
// formatPosts uses roomId.slice(-5) to show only last 5 chars
|
|
560
|
-
expect(result).toContain('Conversation: oom-1');
|
|
561
|
-
expect(result).toContain('Name: Alice');
|
|
562
|
-
expect(result).toContain('First message');
|
|
563
|
-
expect(result).toContain('Second message');
|
|
564
|
-
expect(result).toContain('Source: twitter');
|
|
565
|
-
});
|
|
566
|
-
|
|
567
|
-
it('should include reply information', () => {
|
|
568
|
-
const messages: Memory[] = [
|
|
569
|
-
{
|
|
570
|
-
id: 'msg-1' as any,
|
|
571
|
-
entityId: 'entity-1' as any,
|
|
572
|
-
roomId: 'room-1' as any,
|
|
573
|
-
createdAt: Date.now(),
|
|
574
|
-
content: {
|
|
575
|
-
text: 'Reply message',
|
|
576
|
-
inReplyTo: '12345678-1234-1234-1234-123456789012' as any,
|
|
577
|
-
source: 'chat',
|
|
578
|
-
} as Content,
|
|
579
|
-
},
|
|
580
|
-
];
|
|
581
|
-
|
|
582
|
-
const result = formatPosts({ messages, entities: mockEntities });
|
|
583
|
-
|
|
584
|
-
expect(result).toContain('In reply to: 12345678-1234-1234-1234-123456789012');
|
|
585
|
-
});
|
|
586
|
-
|
|
587
|
-
it('should format without conversation header when specified', () => {
|
|
588
|
-
const messages: Memory[] = [
|
|
589
|
-
{
|
|
590
|
-
id: 'msg-1' as any,
|
|
591
|
-
entityId: 'entity-1' as any,
|
|
592
|
-
roomId: 'room-1' as any,
|
|
593
|
-
createdAt: Date.now(),
|
|
594
|
-
content: { text: 'Message', source: 'chat' } as Content,
|
|
595
|
-
},
|
|
596
|
-
];
|
|
597
|
-
|
|
598
|
-
const result = formatPosts({
|
|
599
|
-
messages,
|
|
600
|
-
entities: mockEntities,
|
|
601
|
-
conversationHeader: false,
|
|
602
|
-
});
|
|
603
|
-
|
|
604
|
-
expect(result).not.toContain('Conversation:');
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
it('should handle missing entity with warning', () => {
|
|
608
|
-
// This tests line 209 - logger.warn when entity not found
|
|
609
|
-
const messages: Memory[] = [
|
|
610
|
-
{
|
|
611
|
-
id: 'msg-1' as any,
|
|
612
|
-
entityId: 'non-existent-entity' as any,
|
|
613
|
-
roomId: 'room-1' as any,
|
|
614
|
-
createdAt: Date.now(),
|
|
615
|
-
content: {
|
|
616
|
-
text: 'Message from unknown entity',
|
|
617
|
-
source: 'chat',
|
|
618
|
-
} as Content,
|
|
619
|
-
},
|
|
620
|
-
];
|
|
621
|
-
|
|
622
|
-
// Empty entities array to ensure entity is not found
|
|
623
|
-
const result = formatPosts({ messages, entities: [] });
|
|
624
|
-
|
|
625
|
-
// Should use "Unknown User" when entity not found
|
|
626
|
-
expect(result).toContain('Unknown User');
|
|
627
|
-
expect(result).toContain('unknown');
|
|
628
|
-
expect(result).toContain('non-existent-entity');
|
|
629
|
-
});
|
|
630
|
-
|
|
631
|
-
it('should handle messages without roomId', () => {
|
|
632
|
-
const messages: Memory[] = [
|
|
633
|
-
{
|
|
634
|
-
id: 'msg-1' as any,
|
|
635
|
-
entityId: 'entity-1' as any,
|
|
636
|
-
roomId: null as any, // No roomId
|
|
637
|
-
createdAt: Date.now(),
|
|
638
|
-
content: {
|
|
639
|
-
text: 'Message without room',
|
|
640
|
-
source: 'chat',
|
|
641
|
-
} as Content,
|
|
642
|
-
},
|
|
643
|
-
];
|
|
644
|
-
|
|
645
|
-
const result = formatPosts({ messages, entities: mockEntities });
|
|
646
|
-
|
|
647
|
-
// Messages without roomId should be filtered out
|
|
648
|
-
expect(result).toBe('');
|
|
649
|
-
});
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
describe('validateUuid', () => {
|
|
653
|
-
it('should validate correct UUID format', () => {
|
|
654
|
-
const validUuid = '123e4567-e89b-12d3-a456-426614174000';
|
|
655
|
-
const result = validateUuid(validUuid);
|
|
656
|
-
expect(result).toBe(validUuid);
|
|
657
|
-
});
|
|
658
|
-
|
|
659
|
-
it('should return null for invalid UUID format', () => {
|
|
660
|
-
expect(validateUuid('not-a-uuid')).toBeNull();
|
|
661
|
-
expect(validateUuid('123')).toBeNull();
|
|
662
|
-
expect(validateUuid('')).toBeNull();
|
|
663
|
-
expect(validateUuid(null)).toBeNull();
|
|
664
|
-
expect(validateUuid(undefined)).toBeNull();
|
|
665
|
-
expect(validateUuid(123)).toBeNull();
|
|
666
|
-
expect(validateUuid({})).toBeNull();
|
|
667
|
-
});
|
|
668
|
-
|
|
669
|
-
it('should handle UUID-like strings that are invalid', () => {
|
|
670
|
-
// Wrong format variations
|
|
671
|
-
expect(validateUuid('123e4567-e89b-12d3-a456')).toBeNull(); // Too short
|
|
672
|
-
expect(validateUuid('123e4567-e89b-12d3-a456-426614174000-extra')).toBeNull(); // Too long
|
|
673
|
-
expect(validateUuid('123e4567e89b12d3a456426614174000')).toBeNull(); // No dashes
|
|
674
|
-
expect(validateUuid('XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX')).toBeNull(); // Invalid chars
|
|
675
|
-
});
|
|
676
|
-
});
|
|
677
|
-
|
|
678
|
-
describe('stringToUuid', () => {
|
|
679
|
-
it('should convert string to UUID', () => {
|
|
680
|
-
const result = stringToUuid('test-string');
|
|
681
|
-
expect(result).toBeDefined();
|
|
682
|
-
expect(typeof result).toBe('string');
|
|
683
|
-
// Should be in UUID format
|
|
684
|
-
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it('should convert number to UUID', () => {
|
|
688
|
-
const result = stringToUuid(12345);
|
|
689
|
-
expect(result).toBeDefined();
|
|
690
|
-
expect(typeof result).toBe('string');
|
|
691
|
-
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
692
|
-
});
|
|
693
|
-
|
|
694
|
-
it('should throw TypeError for non-string/non-number input', () => {
|
|
695
|
-
expect(() => stringToUuid(null as any)).toThrow(TypeError);
|
|
696
|
-
expect(() => stringToUuid(undefined as any)).toThrow(TypeError);
|
|
697
|
-
expect(() => stringToUuid({} as any)).toThrow(TypeError);
|
|
698
|
-
expect(() => stringToUuid([] as any)).toThrow(TypeError);
|
|
699
|
-
expect(() => stringToUuid(true as any)).toThrow(TypeError);
|
|
700
|
-
});
|
|
701
|
-
|
|
702
|
-
it('should generate consistent UUID for same input', () => {
|
|
703
|
-
const input = 'consistent-input';
|
|
704
|
-
const uuid1 = stringToUuid(input);
|
|
705
|
-
const uuid2 = stringToUuid(input);
|
|
706
|
-
expect(uuid1).toBe(uuid2);
|
|
707
|
-
});
|
|
708
|
-
|
|
709
|
-
it('should generate different UUIDs for different inputs', () => {
|
|
710
|
-
const uuid1 = stringToUuid('input1');
|
|
711
|
-
const uuid2 = stringToUuid('input2');
|
|
712
|
-
expect(uuid1).not.toBe(uuid2);
|
|
713
|
-
});
|
|
714
|
-
|
|
715
|
-
it('should handle special characters in string input', () => {
|
|
716
|
-
const specialChars = 'test@#$%^&*()_+-=[]{}|;\':",./<>?';
|
|
717
|
-
const result = stringToUuid(specialChars);
|
|
718
|
-
expect(result).toBeDefined();
|
|
719
|
-
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
720
|
-
});
|
|
721
|
-
|
|
722
|
-
it('should handle empty string', () => {
|
|
723
|
-
const result = stringToUuid('');
|
|
724
|
-
expect(result).toBeDefined();
|
|
725
|
-
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
726
|
-
});
|
|
727
|
-
|
|
728
|
-
it('should handle very long strings', () => {
|
|
729
|
-
const longString = 'a'.repeat(1000);
|
|
730
|
-
const result = stringToUuid(longString);
|
|
731
|
-
expect(result).toBeDefined();
|
|
732
|
-
expect(result).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
|
733
|
-
});
|
|
734
|
-
});
|
|
735
|
-
|
|
736
|
-
describe('safeReplacer', () => {
|
|
737
|
-
it('should handle circular references', () => {
|
|
738
|
-
const obj: any = { a: 1, b: { c: 2 } };
|
|
739
|
-
obj.circular = obj; // Create circular reference
|
|
740
|
-
obj.b.parent = obj; // Another circular reference
|
|
741
|
-
|
|
742
|
-
const replacer = safeReplacer();
|
|
743
|
-
const result = JSON.stringify(obj, replacer);
|
|
744
|
-
|
|
745
|
-
expect(result).toContain('[Circular]');
|
|
746
|
-
expect(result).toContain('"a":1');
|
|
747
|
-
expect(result).toContain('"c":2');
|
|
748
|
-
|
|
749
|
-
// Should not throw error
|
|
750
|
-
expect(() => JSON.stringify(obj, replacer)).not.toThrow();
|
|
751
|
-
});
|
|
752
|
-
|
|
753
|
-
it('should handle non-circular objects normally', () => {
|
|
754
|
-
const obj = {
|
|
755
|
-
a: 1,
|
|
756
|
-
b: { c: 2 },
|
|
757
|
-
d: [1, 2, 3],
|
|
758
|
-
e: null,
|
|
759
|
-
f: 'string',
|
|
760
|
-
};
|
|
761
|
-
|
|
762
|
-
const replacer = safeReplacer();
|
|
763
|
-
const result = JSON.stringify(obj, replacer);
|
|
764
|
-
|
|
765
|
-
expect(result).toBe(JSON.stringify(obj)); // Should be identical
|
|
766
|
-
expect(result).not.toContain('[Circular]');
|
|
767
|
-
});
|
|
768
|
-
|
|
769
|
-
it('should handle nested circular references', () => {
|
|
770
|
-
const obj: any = {
|
|
771
|
-
level1: {
|
|
772
|
-
level2: {
|
|
773
|
-
level3: {},
|
|
774
|
-
},
|
|
775
|
-
},
|
|
776
|
-
};
|
|
777
|
-
obj.level1.level2.level3.back = obj.level1; // Circular reference
|
|
778
|
-
|
|
779
|
-
const replacer = safeReplacer();
|
|
780
|
-
const result = JSON.stringify(obj, replacer);
|
|
781
|
-
|
|
782
|
-
expect(result).toContain('[Circular]');
|
|
783
|
-
expect(() => JSON.stringify(obj, replacer)).not.toThrow();
|
|
784
|
-
});
|
|
785
|
-
|
|
786
|
-
it('should handle arrays with circular references', () => {
|
|
787
|
-
const arr: any[] = [1, 2, 3];
|
|
788
|
-
const obj = { arr, value: 'test' };
|
|
789
|
-
arr.push(obj); // Circular reference through array
|
|
790
|
-
|
|
791
|
-
const replacer = safeReplacer();
|
|
792
|
-
const result = JSON.stringify({ data: arr }, replacer);
|
|
793
|
-
|
|
794
|
-
expect(result).toContain('[Circular]');
|
|
795
|
-
expect(result).toContain('1');
|
|
796
|
-
expect(result).toContain('2');
|
|
797
|
-
expect(result).toContain('3');
|
|
798
|
-
});
|
|
799
|
-
});
|
|
800
|
-
|
|
801
|
-
describe('composePrompt', () => {
|
|
802
|
-
it('should compose prompt with state values', () => {
|
|
803
|
-
const state = {
|
|
804
|
-
name: 'Alice',
|
|
805
|
-
role: 'developer',
|
|
806
|
-
task: 'write tests',
|
|
807
|
-
};
|
|
808
|
-
const template = `Hello {{name}}, as a {{role}}, please {{task}}.`;
|
|
809
|
-
|
|
810
|
-
const result = composePrompt({ state, template });
|
|
811
|
-
|
|
812
|
-
expect(result).toBe('Hello Alice, as a developer, please write tests.');
|
|
813
|
-
});
|
|
814
|
-
|
|
815
|
-
it('should handle missing state values', () => {
|
|
816
|
-
const state = {
|
|
817
|
-
name: 'Bob',
|
|
818
|
-
};
|
|
819
|
-
const template = `Hello {{name}}, your role is {{role}}.`;
|
|
820
|
-
|
|
821
|
-
const result = composePrompt({ state, template });
|
|
822
|
-
|
|
823
|
-
// Handlebars replaces missing values with empty string
|
|
824
|
-
expect(result).toBe('Hello Bob, your role is .');
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it('should handle empty state', () => {
|
|
828
|
-
const state = {};
|
|
829
|
-
const template = `Template with {{placeholder}}.`;
|
|
830
|
-
|
|
831
|
-
const result = composePrompt({ state, template });
|
|
832
|
-
|
|
833
|
-
// Handlebars replaces missing values with empty string
|
|
834
|
-
expect(result).toBe('Template with .');
|
|
835
|
-
});
|
|
836
|
-
|
|
837
|
-
it('should handle multiple occurrences of same placeholder', () => {
|
|
838
|
-
const state = {
|
|
839
|
-
word: 'test',
|
|
840
|
-
};
|
|
841
|
-
const template = `{{word}} {{word}} {{word}}`;
|
|
842
|
-
|
|
843
|
-
const result = composePrompt({ state, template });
|
|
844
|
-
|
|
845
|
-
expect(result).toBe('test test test');
|
|
846
|
-
});
|
|
847
|
-
});
|
|
848
|
-
|
|
849
|
-
describe('composePromptFromState', () => {
|
|
850
|
-
it('should compose prompt from State object', () => {
|
|
851
|
-
const mockState: State = {
|
|
852
|
-
// Required State properties
|
|
853
|
-
userId: 'user-123' as any,
|
|
854
|
-
agentId: 'agent-123' as any,
|
|
855
|
-
roomId: 'room-123' as any,
|
|
856
|
-
bio: 'I am a helpful assistant',
|
|
857
|
-
lore: 'Created to help with tasks',
|
|
858
|
-
senderName: 'Assistant',
|
|
859
|
-
actors: 'user123',
|
|
860
|
-
actorsData: [
|
|
861
|
-
{
|
|
862
|
-
id: 'actor-1' as any,
|
|
863
|
-
names: ['John'],
|
|
864
|
-
agentId: 'agent-123' as any,
|
|
865
|
-
},
|
|
866
|
-
],
|
|
867
|
-
// Template values
|
|
868
|
-
recentMessages: 'Recent conversation history',
|
|
869
|
-
relevantKnowledge: 'Relevant facts',
|
|
870
|
-
recentMessagesData: [
|
|
871
|
-
{
|
|
872
|
-
id: 'msg-1' as any,
|
|
873
|
-
entityId: 'entity-1' as any,
|
|
874
|
-
roomId: 'room-123' as any,
|
|
875
|
-
createdAt: Date.now(),
|
|
876
|
-
content: {
|
|
877
|
-
text: 'Hello',
|
|
878
|
-
source: 'chat',
|
|
879
|
-
} as Content,
|
|
880
|
-
},
|
|
881
|
-
],
|
|
882
|
-
// Additional required State properties
|
|
883
|
-
values: {},
|
|
884
|
-
data: {},
|
|
885
|
-
text: '',
|
|
886
|
-
};
|
|
887
|
-
|
|
888
|
-
const template = `Bio: {{bio}}\nLore: {{lore}}\nRecent: {{recentMessages}}`;
|
|
889
|
-
|
|
890
|
-
const result = composePromptFromState({ state: mockState, template });
|
|
891
|
-
|
|
892
|
-
expect(result).toContain('Bio: I am a helpful assistant');
|
|
893
|
-
expect(result).toContain('Lore: Created to help with tasks');
|
|
894
|
-
expect(result).toContain('Recent: Recent conversation history');
|
|
895
|
-
});
|
|
896
|
-
|
|
897
|
-
it('should handle State with array data', () => {
|
|
898
|
-
const mockState: State = {
|
|
899
|
-
userId: 'user-123' as any,
|
|
900
|
-
agentId: 'agent-123' as any,
|
|
901
|
-
roomId: 'room-123' as any,
|
|
902
|
-
bio: 'Assistant bio',
|
|
903
|
-
lore: 'Assistant lore',
|
|
904
|
-
senderName: 'User',
|
|
905
|
-
actors: '',
|
|
906
|
-
actorsData: [
|
|
907
|
-
{
|
|
908
|
-
id: 'actor-1' as any,
|
|
909
|
-
names: ['Alice'],
|
|
910
|
-
agentId: 'agent-123' as any,
|
|
911
|
-
},
|
|
912
|
-
{
|
|
913
|
-
id: 'actor-2' as any,
|
|
914
|
-
names: ['Bob'],
|
|
915
|
-
agentId: 'agent-123' as any,
|
|
916
|
-
},
|
|
917
|
-
],
|
|
918
|
-
recentMessagesData: [],
|
|
919
|
-
values: {},
|
|
920
|
-
data: {},
|
|
921
|
-
text: '',
|
|
922
|
-
};
|
|
923
|
-
|
|
924
|
-
const template = `Actors: {{actors}}\nSender: {{senderName}}`;
|
|
925
|
-
|
|
926
|
-
const result = composePromptFromState({ state: mockState, template });
|
|
927
|
-
|
|
928
|
-
expect(result).toContain('Sender: User');
|
|
929
|
-
// actors field might be generated from actorsData
|
|
930
|
-
expect(result).toBeDefined();
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
it('should handle missing properties in State', () => {
|
|
934
|
-
const mockState: State = {
|
|
935
|
-
userId: 'user-123' as any,
|
|
936
|
-
agentId: 'agent-123' as any,
|
|
937
|
-
roomId: 'room-123' as any,
|
|
938
|
-
bio: '',
|
|
939
|
-
lore: '',
|
|
940
|
-
senderName: '',
|
|
941
|
-
actors: '',
|
|
942
|
-
actorsData: [],
|
|
943
|
-
recentMessagesData: [],
|
|
944
|
-
values: {},
|
|
945
|
-
data: {},
|
|
946
|
-
text: '',
|
|
947
|
-
};
|
|
948
|
-
|
|
949
|
-
const template = `Bio: {{bio}}\nMissing: {{missingProp}}`;
|
|
950
|
-
|
|
951
|
-
const result = composePromptFromState({ state: mockState, template });
|
|
952
|
-
|
|
953
|
-
expect(result).toContain('Bio: ');
|
|
954
|
-
// Handlebars replaces missing values with empty string
|
|
955
|
-
expect(result).toBe('Bio: \nMissing: ');
|
|
956
|
-
});
|
|
957
|
-
});
|
|
958
|
-
|
|
959
|
-
describe('addHeader', () => {
|
|
960
|
-
it('should add header to body', () => {
|
|
961
|
-
const header = '# Title';
|
|
962
|
-
const body = 'This is the body content.';
|
|
963
|
-
|
|
964
|
-
const result = addHeader(header, body);
|
|
965
|
-
|
|
966
|
-
// addHeader adds only single newline between header and body, and adds newline at end
|
|
967
|
-
expect(result).toBe('# Title\nThis is the body content.\n');
|
|
968
|
-
});
|
|
969
|
-
|
|
970
|
-
it('should handle empty header', () => {
|
|
971
|
-
const header = '';
|
|
972
|
-
const body = 'Body content';
|
|
973
|
-
|
|
974
|
-
const result = addHeader(header, body);
|
|
975
|
-
|
|
976
|
-
// Empty header results in just body with newline at end
|
|
977
|
-
expect(result).toBe('Body content\n');
|
|
978
|
-
});
|
|
979
|
-
|
|
980
|
-
it('should handle empty body', () => {
|
|
981
|
-
const header = 'Header';
|
|
982
|
-
const body = '';
|
|
983
|
-
|
|
984
|
-
const result = addHeader(header, body);
|
|
985
|
-
|
|
986
|
-
// Empty body returns empty string regardless of header
|
|
987
|
-
expect(result).toBe('');
|
|
988
|
-
});
|
|
989
|
-
|
|
990
|
-
it('should handle both empty', () => {
|
|
991
|
-
const result = addHeader('', '');
|
|
992
|
-
|
|
993
|
-
// Both empty returns empty string
|
|
994
|
-
expect(result).toBe('');
|
|
995
|
-
});
|
|
996
|
-
|
|
997
|
-
it('should handle multiline header and body', () => {
|
|
998
|
-
const header = 'Line 1\nLine 2';
|
|
999
|
-
const body = 'Body line 1\nBody line 2';
|
|
1000
|
-
|
|
1001
|
-
const result = addHeader(header, body);
|
|
1002
|
-
|
|
1003
|
-
// Single newline between header and body, newline at end
|
|
1004
|
-
expect(result).toBe('Line 1\nLine 2\nBody line 1\nBody line 2\n');
|
|
1005
|
-
});
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
// we test this via composePrompt and composePromptFromState
|
|
1009
|
-
/*
|
|
1010
|
-
it('upgradeDoubleToTriple converts double braces to triple', () => {
|
|
1011
|
-
const tpl = 'Hello {{name}} and {{#if cond}}{{value}}{{/if}}';
|
|
1012
|
-
const out = upgradeDoubleToTriple(tpl);
|
|
1013
|
-
expect(out).toBe('Hello {{{name}}} and {{#if cond}}{{{value}}}{{/if}}');
|
|
1014
|
-
});
|
|
1015
|
-
*/
|
|
1016
|
-
|
|
1017
|
-
it('addHeader prepends header when body exists', () => {
|
|
1018
|
-
expect(addHeader('Head', 'Body')).toBe('Head\nBody\n');
|
|
1019
|
-
expect(addHeader('Head', '')).toBe('');
|
|
1020
|
-
});
|
|
1021
|
-
|
|
1022
|
-
// we test this via composePrompt and composePromptFromState
|
|
1023
|
-
/*
|
|
1024
|
-
it('composeRandomUser replaces placeholders', () => {
|
|
1025
|
-
const result = composeRandomUser('hi {{name1}} {{name2}}', 2);
|
|
1026
|
-
expect(result).not.toContain('{{');
|
|
1027
|
-
});
|
|
1028
|
-
*/
|
|
1029
|
-
|
|
1030
|
-
it('parseKeyValueXml parses simple xml block', () => {
|
|
1031
|
-
const xml = '<response><key>value</key><actions>a,b</actions><simple>true</simple></response>';
|
|
1032
|
-
const parsed = parseKeyValueXml(xml);
|
|
1033
|
-
expect(parsed).toEqual({ key: 'value', actions: ['a', 'b'], simple: true });
|
|
1034
|
-
});
|
|
1035
|
-
|
|
1036
|
-
it('safeReplacer handles circular objects', () => {
|
|
1037
|
-
const obj: any = { a: 1 };
|
|
1038
|
-
obj.self = obj;
|
|
1039
|
-
const str = JSON.stringify(obj, safeReplacer());
|
|
1040
|
-
expect(str).toContain('[Circular]');
|
|
1041
|
-
});
|
|
1042
|
-
|
|
1043
|
-
it('validateUuid validates correct uuid and rejects bad values', () => {
|
|
1044
|
-
const valid = validateUuid('123e4567-e89b-12d3-a456-426614174000');
|
|
1045
|
-
const invalid = validateUuid('not-a-uuid');
|
|
1046
|
-
expect(valid).toBe('123e4567-e89b-12d3-a456-426614174000');
|
|
1047
|
-
expect(invalid).toBeNull();
|
|
1048
|
-
});
|
|
1049
|
-
|
|
1050
|
-
it('composePrompt inserts state values', () => {
|
|
1051
|
-
//const spy = vi.spyOn(utils, 'composeRandomUser').mockImplementation((t) => t);
|
|
1052
|
-
const out = utils.composePrompt({ state: { a: 'x' }, template: 'Hello {{a}}' });
|
|
1053
|
-
expect(out).toBe('Hello x');
|
|
1054
|
-
//spy.mockRestore();
|
|
1055
|
-
});
|
|
1056
|
-
|
|
1057
|
-
it('composePromptFromState flattens state values', () => {
|
|
1058
|
-
//const spy = vi.spyOn(utils, 'composeRandomUser').mockImplementation((t) => t);
|
|
1059
|
-
const out = utils.composePromptFromState({
|
|
1060
|
-
state: { values: { b: 'y' }, data: {}, text: '', c: 'z' },
|
|
1061
|
-
template: '{{b}} {{c}}',
|
|
1062
|
-
});
|
|
1063
|
-
expect(out).toBe('y z');
|
|
1064
|
-
//spy.mockRestore();
|
|
1065
|
-
});
|
|
1066
|
-
|
|
1067
|
-
it('formatPosts formats conversation text', () => {
|
|
1068
|
-
const messages = [
|
|
1069
|
-
{
|
|
1070
|
-
id: '1',
|
|
1071
|
-
entityId: 'e1',
|
|
1072
|
-
roomId: 'r1',
|
|
1073
|
-
createdAt: 1,
|
|
1074
|
-
content: { text: 'hi', source: 'chat' },
|
|
1075
|
-
},
|
|
1076
|
-
{
|
|
1077
|
-
id: '2',
|
|
1078
|
-
entityId: 'e1',
|
|
1079
|
-
roomId: 'r1',
|
|
1080
|
-
createdAt: 2,
|
|
1081
|
-
content: { text: 'there', source: 'chat' },
|
|
1082
|
-
},
|
|
1083
|
-
] as any;
|
|
1084
|
-
const entities = [{ id: 'e1', names: ['Alice'] }] as any;
|
|
1085
|
-
const result = utils.formatPosts({ messages, entities });
|
|
1086
|
-
expect(result).toContain('Conversation:');
|
|
1087
|
-
expect(result).toContain('Alice');
|
|
1088
|
-
expect(result).toContain('hi');
|
|
1089
|
-
expect(result).toContain('there');
|
|
1090
|
-
});
|
|
1091
|
-
|
|
1092
|
-
it('trimTokens truncates using runtime tokenizer', async () => {
|
|
1093
|
-
const runtime = {
|
|
1094
|
-
useModel: mock(
|
|
1095
|
-
async (type: (typeof ModelType)[keyof typeof ModelType], { prompt, tokens }: any) => {
|
|
1096
|
-
if (type === ModelType.TEXT_TOKENIZER_ENCODE) return prompt.split(' ');
|
|
1097
|
-
if (type === ModelType.TEXT_TOKENIZER_DECODE) return tokens.join(' ');
|
|
1098
|
-
return [];
|
|
1099
|
-
}
|
|
1100
|
-
),
|
|
1101
|
-
} as any;
|
|
1102
|
-
const result = await utils.trimTokens('a b c d e', 3, runtime);
|
|
1103
|
-
expect(result).toBe('c d e');
|
|
1104
|
-
});
|
|
1105
|
-
});
|