@jarcelao/pi-exa-api 0.2.2 → 0.3.0
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 +1 -1
- package/extensions/api-key.ts +12 -0
- package/extensions/content-types.ts +43 -0
- package/extensions/errors.ts +12 -0
- package/extensions/formatters.ts +167 -0
- package/extensions/index.ts +66 -0
- package/extensions/temp-file.ts +19 -0
- package/extensions/tools/code-context.ts +163 -0
- package/extensions/tools/fetch.ts +154 -0
- package/extensions/tools/search.ts +148 -0
- package/extensions/types.ts +68 -0
- package/package.json +2 -2
- package/tests/api-key.test.ts +39 -0
- package/tests/content-type-mapping.test.ts +51 -0
- package/tests/extension-registration.test.ts +161 -0
- package/tests/formatting.test.ts +216 -0
- package/exa-search.test.ts +0 -851
- package/extensions/exa-search.ts +0 -646
package/exa-search.test.ts
DELETED
|
@@ -1,851 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, afterEach } from "vitest";
|
|
2
|
-
import exaSearchExtension, {
|
|
3
|
-
getApiKey,
|
|
4
|
-
mapSearchContentType,
|
|
5
|
-
mapFetchContentType,
|
|
6
|
-
formatSearchResults,
|
|
7
|
-
formatFetchResult,
|
|
8
|
-
formatCodeContextResult,
|
|
9
|
-
parseCostDollars,
|
|
10
|
-
createMissingApiKeyError,
|
|
11
|
-
} from "./extensions/exa-search.ts";
|
|
12
|
-
import {
|
|
13
|
-
truncateHead,
|
|
14
|
-
DEFAULT_MAX_BYTES,
|
|
15
|
-
DEFAULT_MAX_LINES,
|
|
16
|
-
formatSize,
|
|
17
|
-
} from "@mariozechner/pi-coding-agent";
|
|
18
|
-
|
|
19
|
-
describe("API Key Management", () => {
|
|
20
|
-
afterEach(() => {
|
|
21
|
-
delete process.env.EXA_API_KEY;
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
it("should return undefined when EXA_API_KEY is not set", () => {
|
|
25
|
-
delete process.env.EXA_API_KEY;
|
|
26
|
-
expect(getApiKey()).toBeUndefined();
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it("should return API key when EXA_API_KEY is set", () => {
|
|
30
|
-
const testKey = "test-api-key-123";
|
|
31
|
-
process.env.EXA_API_KEY = testKey;
|
|
32
|
-
expect(getApiKey()).toBe(testKey);
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it("should treat empty string as not configured", () => {
|
|
36
|
-
process.env.EXA_API_KEY = "";
|
|
37
|
-
expect(getApiKey()).toBeUndefined();
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it("should throw descriptive error when API key is missing", () => {
|
|
41
|
-
const error = createMissingApiKeyError();
|
|
42
|
-
expect(error.message).toContain("EXA_API_KEY");
|
|
43
|
-
expect(error.message).toContain("environment variable");
|
|
44
|
-
});
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
describe("ContentType Mapping (Search)", () => {
|
|
48
|
-
it('should map "text" to contents.text', () => {
|
|
49
|
-
const result = mapSearchContentType("text");
|
|
50
|
-
expect(result).toEqual({ text: true });
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('should map "highlights" to contents.highlights', () => {
|
|
54
|
-
const result = mapSearchContentType("highlights");
|
|
55
|
-
expect(result).toEqual({ highlights: true });
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should map "summary" to contents.summary', () => {
|
|
59
|
-
const result = mapSearchContentType("summary");
|
|
60
|
-
expect(result).toEqual({ summary: true });
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('should map "none" to undefined (metadata only)', () => {
|
|
64
|
-
const result = mapSearchContentType("none");
|
|
65
|
-
expect(result).toBeUndefined();
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it("should default to highlights when not specified", () => {
|
|
69
|
-
const result = mapSearchContentType(undefined);
|
|
70
|
-
expect(result).toEqual({ highlights: true });
|
|
71
|
-
});
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
describe("ContentType Mapping (Fetch)", () => {
|
|
75
|
-
it('should map "text" to contents.text', () => {
|
|
76
|
-
const result = mapFetchContentType("text");
|
|
77
|
-
expect(result).toEqual({ text: true });
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should map "highlights" to contents.highlights', () => {
|
|
81
|
-
const result = mapFetchContentType("highlights");
|
|
82
|
-
expect(result).toEqual({ highlights: true });
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should map "summary" to contents.summary', () => {
|
|
86
|
-
const result = mapFetchContentType("summary");
|
|
87
|
-
expect(result).toEqual({ summary: true });
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it("should default to text for fetch when not specified", () => {
|
|
91
|
-
const result = mapFetchContentType(undefined);
|
|
92
|
-
expect(result).toEqual({ text: true });
|
|
93
|
-
});
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
describe("Parameter Validation", () => {
|
|
97
|
-
it("should accept valid search params (query only)", () => {
|
|
98
|
-
const params = { query: "test query" };
|
|
99
|
-
expect(params.query).toBeTruthy();
|
|
100
|
-
expect(params.query.trim()).not.toBe("");
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it("should accept valid search params (all options)", () => {
|
|
104
|
-
const params = {
|
|
105
|
-
query: "test",
|
|
106
|
-
contentType: "highlights" as const,
|
|
107
|
-
numResults: 10,
|
|
108
|
-
};
|
|
109
|
-
expect(params.query).toBeTruthy();
|
|
110
|
-
expect(["text", "highlights", "summary", "none"].includes(params.contentType)).toBe(true);
|
|
111
|
-
expect(params.numResults).toBeGreaterThanOrEqual(1);
|
|
112
|
-
expect(params.numResults).toBeLessThanOrEqual(100);
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
it("should accept valid fetch params", () => {
|
|
116
|
-
const params = {
|
|
117
|
-
url: "https://example.com",
|
|
118
|
-
contentType: "text" as const,
|
|
119
|
-
maxCharacters: 5000,
|
|
120
|
-
};
|
|
121
|
-
expect(params.url).toBeTruthy();
|
|
122
|
-
expect(["text", "highlights", "summary"].includes(params.contentType)).toBe(true);
|
|
123
|
-
expect(params.maxCharacters).toBeGreaterThan(0);
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
it("should reject fetch params with invalid contentType", () => {
|
|
127
|
-
const invalidContentTypes = ["none", "invalid", "full"];
|
|
128
|
-
for (const ct of invalidContentTypes) {
|
|
129
|
-
expect(["text", "highlights", "summary"].includes(ct as string)).toBe(false);
|
|
130
|
-
}
|
|
131
|
-
});
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
describe("Output Truncation", () => {
|
|
135
|
-
it("should not truncate short output", () => {
|
|
136
|
-
const text = "Short text";
|
|
137
|
-
const result = truncateHead(text);
|
|
138
|
-
expect(result.truncated).toBe(false);
|
|
139
|
-
expect(result.content).toBe(text);
|
|
140
|
-
expect(result.outputLines).toBe(1);
|
|
141
|
-
});
|
|
142
|
-
|
|
143
|
-
it("should truncate by line count", () => {
|
|
144
|
-
const lines = Array.from({ length: 100 }, (_, i) => `Line ${i}`);
|
|
145
|
-
const text = lines.join("\n");
|
|
146
|
-
const result = truncateHead(text, { maxLines: 10 });
|
|
147
|
-
expect(result.truncated).toBe(true);
|
|
148
|
-
expect(result.outputLines).toBe(10);
|
|
149
|
-
expect(result.totalLines).toBe(100);
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it("should truncate by byte size", () => {
|
|
153
|
-
const text = "x".repeat(10000);
|
|
154
|
-
const result = truncateHead(text, { maxBytes: 1000 });
|
|
155
|
-
expect(result.truncated).toBe(true);
|
|
156
|
-
expect(result.outputBytes).toBeLessThanOrEqual(1000);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it("should respect DEFAULT_MAX_LINES constant", () => {
|
|
160
|
-
expect(DEFAULT_MAX_LINES).toBe(2000);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("should respect DEFAULT_MAX_BYTES constant", () => {
|
|
164
|
-
expect(DEFAULT_MAX_BYTES).toBe(50 * 1024);
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it("formatSize should format bytes correctly", () => {
|
|
168
|
-
expect(formatSize(500)).toBe("500B");
|
|
169
|
-
expect(formatSize(1024)).toBe("1.0KB");
|
|
170
|
-
expect(formatSize(1024 * 50)).toBe("50.0KB");
|
|
171
|
-
expect(formatSize(1024 * 1024)).toBe("1.0MB");
|
|
172
|
-
});
|
|
173
|
-
});
|
|
174
|
-
|
|
175
|
-
describe("Search Result Formatting", () => {
|
|
176
|
-
it("should format basic search results", () => {
|
|
177
|
-
const response = {
|
|
178
|
-
results: [
|
|
179
|
-
{
|
|
180
|
-
title: "Test Article",
|
|
181
|
-
url: "https://example.com/article",
|
|
182
|
-
publishedDate: "2024-01-15",
|
|
183
|
-
author: "John Doe",
|
|
184
|
-
},
|
|
185
|
-
],
|
|
186
|
-
};
|
|
187
|
-
|
|
188
|
-
const formatted = formatSearchResults(response);
|
|
189
|
-
expect(formatted).toContain("Test Article");
|
|
190
|
-
expect(formatted).toContain("https://example.com/article");
|
|
191
|
-
expect(formatted).toContain("2024-01-15");
|
|
192
|
-
expect(formatted).toContain("John Doe");
|
|
193
|
-
expect(formatted).toContain("--- Result 1 ---");
|
|
194
|
-
});
|
|
195
|
-
|
|
196
|
-
it("should format results with highlights", () => {
|
|
197
|
-
const response = {
|
|
198
|
-
results: [
|
|
199
|
-
{
|
|
200
|
-
title: "Test",
|
|
201
|
-
url: "https://example.com",
|
|
202
|
-
highlights: ["First highlight", "Second highlight"],
|
|
203
|
-
},
|
|
204
|
-
],
|
|
205
|
-
};
|
|
206
|
-
|
|
207
|
-
const formatted = formatSearchResults(response);
|
|
208
|
-
expect(formatted).toContain("Highlights:");
|
|
209
|
-
expect(formatted).toContain("First highlight");
|
|
210
|
-
expect(formatted).toContain("Second highlight");
|
|
211
|
-
expect(formatted).toContain(" • First highlight");
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
it("should format results with summary", () => {
|
|
215
|
-
const response = {
|
|
216
|
-
results: [
|
|
217
|
-
{
|
|
218
|
-
title: "Test",
|
|
219
|
-
url: "https://example.com",
|
|
220
|
-
summary: "This is a summary",
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
|
|
225
|
-
const formatted = formatSearchResults(response);
|
|
226
|
-
expect(formatted).toContain("Summary: This is a summary");
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
it("should include cost information", () => {
|
|
230
|
-
const response = {
|
|
231
|
-
results: [{ title: "Test", url: "https://example.com" }],
|
|
232
|
-
costDollars: { total: 0.000123 },
|
|
233
|
-
};
|
|
234
|
-
|
|
235
|
-
const formatted = formatSearchResults(response);
|
|
236
|
-
expect(formatted).toContain("Cost: $0.000123");
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it("should handle null publishedDate and author", () => {
|
|
240
|
-
const response = {
|
|
241
|
-
results: [
|
|
242
|
-
{
|
|
243
|
-
title: "Test",
|
|
244
|
-
url: "https://example.com",
|
|
245
|
-
publishedDate: null,
|
|
246
|
-
author: null,
|
|
247
|
-
},
|
|
248
|
-
],
|
|
249
|
-
};
|
|
250
|
-
|
|
251
|
-
const formatted = formatSearchResults(response);
|
|
252
|
-
expect(formatted).toContain("Title: Test");
|
|
253
|
-
expect(formatted).not.toContain("Published:");
|
|
254
|
-
expect(formatted).not.toContain("Author:");
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it("should truncate long text preview", () => {
|
|
258
|
-
const longText = "a".repeat(600);
|
|
259
|
-
const response = {
|
|
260
|
-
results: [
|
|
261
|
-
{
|
|
262
|
-
title: "Test",
|
|
263
|
-
url: "https://example.com",
|
|
264
|
-
text: longText,
|
|
265
|
-
},
|
|
266
|
-
],
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
const formatted = formatSearchResults(response);
|
|
270
|
-
expect(formatted).toContain("a".repeat(500));
|
|
271
|
-
expect(formatted).toContain("...");
|
|
272
|
-
expect(formatted).not.toContain("a".repeat(501));
|
|
273
|
-
});
|
|
274
|
-
});
|
|
275
|
-
|
|
276
|
-
describe("Fetch Result Formatting", () => {
|
|
277
|
-
it("should format text content", () => {
|
|
278
|
-
const result = {
|
|
279
|
-
title: "Page Title",
|
|
280
|
-
url: "https://example.com",
|
|
281
|
-
text: "Full page content here",
|
|
282
|
-
};
|
|
283
|
-
|
|
284
|
-
const formatted = formatFetchResult(result, "text");
|
|
285
|
-
expect(formatted).toContain("Title: Page Title");
|
|
286
|
-
expect(formatted).toContain("Full page content here");
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it("should include cost in fetch details", () => {
|
|
290
|
-
const details = {
|
|
291
|
-
url: "https://example.com",
|
|
292
|
-
title: "Test Page",
|
|
293
|
-
cost: { total: 0.000123 },
|
|
294
|
-
};
|
|
295
|
-
expect(details.cost).toBeDefined();
|
|
296
|
-
expect(details.cost?.total).toBe(0.000123);
|
|
297
|
-
});
|
|
298
|
-
|
|
299
|
-
it("should format highlights", () => {
|
|
300
|
-
const result = {
|
|
301
|
-
title: "Test Page",
|
|
302
|
-
url: "https://example.com",
|
|
303
|
-
highlights: ["Key point 1", "Key point 2"],
|
|
304
|
-
};
|
|
305
|
-
|
|
306
|
-
const formatted = formatFetchResult(result, "highlights");
|
|
307
|
-
expect(formatted).toContain("Highlights:");
|
|
308
|
-
expect(formatted).toContain("Key point 1");
|
|
309
|
-
expect(formatted).toContain("Key point 2");
|
|
310
|
-
expect(formatted).toContain(" • Key point 1");
|
|
311
|
-
});
|
|
312
|
-
|
|
313
|
-
it("should format summary", () => {
|
|
314
|
-
const result = {
|
|
315
|
-
title: "Test Page",
|
|
316
|
-
url: "https://example.com",
|
|
317
|
-
summary: "This page is about...",
|
|
318
|
-
};
|
|
319
|
-
|
|
320
|
-
const formatted = formatFetchResult(result, "summary");
|
|
321
|
-
expect(formatted).toContain("Summary:");
|
|
322
|
-
expect(formatted).toContain("This page is about...");
|
|
323
|
-
});
|
|
324
|
-
|
|
325
|
-
it("should not include title if not provided", () => {
|
|
326
|
-
const result = {
|
|
327
|
-
url: "https://example.com",
|
|
328
|
-
text: "Content only",
|
|
329
|
-
} as { title?: string; url: string; text: string };
|
|
330
|
-
|
|
331
|
-
const formatted = formatFetchResult(result as Parameters<typeof formatFetchResult>[0], "text");
|
|
332
|
-
expect(formatted).toContain("https://example.com");
|
|
333
|
-
expect(formatted).not.toContain("Title:");
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
|
-
describe("Error Handling", () => {
|
|
338
|
-
it("should create descriptive missing API key error", () => {
|
|
339
|
-
const error = createMissingApiKeyError();
|
|
340
|
-
expect(error.message).toContain("Exa API key");
|
|
341
|
-
expect(error.message).toContain("EXA_API_KEY");
|
|
342
|
-
expect(error.message).toContain("environment variable");
|
|
343
|
-
});
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
describe("parseCostDollars", () => {
|
|
347
|
-
it("should parse JSON string costDollars", () => {
|
|
348
|
-
const costString = '{"total":0.007,"search":{"neural":0.007}}';
|
|
349
|
-
const parsed = parseCostDollars(costString);
|
|
350
|
-
expect(parsed).toEqual({ total: 0.007, search: { neural: 0.007 } });
|
|
351
|
-
expect(parsed.total).toBe(0.007);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("should pass through object costDollars unchanged", () => {
|
|
355
|
-
const costObject = { total: 1.5 };
|
|
356
|
-
const parsed = parseCostDollars(costObject);
|
|
357
|
-
expect(parsed).toEqual({ total: 1.5 });
|
|
358
|
-
});
|
|
359
|
-
|
|
360
|
-
it("should handle various cost string formats", () => {
|
|
361
|
-
expect(parseCostDollars('{"total":0}').total).toBe(0);
|
|
362
|
-
expect(parseCostDollars('{"total":123.456}').total).toBe(123.456);
|
|
363
|
-
expect(parseCostDollars({ total: 99.9 }).total).toBe(99.9);
|
|
364
|
-
});
|
|
365
|
-
});
|
|
366
|
-
|
|
367
|
-
describe("Code Context Result Formatting", () => {
|
|
368
|
-
it("should format code context response with string costDollars", () => {
|
|
369
|
-
const response = {
|
|
370
|
-
requestId: "req_12345",
|
|
371
|
-
query: "how to use React hooks for state management",
|
|
372
|
-
response: "## useState Example\n\n```javascript\nconst [count, setCount] = useState(0);\n```",
|
|
373
|
-
resultsCount: 502,
|
|
374
|
-
costDollars: '{"total":0.007,"search":{"neural":0.007}}',
|
|
375
|
-
searchTime: 1.234,
|
|
376
|
-
outputTokens: 4805,
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
const formatted = formatCodeContextResult(response);
|
|
380
|
-
expect(formatted).toContain("Query: how to use React hooks for state management");
|
|
381
|
-
expect(formatted).toContain("Results: 502 sources");
|
|
382
|
-
expect(formatted).toContain("Output tokens: 4805");
|
|
383
|
-
expect(formatted).toContain("--- Code Context ---");
|
|
384
|
-
expect(formatted).toContain("## useState Example");
|
|
385
|
-
expect(formatted).toContain("const [count, setCount] = useState(0);");
|
|
386
|
-
expect(formatted).toContain("Cost: $0.007000");
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it("should format code context response with object costDollars", () => {
|
|
390
|
-
const response = {
|
|
391
|
-
requestId: "req_67890",
|
|
392
|
-
query: "test query",
|
|
393
|
-
response: "Some code examples...",
|
|
394
|
-
resultsCount: 10,
|
|
395
|
-
costDollars: { total: 1.5 },
|
|
396
|
-
searchTime: 0.5,
|
|
397
|
-
outputTokens: 1000,
|
|
398
|
-
};
|
|
399
|
-
|
|
400
|
-
const formatted = formatCodeContextResult(response);
|
|
401
|
-
expect(formatted).toContain("Cost: $1.500000");
|
|
402
|
-
});
|
|
403
|
-
|
|
404
|
-
it("should include query in formatted output", () => {
|
|
405
|
-
const response = {
|
|
406
|
-
requestId: "req_abc",
|
|
407
|
-
query: "Express.js middleware authentication",
|
|
408
|
-
response: "Some code examples...",
|
|
409
|
-
resultsCount: 100,
|
|
410
|
-
costDollars: '{"total":0.5}',
|
|
411
|
-
searchTime: 0.5,
|
|
412
|
-
outputTokens: 2000,
|
|
413
|
-
};
|
|
414
|
-
|
|
415
|
-
const formatted = formatCodeContextResult(response);
|
|
416
|
-
expect(formatted).toContain("Query: Express.js middleware authentication");
|
|
417
|
-
});
|
|
418
|
-
|
|
419
|
-
it("should format cost correctly with decimals", () => {
|
|
420
|
-
const response = {
|
|
421
|
-
requestId: "req_xyz",
|
|
422
|
-
query: "test query",
|
|
423
|
-
response: "response content",
|
|
424
|
-
resultsCount: 10,
|
|
425
|
-
costDollars: '{"total":0.123456}',
|
|
426
|
-
searchTime: 0.1,
|
|
427
|
-
outputTokens: 500,
|
|
428
|
-
};
|
|
429
|
-
|
|
430
|
-
const formatted = formatCodeContextResult(response);
|
|
431
|
-
expect(formatted).toContain("Cost: $0.123456");
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
it("should display results count and output tokens", () => {
|
|
435
|
-
const response = {
|
|
436
|
-
requestId: "req_test",
|
|
437
|
-
query: "pandas dataframe operations",
|
|
438
|
-
response: "Code examples here",
|
|
439
|
-
resultsCount: 150,
|
|
440
|
-
costDollars: '{"total":0.75}',
|
|
441
|
-
searchTime: 0.8,
|
|
442
|
-
outputTokens: 3500,
|
|
443
|
-
};
|
|
444
|
-
|
|
445
|
-
const formatted = formatCodeContextResult(response);
|
|
446
|
-
expect(formatted).toContain("Results: 150 sources");
|
|
447
|
-
expect(formatted).toContain("Output tokens: 3500");
|
|
448
|
-
});
|
|
449
|
-
});
|
|
450
|
-
|
|
451
|
-
describe("Code Context Parameter Validation", () => {
|
|
452
|
-
it("should accept valid params with query only", () => {
|
|
453
|
-
const params = { query: "React hooks examples" };
|
|
454
|
-
expect(params.query).toBeTruthy();
|
|
455
|
-
expect(params.query.trim()).not.toBe("");
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it("should accept valid params with dynamic tokens", () => {
|
|
459
|
-
const params = {
|
|
460
|
-
query: "Express middleware",
|
|
461
|
-
tokensNum: "dynamic" as const,
|
|
462
|
-
};
|
|
463
|
-
expect(typeof params.query).toBe("string");
|
|
464
|
-
expect(params.tokensNum).toBe("dynamic");
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("should accept valid params with numeric tokens", () => {
|
|
468
|
-
const params = {
|
|
469
|
-
query: "Next.js configuration",
|
|
470
|
-
tokensNum: 5000,
|
|
471
|
-
};
|
|
472
|
-
expect(params.tokensNum).toBeGreaterThanOrEqual(50);
|
|
473
|
-
expect(params.tokensNum).toBeLessThanOrEqual(100000);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("should accept token counts in valid range", () => {
|
|
477
|
-
const validTokenCounts = [50, 1000, 5000, 10000, 50000, 100000];
|
|
478
|
-
for (const count of validTokenCounts) {
|
|
479
|
-
expect(count).toBeGreaterThanOrEqual(50);
|
|
480
|
-
expect(count).toBeLessThanOrEqual(100000);
|
|
481
|
-
}
|
|
482
|
-
});
|
|
483
|
-
|
|
484
|
-
it("should accept query strings up to 2000 characters", () => {
|
|
485
|
-
const longQuery = "a".repeat(2000);
|
|
486
|
-
expect(longQuery.length).toBe(2000);
|
|
487
|
-
});
|
|
488
|
-
});
|
|
489
|
-
|
|
490
|
-
describe("tokensNum Type Coercion", () => {
|
|
491
|
-
// Helper function that mirrors the type coercion logic in exa_code_context execute
|
|
492
|
-
function coerceTokensNum(tokensNum: string | number | undefined): string | number {
|
|
493
|
-
const value = tokensNum ?? "dynamic";
|
|
494
|
-
if (typeof value === "string" && value !== "dynamic") {
|
|
495
|
-
return Number(value);
|
|
496
|
-
}
|
|
497
|
-
return value;
|
|
498
|
-
}
|
|
499
|
-
|
|
500
|
-
it("should return 'dynamic' when tokensNum is undefined", () => {
|
|
501
|
-
const result = coerceTokensNum(undefined);
|
|
502
|
-
expect(result).toBe("dynamic");
|
|
503
|
-
expect(typeof result).toBe("string");
|
|
504
|
-
});
|
|
505
|
-
|
|
506
|
-
it("should preserve 'dynamic' string", () => {
|
|
507
|
-
const result = coerceTokensNum("dynamic");
|
|
508
|
-
expect(result).toBe("dynamic");
|
|
509
|
-
expect(typeof result).toBe("string");
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
it("should preserve numeric tokens", () => {
|
|
513
|
-
const result = coerceTokensNum(5000);
|
|
514
|
-
expect(result).toBe(5000);
|
|
515
|
-
expect(typeof result).toBe("number");
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
it("should coerce numeric string to number", () => {
|
|
519
|
-
const result = coerceTokensNum("5000");
|
|
520
|
-
expect(result).toBe(5000);
|
|
521
|
-
expect(typeof result).toBe("number");
|
|
522
|
-
});
|
|
523
|
-
|
|
524
|
-
it("should coerce various numeric strings", () => {
|
|
525
|
-
expect(coerceTokensNum("1000")).toBe(1000);
|
|
526
|
-
expect(coerceTokensNum("10000")).toBe(10000);
|
|
527
|
-
expect(coerceTokensNum("3000")).toBe(3000);
|
|
528
|
-
expect(typeof coerceTokensNum("5000")).toBe("number");
|
|
529
|
-
});
|
|
530
|
-
|
|
531
|
-
it("should handle boundary token values", () => {
|
|
532
|
-
expect(coerceTokensNum(50)).toBe(50);
|
|
533
|
-
expect(coerceTokensNum(100000)).toBe(100000);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it("should not modify actual numbers", () => {
|
|
537
|
-
const originalNumber = 7500;
|
|
538
|
-
const result = coerceTokensNum(originalNumber);
|
|
539
|
-
expect(result).toBe(originalNumber);
|
|
540
|
-
expect(result).toBe(7500);
|
|
541
|
-
});
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
describe("Extension Registration", () => {
|
|
545
|
-
function createMockExtensionAPI() {
|
|
546
|
-
const tools: unknown[] = [];
|
|
547
|
-
const commands: Map<string, unknown> = new Map();
|
|
548
|
-
const eventHandlers: Map<string, unknown[]> = new Map();
|
|
549
|
-
|
|
550
|
-
const api = {
|
|
551
|
-
registerTool: (tool: unknown) => tools.push(tool),
|
|
552
|
-
registerCommand: (name: string, command: unknown) => commands.set(name, command),
|
|
553
|
-
on: (event: string, handler: unknown) => {
|
|
554
|
-
const handlers = eventHandlers.get(event) || [];
|
|
555
|
-
handlers.push(handler);
|
|
556
|
-
eventHandlers.set(event, handlers);
|
|
557
|
-
},
|
|
558
|
-
getTools: () => tools,
|
|
559
|
-
getCommands: () => Array.from(commands.entries()),
|
|
560
|
-
getEventHandlers: (event: string) => eventHandlers.get(event) || [],
|
|
561
|
-
};
|
|
562
|
-
|
|
563
|
-
return api;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
it("should register exa_search tool", () => {
|
|
567
|
-
const api = createMockExtensionAPI();
|
|
568
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
569
|
-
|
|
570
|
-
const tools = api.getTools();
|
|
571
|
-
expect(tools.length).toBe(3);
|
|
572
|
-
|
|
573
|
-
const exaSearchTool = tools.find((t: unknown) => (t as { name: string }).name === "exa_search");
|
|
574
|
-
expect(exaSearchTool).toBeDefined();
|
|
575
|
-
expect((exaSearchTool as { name: string }).name).toBe("exa_search");
|
|
576
|
-
expect((exaSearchTool as { label: string }).label).toBe("Exa Search");
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
it("should register exa_fetch tool", () => {
|
|
580
|
-
const api = createMockExtensionAPI();
|
|
581
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
582
|
-
|
|
583
|
-
const tools = api.getTools();
|
|
584
|
-
const exaFetchTool = tools.find((t: unknown) => (t as { name: string }).name === "exa_fetch");
|
|
585
|
-
expect(exaFetchTool).toBeDefined();
|
|
586
|
-
expect((exaFetchTool as { name: string }).name).toBe("exa_fetch");
|
|
587
|
-
expect((exaFetchTool as { label: string }).label).toBe("Exa Fetch");
|
|
588
|
-
});
|
|
589
|
-
|
|
590
|
-
it("should register exa_code_context tool", () => {
|
|
591
|
-
const api = createMockExtensionAPI();
|
|
592
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
593
|
-
|
|
594
|
-
const tools = api.getTools();
|
|
595
|
-
const codeContextTool = tools.find(
|
|
596
|
-
(t: unknown) => (t as { name: string }).name === "exa_code_context",
|
|
597
|
-
);
|
|
598
|
-
expect(codeContextTool).toBeDefined();
|
|
599
|
-
expect((codeContextTool as { name: string }).name).toBe("exa_code_context");
|
|
600
|
-
expect((codeContextTool as { label: string }).label).toBe("Exa Code Context");
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
it("should have execute function on exa_code_context tool", () => {
|
|
604
|
-
const api = createMockExtensionAPI();
|
|
605
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
606
|
-
|
|
607
|
-
const tools = api.getTools();
|
|
608
|
-
const codeContextTool = tools.find(
|
|
609
|
-
(t: unknown) => (t as { name: string }).name === "exa_code_context",
|
|
610
|
-
) as { execute: unknown };
|
|
611
|
-
expect(typeof codeContextTool?.execute).toBe("function");
|
|
612
|
-
});
|
|
613
|
-
|
|
614
|
-
it("should register /exa-status command", () => {
|
|
615
|
-
const api = createMockExtensionAPI();
|
|
616
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
617
|
-
|
|
618
|
-
const commands = api.getCommands();
|
|
619
|
-
const exaStatusCmd = commands.find(([name]) => name === "exa-status");
|
|
620
|
-
expect(exaStatusCmd).toBeDefined();
|
|
621
|
-
expect(exaStatusCmd![0]).toBe("exa-status");
|
|
622
|
-
expect((exaStatusCmd![1] as { description: string }).description).toContain("API key");
|
|
623
|
-
});
|
|
624
|
-
|
|
625
|
-
it("should register session_start event handler", () => {
|
|
626
|
-
const api = createMockExtensionAPI();
|
|
627
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
628
|
-
|
|
629
|
-
const handlers = api.getEventHandlers("session_start");
|
|
630
|
-
expect(handlers.length).toBe(1);
|
|
631
|
-
});
|
|
632
|
-
|
|
633
|
-
it("should have execute function on exa_search tool", () => {
|
|
634
|
-
const api = createMockExtensionAPI();
|
|
635
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
636
|
-
|
|
637
|
-
const tools = api.getTools();
|
|
638
|
-
const exaSearchTool = tools.find(
|
|
639
|
-
(t: unknown) => (t as { name: string }).name === "exa_search",
|
|
640
|
-
) as { execute: unknown };
|
|
641
|
-
expect(typeof exaSearchTool?.execute).toBe("function");
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
it("should have execute function on exa_fetch tool", () => {
|
|
645
|
-
const api = createMockExtensionAPI();
|
|
646
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
647
|
-
|
|
648
|
-
const tools = api.getTools();
|
|
649
|
-
const exaFetchTool = tools.find(
|
|
650
|
-
(t: unknown) => (t as { name: string }).name === "exa_fetch",
|
|
651
|
-
) as { execute: unknown };
|
|
652
|
-
expect(typeof exaFetchTool?.execute).toBe("function");
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
it("should display cost in exa_fetch renderResult", () => {
|
|
656
|
-
const api = createMockExtensionAPI();
|
|
657
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
658
|
-
|
|
659
|
-
const tools = api.getTools();
|
|
660
|
-
const exaFetchTool = tools.find(
|
|
661
|
-
(t: unknown) => (t as { name: string }).name === "exa_fetch",
|
|
662
|
-
) as { renderResult: Function };
|
|
663
|
-
|
|
664
|
-
const mockTheme = {
|
|
665
|
-
fg: (name: string, text: string) => text,
|
|
666
|
-
};
|
|
667
|
-
|
|
668
|
-
const mockResult = {
|
|
669
|
-
content: [{ type: "text", text: "test" }],
|
|
670
|
-
details: {
|
|
671
|
-
url: "https://example.com",
|
|
672
|
-
title: "Test Page",
|
|
673
|
-
cost: { total: 0.000123 },
|
|
674
|
-
},
|
|
675
|
-
};
|
|
676
|
-
|
|
677
|
-
const rendered = exaFetchTool.renderResult(
|
|
678
|
-
mockResult,
|
|
679
|
-
{ expanded: false, isPartial: false },
|
|
680
|
-
mockTheme,
|
|
681
|
-
);
|
|
682
|
-
|
|
683
|
-
expect(rendered.text).toContain("Test Page");
|
|
684
|
-
expect(rendered.text).toContain("$0.000123");
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
it("should display cost without title in exa_fetch renderResult", () => {
|
|
688
|
-
const api = createMockExtensionAPI();
|
|
689
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
690
|
-
|
|
691
|
-
const tools = api.getTools();
|
|
692
|
-
const exaFetchTool = tools.find(
|
|
693
|
-
(t: unknown) => (t as { name: string }).name === "exa_fetch",
|
|
694
|
-
) as { renderResult: Function };
|
|
695
|
-
|
|
696
|
-
const mockTheme = {
|
|
697
|
-
fg: (name: string, text: string) => text,
|
|
698
|
-
};
|
|
699
|
-
|
|
700
|
-
const mockResult = {
|
|
701
|
-
content: [{ type: "text", text: "test" }],
|
|
702
|
-
details: {
|
|
703
|
-
url: "https://example.com",
|
|
704
|
-
cost: { total: 0.000456 },
|
|
705
|
-
},
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
const rendered = exaFetchTool.renderResult(
|
|
709
|
-
mockResult,
|
|
710
|
-
{ expanded: false, isPartial: false },
|
|
711
|
-
mockTheme,
|
|
712
|
-
);
|
|
713
|
-
|
|
714
|
-
expect(rendered.text).toContain("Fetched");
|
|
715
|
-
expect(rendered.text).toContain("$0.000456");
|
|
716
|
-
});
|
|
717
|
-
|
|
718
|
-
it("should display stats and cost in exa_code_context renderResult", () => {
|
|
719
|
-
const api = createMockExtensionAPI();
|
|
720
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
721
|
-
|
|
722
|
-
const tools = api.getTools();
|
|
723
|
-
const codeContextTool = tools.find(
|
|
724
|
-
(t: unknown) => (t as { name: string }).name === "exa_code_context",
|
|
725
|
-
) as { renderResult: Function };
|
|
726
|
-
|
|
727
|
-
const mockTheme = {
|
|
728
|
-
fg: (name: string, text: string) => text,
|
|
729
|
-
};
|
|
730
|
-
|
|
731
|
-
const mockResult = {
|
|
732
|
-
content: [{ type: "text", text: "test" }],
|
|
733
|
-
details: {
|
|
734
|
-
query: "React hooks",
|
|
735
|
-
resultsCount: 502,
|
|
736
|
-
outputTokens: 4805,
|
|
737
|
-
cost: { total: 1.0 },
|
|
738
|
-
},
|
|
739
|
-
};
|
|
740
|
-
|
|
741
|
-
const rendered = codeContextTool.renderResult(
|
|
742
|
-
mockResult,
|
|
743
|
-
{ expanded: false, isPartial: false },
|
|
744
|
-
mockTheme,
|
|
745
|
-
);
|
|
746
|
-
|
|
747
|
-
expect(rendered.text).toContain("502 sources");
|
|
748
|
-
expect(rendered.text).toContain("4805 tokens");
|
|
749
|
-
expect(rendered.text).toContain("$1.000000");
|
|
750
|
-
});
|
|
751
|
-
|
|
752
|
-
it("should display exa_code_context renderResult without cost", () => {
|
|
753
|
-
const api = createMockExtensionAPI();
|
|
754
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
755
|
-
|
|
756
|
-
const tools = api.getTools();
|
|
757
|
-
const codeContextTool = tools.find(
|
|
758
|
-
(t: unknown) => (t as { name: string }).name === "exa_code_context",
|
|
759
|
-
) as { renderResult: Function };
|
|
760
|
-
|
|
761
|
-
const mockTheme = {
|
|
762
|
-
fg: (name: string, text: string) => text,
|
|
763
|
-
};
|
|
764
|
-
|
|
765
|
-
const mockResult = {
|
|
766
|
-
content: [{ type: "text", text: "test" }],
|
|
767
|
-
details: {
|
|
768
|
-
query: "Express middleware",
|
|
769
|
-
resultsCount: 100,
|
|
770
|
-
outputTokens: 2000,
|
|
771
|
-
},
|
|
772
|
-
};
|
|
773
|
-
|
|
774
|
-
const rendered = codeContextTool.renderResult(
|
|
775
|
-
mockResult,
|
|
776
|
-
{ expanded: false, isPartial: false },
|
|
777
|
-
mockTheme,
|
|
778
|
-
);
|
|
779
|
-
|
|
780
|
-
expect(rendered.text).toContain("100 sources");
|
|
781
|
-
expect(rendered.text).toContain("2000 tokens");
|
|
782
|
-
expect(rendered.text).not.toContain("$");
|
|
783
|
-
});
|
|
784
|
-
|
|
785
|
-
it("should handle exa_code_context renderResult without details", () => {
|
|
786
|
-
const api = createMockExtensionAPI();
|
|
787
|
-
exaSearchExtension(api as unknown as import("@mariozechner/pi-coding-agent").ExtensionAPI);
|
|
788
|
-
|
|
789
|
-
const tools = api.getTools();
|
|
790
|
-
const codeContextTool = tools.find(
|
|
791
|
-
(t: unknown) => (t as { name: string }).name === "exa_code_context",
|
|
792
|
-
) as { renderResult: Function };
|
|
793
|
-
|
|
794
|
-
const mockTheme = {
|
|
795
|
-
fg: (name: string, text: string) => text,
|
|
796
|
-
};
|
|
797
|
-
|
|
798
|
-
const mockResult = {
|
|
799
|
-
content: [{ type: "text", text: "Some code context output here" }],
|
|
800
|
-
};
|
|
801
|
-
|
|
802
|
-
const rendered = codeContextTool.renderResult(
|
|
803
|
-
mockResult,
|
|
804
|
-
{ expanded: false, isPartial: false },
|
|
805
|
-
mockTheme,
|
|
806
|
-
);
|
|
807
|
-
|
|
808
|
-
expect(rendered.text).toContain("Some code context");
|
|
809
|
-
});
|
|
810
|
-
});
|
|
811
|
-
|
|
812
|
-
describe("Tool Execute Validation", () => {
|
|
813
|
-
it("should validate search parameters structure", () => {
|
|
814
|
-
const searchParams = {
|
|
815
|
-
query: "test query",
|
|
816
|
-
contentType: "highlights" as const,
|
|
817
|
-
numResults: 10,
|
|
818
|
-
};
|
|
819
|
-
|
|
820
|
-
expect(typeof searchParams.query).toBe("string");
|
|
821
|
-
expect(searchParams.query.trim().length).toBeGreaterThan(0);
|
|
822
|
-
expect(["text", "highlights", "summary", "none"].includes(searchParams.contentType)).toBe(true);
|
|
823
|
-
expect(searchParams.numResults).toBeGreaterThanOrEqual(1);
|
|
824
|
-
expect(searchParams.numResults).toBeLessThanOrEqual(100);
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it("should validate fetch parameters structure", () => {
|
|
828
|
-
const fetchParams = {
|
|
829
|
-
url: "https://example.com",
|
|
830
|
-
contentType: "text" as const,
|
|
831
|
-
maxCharacters: 5000,
|
|
832
|
-
};
|
|
833
|
-
|
|
834
|
-
expect(typeof fetchParams.url).toBe("string");
|
|
835
|
-
expect(fetchParams.url.length).toBeGreaterThan(0);
|
|
836
|
-
expect(["text", "highlights", "summary"].includes(fetchParams.contentType)).toBe(true);
|
|
837
|
-
expect(fetchParams.maxCharacters).toBeGreaterThan(0);
|
|
838
|
-
});
|
|
839
|
-
|
|
840
|
-
it("should handle getContents options mapping", () => {
|
|
841
|
-
const url = "https://example.com";
|
|
842
|
-
const options = {
|
|
843
|
-
text: true,
|
|
844
|
-
maxCharacters: 10000,
|
|
845
|
-
};
|
|
846
|
-
|
|
847
|
-
expect(url).toBeTruthy();
|
|
848
|
-
expect(options.text).toBe(true);
|
|
849
|
-
expect(options.maxCharacters).toBeGreaterThan(0);
|
|
850
|
-
});
|
|
851
|
-
});
|