@kernel.chat/kbot 3.2.0 → 3.2.1

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.
@@ -0,0 +1,303 @@
1
+ // kbot Git Tools Tests
2
+ import { describe, it, expect, beforeAll, afterAll } from 'vitest';
3
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ import { tmpdir } from 'node:os';
6
+ import { execSync } from 'node:child_process';
7
+ import { executeTool, getTool } from './index.js';
8
+ import { registerGitTools } from './git.js';
9
+ // Register once
10
+ registerGitTools();
11
+ // Temp git repo for tests
12
+ const TEST_DIR = join(tmpdir(), 'kbot-git-test-' + Date.now());
13
+ /** Run a command in the test repo */
14
+ function run(cmd) {
15
+ return execSync(cmd, { cwd: TEST_DIR, encoding: 'utf-8' }).trim();
16
+ }
17
+ beforeAll(() => {
18
+ mkdirSync(TEST_DIR, { recursive: true });
19
+ // Initialize a git repo with an initial commit
20
+ run('git init');
21
+ run('git config user.email "test@kbot.dev"');
22
+ run('git config user.name "KBot Test"');
23
+ writeFileSync(join(TEST_DIR, 'README.md'), '# Test Repo');
24
+ run('git add .');
25
+ run('git commit -m "initial commit"');
26
+ // Override cwd so git tools operate on our test repo
27
+ process.chdir(TEST_DIR);
28
+ });
29
+ afterAll(() => {
30
+ try {
31
+ rmSync(TEST_DIR, { recursive: true, force: true });
32
+ }
33
+ catch { }
34
+ });
35
+ // ─────────────────────────────────────────────────────────────────────
36
+ // 1. Registration
37
+ // ─────────────────────────────────────────────────────────────────────
38
+ describe('Git Tools Registration', () => {
39
+ it('registers git_status', () => {
40
+ const tool = getTool('git_status');
41
+ expect(tool).toBeTruthy();
42
+ expect(tool.tier).toBe('free');
43
+ });
44
+ it('registers git_diff', () => {
45
+ const tool = getTool('git_diff');
46
+ expect(tool).toBeTruthy();
47
+ expect(tool.parameters.staged).toBeTruthy();
48
+ });
49
+ it('registers git_log', () => {
50
+ const tool = getTool('git_log');
51
+ expect(tool).toBeTruthy();
52
+ });
53
+ it('registers git_commit', () => {
54
+ const tool = getTool('git_commit');
55
+ expect(tool).toBeTruthy();
56
+ expect(tool.parameters.message.required).toBe(true);
57
+ });
58
+ it('registers git_branch', () => {
59
+ const tool = getTool('git_branch');
60
+ expect(tool).toBeTruthy();
61
+ expect(tool.parameters.name.required).toBe(true);
62
+ });
63
+ it('registers git_push', () => {
64
+ const tool = getTool('git_push');
65
+ expect(tool).toBeTruthy();
66
+ });
67
+ });
68
+ // ─────────────────────────────────────────────────────────────────────
69
+ // 2. git_status
70
+ // ─────────────────────────────────────────────────────────────────────
71
+ describe('git_status', () => {
72
+ it('returns empty on clean repo', async () => {
73
+ const result = await executeTool({
74
+ id: 'gs-1',
75
+ name: 'git_status',
76
+ arguments: {},
77
+ });
78
+ expect(result.error).toBeUndefined();
79
+ // Clean repo should return empty or whitespace
80
+ expect(result.result.trim()).toBe('');
81
+ });
82
+ it('shows modified files', async () => {
83
+ writeFileSync(join(TEST_DIR, 'README.md'), '# Modified');
84
+ const result = await executeTool({
85
+ id: 'gs-2',
86
+ name: 'git_status',
87
+ arguments: {},
88
+ });
89
+ expect(result.error).toBeUndefined();
90
+ expect(result.result).toContain('README.md');
91
+ expect(result.result).toMatch(/M\s/);
92
+ // Restore
93
+ run('git checkout -- README.md');
94
+ });
95
+ it('shows untracked files', async () => {
96
+ writeFileSync(join(TEST_DIR, 'newfile.txt'), 'new');
97
+ const result = await executeTool({
98
+ id: 'gs-3',
99
+ name: 'git_status',
100
+ arguments: {},
101
+ });
102
+ expect(result.error).toBeUndefined();
103
+ expect(result.result).toContain('newfile.txt');
104
+ expect(result.result).toContain('??');
105
+ // Cleanup
106
+ run('rm newfile.txt');
107
+ });
108
+ });
109
+ // ─────────────────────────────────────────────────────────────────────
110
+ // 3. git_diff
111
+ // ─────────────────────────────────────────────────────────────────────
112
+ describe('git_diff', () => {
113
+ it('returns "No changes" on clean repo', async () => {
114
+ const result = await executeTool({
115
+ id: 'gd-1',
116
+ name: 'git_diff',
117
+ arguments: {},
118
+ });
119
+ expect(result.error).toBeUndefined();
120
+ expect(result.result).toBe('No changes');
121
+ });
122
+ it('shows unstaged changes', async () => {
123
+ writeFileSync(join(TEST_DIR, 'README.md'), '# Changed Content');
124
+ const result = await executeTool({
125
+ id: 'gd-2',
126
+ name: 'git_diff',
127
+ arguments: {},
128
+ });
129
+ expect(result.error).toBeUndefined();
130
+ expect(result.result).toContain('Changed Content');
131
+ expect(result.result).toContain('diff --git');
132
+ // Restore
133
+ run('git checkout -- README.md');
134
+ });
135
+ it('shows staged changes with staged flag', async () => {
136
+ writeFileSync(join(TEST_DIR, 'README.md'), '# Staged Change');
137
+ run('git add README.md');
138
+ const result = await executeTool({
139
+ id: 'gd-3',
140
+ name: 'git_diff',
141
+ arguments: { staged: true },
142
+ });
143
+ expect(result.error).toBeUndefined();
144
+ expect(result.result).toContain('Staged Change');
145
+ // Unstage and restore
146
+ run('git reset HEAD README.md');
147
+ run('git checkout -- README.md');
148
+ });
149
+ it('limits diff to a specific path', async () => {
150
+ writeFileSync(join(TEST_DIR, 'README.md'), '# Path Diff');
151
+ writeFileSync(join(TEST_DIR, 'other.txt'), 'other change');
152
+ run('git add other.txt');
153
+ const result = await executeTool({
154
+ id: 'gd-4',
155
+ name: 'git_diff',
156
+ arguments: { path: 'README.md' },
157
+ });
158
+ expect(result.error).toBeUndefined();
159
+ expect(result.result).toContain('Path Diff');
160
+ expect(result.result).not.toContain('other change');
161
+ // Restore
162
+ run('git checkout -- README.md');
163
+ run('git reset HEAD other.txt');
164
+ run('rm -f other.txt');
165
+ });
166
+ });
167
+ // ─────────────────────────────────────────────────────────────────────
168
+ // 4. git_log
169
+ // ─────────────────────────────────────────────────────────────────────
170
+ describe('git_log', () => {
171
+ it('shows commit history', async () => {
172
+ const result = await executeTool({
173
+ id: 'gl-1',
174
+ name: 'git_log',
175
+ arguments: {},
176
+ });
177
+ expect(result.error).toBeUndefined();
178
+ expect(result.result).toContain('initial commit');
179
+ });
180
+ it('respects count parameter', async () => {
181
+ // Add a second commit
182
+ writeFileSync(join(TEST_DIR, 'log-test.txt'), 'log test');
183
+ run('git add log-test.txt');
184
+ run('git commit -m "second commit"');
185
+ const result = await executeTool({
186
+ id: 'gl-2',
187
+ name: 'git_log',
188
+ arguments: { count: 1 },
189
+ });
190
+ expect(result.error).toBeUndefined();
191
+ expect(result.result).toContain('second commit');
192
+ // With count=1, should not show initial commit (oneline format = 1 line)
193
+ expect(result.result.split('\n').length).toBe(1);
194
+ });
195
+ it('uses oneline format by default', async () => {
196
+ const result = await executeTool({
197
+ id: 'gl-3',
198
+ name: 'git_log',
199
+ arguments: { count: 2 },
200
+ });
201
+ expect(result.error).toBeUndefined();
202
+ // Oneline format: hash + message per line
203
+ const lines = result.result.trim().split('\n');
204
+ expect(lines.length).toBe(2);
205
+ });
206
+ });
207
+ // ─────────────────────────────────────────────────────────────────────
208
+ // 5. git_commit
209
+ // ─────────────────────────────────────────────────────────────────────
210
+ describe('git_commit', () => {
211
+ it('commits staged files with a message', async () => {
212
+ writeFileSync(join(TEST_DIR, 'commit-test.txt'), 'commit me');
213
+ run('git add commit-test.txt');
214
+ const result = await executeTool({
215
+ id: 'gc-1',
216
+ name: 'git_commit',
217
+ arguments: { message: 'test commit via tool' },
218
+ });
219
+ expect(result.error).toBeUndefined();
220
+ expect(result.result).toContain('test commit via tool');
221
+ // Verify it's in the log
222
+ const log = run('git log --oneline -1');
223
+ expect(log).toContain('test commit via tool');
224
+ });
225
+ it('stages and commits specified files', async () => {
226
+ writeFileSync(join(TEST_DIR, 'auto-stage.txt'), 'auto staged');
227
+ const result = await executeTool({
228
+ id: 'gc-2',
229
+ name: 'git_commit',
230
+ arguments: { message: 'auto-stage commit', files: ['auto-stage.txt'] },
231
+ });
232
+ expect(result.error).toBeUndefined();
233
+ expect(result.result).toContain('auto-stage commit');
234
+ });
235
+ it('handles commit with nothing staged', async () => {
236
+ const result = await executeTool({
237
+ id: 'gc-3',
238
+ name: 'git_commit',
239
+ arguments: { message: 'empty commit attempt' },
240
+ });
241
+ // git commit with nothing staged should error
242
+ expect(result.error).toBe(true);
243
+ expect(result.result).toBeTruthy();
244
+ });
245
+ });
246
+ // ─────────────────────────────────────────────────────────────────────
247
+ // 6. git_branch
248
+ // ─────────────────────────────────────────────────────────────────────
249
+ describe('git_branch', () => {
250
+ it('creates a new branch', async () => {
251
+ const result = await executeTool({
252
+ id: 'gb-1',
253
+ name: 'git_branch',
254
+ arguments: { name: 'test-branch-create', create: true },
255
+ });
256
+ expect(result.error).toBeUndefined();
257
+ // git checkout -b outputs to stderr — stdout may be empty, which is fine
258
+ // Verify the branch was created by checking git branch list
259
+ const branches = run('git branch');
260
+ expect(branches).toContain('test-branch-create');
261
+ // Switch back
262
+ run('git checkout master 2>/dev/null || git checkout main 2>/dev/null');
263
+ });
264
+ it('switches to an existing branch', async () => {
265
+ // First ensure the branch exists
266
+ run('git branch test-branch-switch 2>/dev/null || true');
267
+ const result = await executeTool({
268
+ id: 'gb-2',
269
+ name: 'git_branch',
270
+ arguments: { name: 'test-branch-switch' },
271
+ });
272
+ expect(result.error).toBeUndefined();
273
+ // Verify we're on the branch
274
+ const current = run('git rev-parse --abbrev-ref HEAD');
275
+ expect(current).toBe('test-branch-switch');
276
+ // Switch back
277
+ run('git checkout master 2>/dev/null || git checkout main 2>/dev/null');
278
+ });
279
+ it('returns error for nonexistent branch', async () => {
280
+ const result = await executeTool({
281
+ id: 'gb-3',
282
+ name: 'git_branch',
283
+ arguments: { name: 'nonexistent-branch-xyz' },
284
+ });
285
+ expect(result.error).toBe(true);
286
+ });
287
+ });
288
+ // ─────────────────────────────────────────────────────────────────────
289
+ // 7. git_push (should fail — no remote configured)
290
+ // ─────────────────────────────────────────────────────────────────────
291
+ describe('git_push', () => {
292
+ it('returns error when no remote is configured', async () => {
293
+ const result = await executeTool({
294
+ id: 'gp-1',
295
+ name: 'git_push',
296
+ arguments: {},
297
+ });
298
+ expect(result.error).toBe(true);
299
+ // No remote configured, so it should fail
300
+ expect(result.result).toBeTruthy();
301
+ });
302
+ });
303
+ //# sourceMappingURL=git.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"git.test.js","sourceRoot":"","sources":["../../src/tools/git.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=search.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../src/tools/search.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,311 @@
1
+ // kbot Search Tools Tests — Mocked (no real network calls)
2
+ import { describe, it, expect, vi, beforeAll, afterAll } from 'vitest';
3
+ import { executeTool, getTool } from './index.js';
4
+ import { registerSearchTools } from './search.js';
5
+ // Register once
6
+ registerSearchTools();
7
+ // ─────────────────────────────────────────────────────────────────────
8
+ // Mock global fetch to avoid real network calls
9
+ // ─────────────────────────────────────────────────────────────────────
10
+ const originalFetch = globalThis.fetch;
11
+ beforeAll(() => {
12
+ globalThis.fetch = vi.fn();
13
+ });
14
+ afterAll(() => {
15
+ globalThis.fetch = originalFetch;
16
+ });
17
+ function mockFetch(impl) {
18
+ ;
19
+ globalThis.fetch.mockImplementation(impl);
20
+ }
21
+ function mockFetchReset() {
22
+ ;
23
+ globalThis.fetch.mockReset();
24
+ }
25
+ // ─────────────────────────────────────────────────────────────────────
26
+ // 1. Registration
27
+ // ─────────────────────────────────────────────────────────────────────
28
+ describe('Search Tools Registration', () => {
29
+ it('registers web_search', () => {
30
+ const tool = getTool('web_search');
31
+ expect(tool).toBeTruthy();
32
+ expect(tool.tier).toBe('free');
33
+ expect(tool.parameters.query.required).toBe(true);
34
+ });
35
+ it('registers research', () => {
36
+ const tool = getTool('research');
37
+ expect(tool).toBeTruthy();
38
+ expect(tool.tier).toBe('free');
39
+ expect(tool.parameters.topic.required).toBe(true);
40
+ });
41
+ });
42
+ // ─────────────────────────────────────────────────────────────────────
43
+ // 2. web_search — DuckDuckGo success
44
+ // ─────────────────────────────────────────────────────────────────────
45
+ describe('web_search', () => {
46
+ it('returns DuckDuckGo instant answer', async () => {
47
+ mockFetch(async (url) => {
48
+ const urlStr = String(url);
49
+ if (urlStr.includes('duckduckgo.com')) {
50
+ return new Response(JSON.stringify({
51
+ AbstractText: 'TypeScript is a typed superset of JavaScript.',
52
+ AbstractSource: 'Wikipedia',
53
+ RelatedTopics: [],
54
+ }), { status: 200 });
55
+ }
56
+ // Wikipedia fallback
57
+ if (urlStr.includes('wikipedia.org')) {
58
+ return new Response(JSON.stringify({
59
+ extract: 'TypeScript is a programming language developed by Microsoft.',
60
+ }), { status: 200 });
61
+ }
62
+ return new Response('', { status: 404 });
63
+ });
64
+ const result = await executeTool({
65
+ id: 'ws-1',
66
+ name: 'web_search',
67
+ arguments: { query: 'TypeScript' },
68
+ });
69
+ expect(result.error).toBeUndefined();
70
+ expect(result.result).toContain('TypeScript');
71
+ expect(result.result).toContain('Wikipedia');
72
+ });
73
+ it('returns related topics from DuckDuckGo', async () => {
74
+ mockFetch(async (url) => {
75
+ const urlStr = String(url);
76
+ if (urlStr.includes('duckduckgo.com')) {
77
+ return new Response(JSON.stringify({
78
+ AbstractText: '',
79
+ RelatedTopics: [
80
+ { Text: 'React is a JavaScript library' },
81
+ { Text: 'Vue.js is a progressive framework' },
82
+ ],
83
+ }), { status: 200 });
84
+ }
85
+ return new Response(JSON.stringify({}), { status: 200 });
86
+ });
87
+ const result = await executeTool({
88
+ id: 'ws-2',
89
+ name: 'web_search',
90
+ arguments: { query: 'frontend frameworks' },
91
+ });
92
+ expect(result.error).toBeUndefined();
93
+ expect(result.result).toContain('React is a JavaScript library');
94
+ expect(result.result).toContain('Vue.js');
95
+ });
96
+ it('includes StackOverflow for programming queries', async () => {
97
+ mockFetch(async (url) => {
98
+ const urlStr = String(url);
99
+ if (urlStr.includes('duckduckgo.com')) {
100
+ return new Response(JSON.stringify({ AbstractText: '', RelatedTopics: [] }), { status: 200 });
101
+ }
102
+ if (urlStr.includes('wikipedia.org')) {
103
+ return new Response(JSON.stringify({}), { status: 200 });
104
+ }
105
+ if (urlStr.includes('stackexchange.com')) {
106
+ return new Response(JSON.stringify({
107
+ items: [
108
+ { title: 'How to install npm packages', excerpt: 'Use npm install to add packages to your project...' },
109
+ ],
110
+ }), { status: 200 });
111
+ }
112
+ return new Response('', { status: 404 });
113
+ });
114
+ const result = await executeTool({
115
+ id: 'ws-3',
116
+ name: 'web_search',
117
+ arguments: { query: 'how to install npm packages' },
118
+ });
119
+ expect(result.error).toBeUndefined();
120
+ expect(result.result).toContain('Stack Overflow');
121
+ expect(result.result).toContain('npm');
122
+ });
123
+ it('returns fallback message when no results', async () => {
124
+ mockFetch(async () => {
125
+ return new Response(JSON.stringify({ AbstractText: '', RelatedTopics: [] }), { status: 200 });
126
+ });
127
+ const result = await executeTool({
128
+ id: 'ws-4',
129
+ name: 'web_search',
130
+ arguments: { query: 'xyznonexistentquery12345' },
131
+ });
132
+ expect(result.error).toBeUndefined();
133
+ expect(result.result).toContain('No instant results');
134
+ expect(result.result).toContain('url_fetch');
135
+ });
136
+ it('handles fetch failures gracefully', async () => {
137
+ mockFetch(async () => {
138
+ throw new Error('Network error');
139
+ });
140
+ const result = await executeTool({
141
+ id: 'ws-5',
142
+ name: 'web_search',
143
+ arguments: { query: 'test query' },
144
+ });
145
+ // Should not throw — should return fallback
146
+ expect(result.error).toBeUndefined();
147
+ expect(result.result).toContain('No instant results');
148
+ });
149
+ it('handles Wikipedia 404 gracefully', async () => {
150
+ mockFetch(async (url) => {
151
+ const urlStr = String(url);
152
+ if (urlStr.includes('duckduckgo.com')) {
153
+ return new Response(JSON.stringify({
154
+ AbstractText: 'Some answer',
155
+ AbstractSource: 'DDG',
156
+ RelatedTopics: [],
157
+ }), { status: 200 });
158
+ }
159
+ // Wikipedia returns 404
160
+ return new Response('Not found', { status: 404 });
161
+ });
162
+ const result = await executeTool({
163
+ id: 'ws-6',
164
+ name: 'web_search',
165
+ arguments: { query: 'obscure topic' },
166
+ });
167
+ expect(result.error).toBeUndefined();
168
+ expect(result.result).toContain('Some answer');
169
+ });
170
+ });
171
+ // ─────────────────────────────────────────────────────────────────────
172
+ // 3. research
173
+ // ─────────────────────────────────────────────────────────────────────
174
+ describe('research', () => {
175
+ it('fetches provided URLs and DDG/Wikipedia', async () => {
176
+ mockFetch(async (url) => {
177
+ const urlStr = String(url);
178
+ if (urlStr.includes('example.com')) {
179
+ return new Response('<html><body><p>Example content about AI</p></body></html>', {
180
+ status: 200,
181
+ headers: { 'content-type': 'text/html' },
182
+ });
183
+ }
184
+ if (urlStr.includes('duckduckgo.com')) {
185
+ return new Response(JSON.stringify({
186
+ AbstractText: 'AI is a branch of computer science.',
187
+ AbstractSource: 'Wikipedia',
188
+ RelatedTopics: [],
189
+ }), { status: 200 });
190
+ }
191
+ if (urlStr.includes('wikipedia.org')) {
192
+ return new Response(JSON.stringify({
193
+ extract: 'Artificial intelligence is intelligence demonstrated by machines.',
194
+ }), { status: 200 });
195
+ }
196
+ return new Response('', { status: 404 });
197
+ });
198
+ const result = await executeTool({
199
+ id: 'r-1',
200
+ name: 'research',
201
+ arguments: {
202
+ topic: 'artificial intelligence',
203
+ urls: ['https://example.com/ai-page'],
204
+ },
205
+ });
206
+ expect(result.error).toBeUndefined();
207
+ expect(result.result).toContain('Research results for: artificial intelligence');
208
+ expect(result.result).toContain('Example content about AI');
209
+ expect(result.result).toContain('AI is a branch of computer science');
210
+ expect(result.result).toContain('Wikipedia');
211
+ });
212
+ it('limits URLs to 5', async () => {
213
+ let fetchCount = 0;
214
+ mockFetch(async (url) => {
215
+ const urlStr = String(url);
216
+ if (urlStr.includes('example.com')) {
217
+ fetchCount++;
218
+ return new Response(`Content ${fetchCount}`, { status: 200 });
219
+ }
220
+ return new Response(JSON.stringify({ AbstractText: '', RelatedTopics: [] }), { status: 200 });
221
+ });
222
+ const urls = Array.from({ length: 10 }, (_, i) => `https://example.com/page${i}`);
223
+ const result = await executeTool({
224
+ id: 'r-2',
225
+ name: 'research',
226
+ arguments: { topic: 'test', urls },
227
+ });
228
+ expect(result.error).toBeUndefined();
229
+ // Only first 5 URLs should be fetched
230
+ expect(fetchCount).toBeLessThanOrEqual(5);
231
+ });
232
+ it('returns fallback for no results', async () => {
233
+ mockFetch(async () => {
234
+ throw new Error('Network failure');
235
+ });
236
+ const result = await executeTool({
237
+ id: 'r-3',
238
+ name: 'research',
239
+ arguments: { topic: 'impossible topic' },
240
+ });
241
+ expect(result.error).toBeUndefined();
242
+ expect(result.result).toContain('No research results found');
243
+ });
244
+ it('strips HTML from fetched URLs', async () => {
245
+ mockFetch(async (url) => {
246
+ const urlStr = String(url);
247
+ if (urlStr.includes('example.com')) {
248
+ return new Response('<html><head><script>alert("xss")</script><style>body{color:red}</style></head><body><p>Clean text</p></body></html>', { status: 200 });
249
+ }
250
+ return new Response(JSON.stringify({ AbstractText: '', RelatedTopics: [] }), { status: 200 });
251
+ });
252
+ const result = await executeTool({
253
+ id: 'r-4',
254
+ name: 'research',
255
+ arguments: { topic: 'html stripping', urls: ['https://example.com'] },
256
+ });
257
+ expect(result.error).toBeUndefined();
258
+ expect(result.result).toContain('Clean text');
259
+ expect(result.result).not.toContain('alert');
260
+ expect(result.result).not.toContain('<script');
261
+ expect(result.result).not.toContain('<style');
262
+ });
263
+ it('handles mixed URL success and failure', async () => {
264
+ mockFetch(async (url) => {
265
+ const urlStr = String(url);
266
+ if (urlStr.includes('good.com')) {
267
+ return new Response('Good content', { status: 200 });
268
+ }
269
+ if (urlStr.includes('bad.com')) {
270
+ throw new Error('Connection refused');
271
+ }
272
+ if (urlStr.includes('duckduckgo.com')) {
273
+ return new Response(JSON.stringify({ AbstractText: 'DDG result', AbstractSource: 'DDG', RelatedTopics: [] }), { status: 200 });
274
+ }
275
+ return new Response(JSON.stringify({}), { status: 200 });
276
+ });
277
+ const result = await executeTool({
278
+ id: 'r-5',
279
+ name: 'research',
280
+ arguments: {
281
+ topic: 'test',
282
+ urls: ['https://good.com/page', 'https://bad.com/page'],
283
+ },
284
+ });
285
+ expect(result.error).toBeUndefined();
286
+ expect(result.result).toContain('Good content');
287
+ expect(result.result).toContain('DDG result');
288
+ // bad.com should be silently skipped
289
+ });
290
+ it('works without urls parameter', async () => {
291
+ mockFetch(async (url) => {
292
+ const urlStr = String(url);
293
+ if (urlStr.includes('duckduckgo.com')) {
294
+ return new Response(JSON.stringify({
295
+ AbstractText: 'Research result',
296
+ AbstractSource: 'Source',
297
+ RelatedTopics: [{ Text: 'Related item' }],
298
+ }), { status: 200 });
299
+ }
300
+ return new Response(JSON.stringify({ extract: 'Wiki content' }), { status: 200 });
301
+ });
302
+ const result = await executeTool({
303
+ id: 'r-6',
304
+ name: 'research',
305
+ arguments: { topic: 'test topic' },
306
+ });
307
+ expect(result.error).toBeUndefined();
308
+ expect(result.result).toContain('Research result');
309
+ });
310
+ });
311
+ //# sourceMappingURL=search.test.js.map