@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.
- package/dist/cli.js +4 -2
- package/dist/cli.js.map +1 -1
- package/dist/tools/bash.test.d.ts +2 -0
- package/dist/tools/bash.test.d.ts.map +1 -0
- package/dist/tools/bash.test.js +284 -0
- package/dist/tools/bash.test.js.map +1 -0
- package/dist/tools/database.js +3 -1
- package/dist/tools/database.js.map +1 -1
- package/dist/tools/fetch.test.d.ts +2 -0
- package/dist/tools/fetch.test.d.ts.map +1 -0
- package/dist/tools/fetch.test.js +363 -0
- package/dist/tools/fetch.test.js.map +1 -0
- package/dist/tools/files.test.d.ts +2 -0
- package/dist/tools/files.test.d.ts.map +1 -0
- package/dist/tools/files.test.js +395 -0
- package/dist/tools/files.test.js.map +1 -0
- package/dist/tools/git.test.d.ts +2 -0
- package/dist/tools/git.test.d.ts.map +1 -0
- package/dist/tools/git.test.js +303 -0
- package/dist/tools/git.test.js.map +1 -0
- package/dist/tools/search.test.d.ts +2 -0
- package/dist/tools/search.test.d.ts.map +1 -0
- package/dist/tools/search.test.js +311 -0
- package/dist/tools/search.test.js.map +1 -0
- package/dist/updater.js +1 -1
- package/dist/updater.js.map +1 -1
- package/package.json +1 -1
|
@@ -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 @@
|
|
|
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
|