@gotza02/sequential-thinking 2026.2.19 → 2026.2.21
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/dist/codestore.d.ts +28 -0
- package/dist/graph.d.ts +60 -0
- package/dist/graph.js +19 -0
- package/dist/http-server.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/lib.d.ts +67 -0
- package/dist/lib.js +323 -83
- package/dist/notes.d.ts +25 -0
- package/dist/system_test.d.ts +1 -0
- package/dist/test_ts_req.d.ts +1 -0
- package/dist/tools/codestore.d.ts +3 -0
- package/dist/tools/coding.d.ts +3 -0
- package/dist/tools/coding.js +40 -3
- package/dist/tools/filesystem.d.ts +2 -0
- package/dist/tools/graph.d.ts +3 -0
- package/dist/tools/graph.js +18 -0
- package/dist/tools/human.d.ts +65 -0
- package/dist/tools/human.js +305 -0
- package/dist/tools/notes.d.ts +3 -0
- package/dist/tools/thinking.d.ts +3 -0
- package/dist/tools/thinking.js +137 -65
- package/dist/tools/web.d.ts +2 -0
- package/dist/utils.d.ts +32 -0
- package/package.json +3 -1
- package/dist/chaos.test.js +0 -72
- package/dist/codestore.test.js +0 -59
- package/dist/coding.test.js +0 -130
- package/dist/e2e.test.js +0 -122
- package/dist/filesystem.test.js +0 -189
- package/dist/graph.test.js +0 -150
- package/dist/graph_repro.test.js +0 -63
- package/dist/integration.test.js +0 -58
- package/dist/notes.test.js +0 -74
- package/dist/registration.test.js +0 -39
- package/dist/repro_dollar.js +0 -30
- package/dist/repro_dollar_simple.js +0 -22
- package/dist/repro_history.js +0 -41
- package/dist/repro_path.js +0 -17
- package/dist/repro_search.test.js +0 -79
- package/dist/repro_ts_req.js +0 -3
- package/dist/server.test.js +0 -127
- package/dist/stress.test.js +0 -68
- package/dist/utils.test.js +0 -40
- package/dist/verify_cache.test.js +0 -27
- package/dist/verify_edit.test.js +0 -66
- package/dist/verify_notes.test.js +0 -36
- package/dist/verify_viz.test.js +0 -25
- package/dist/web_fallback.test.js +0 -103
- package/dist/web_read.test.js +0 -60
package/dist/server.test.js
DELETED
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterAll } from 'vitest';
|
|
2
|
-
import { SequentialThinkingServer } from './lib.js';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
describe('SequentialThinkingServer', () => {
|
|
5
|
-
let server;
|
|
6
|
-
const testStoragePath = 'test_thoughts.json';
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
if (fs.existsSync(testStoragePath)) {
|
|
9
|
-
fs.unlinkSync(testStoragePath);
|
|
10
|
-
}
|
|
11
|
-
server = new SequentialThinkingServer(testStoragePath);
|
|
12
|
-
});
|
|
13
|
-
afterAll(() => {
|
|
14
|
-
if (fs.existsSync(testStoragePath)) {
|
|
15
|
-
fs.unlinkSync(testStoragePath);
|
|
16
|
-
}
|
|
17
|
-
});
|
|
18
|
-
it('should process a basic linear thought', async () => {
|
|
19
|
-
const input = {
|
|
20
|
-
thought: "First step",
|
|
21
|
-
thoughtNumber: 1,
|
|
22
|
-
totalThoughts: 3,
|
|
23
|
-
nextThoughtNeeded: true,
|
|
24
|
-
thoughtType: 'analysis'
|
|
25
|
-
};
|
|
26
|
-
const result = await server.processThought(input);
|
|
27
|
-
expect(result.isError).toBeUndefined();
|
|
28
|
-
const content = JSON.parse(result.content[0].text);
|
|
29
|
-
expect(content.thoughtNumber).toBe(1);
|
|
30
|
-
expect(content.thoughtHistoryLength).toBe(1);
|
|
31
|
-
});
|
|
32
|
-
it('should handle branching correctly', async () => {
|
|
33
|
-
// Initial thought
|
|
34
|
-
await server.processThought({
|
|
35
|
-
thought: "Root thought",
|
|
36
|
-
thoughtNumber: 1,
|
|
37
|
-
totalThoughts: 3,
|
|
38
|
-
nextThoughtNeeded: true
|
|
39
|
-
});
|
|
40
|
-
// Branch 1
|
|
41
|
-
const branch1Input = {
|
|
42
|
-
thought: "Alternative A",
|
|
43
|
-
thoughtNumber: 2,
|
|
44
|
-
totalThoughts: 3,
|
|
45
|
-
nextThoughtNeeded: true,
|
|
46
|
-
branchFromThought: 1,
|
|
47
|
-
branchId: "branch-A",
|
|
48
|
-
thoughtType: 'generation'
|
|
49
|
-
};
|
|
50
|
-
const result1 = await server.processThought(branch1Input);
|
|
51
|
-
const content1 = JSON.parse(result1.content[0].text);
|
|
52
|
-
expect(content1.branches).toContain("1-branch-A");
|
|
53
|
-
// Branch 2
|
|
54
|
-
const branch2Input = {
|
|
55
|
-
thought: "Alternative B",
|
|
56
|
-
thoughtNumber: 2,
|
|
57
|
-
totalThoughts: 3,
|
|
58
|
-
nextThoughtNeeded: true,
|
|
59
|
-
branchFromThought: 1,
|
|
60
|
-
branchId: "branch-B",
|
|
61
|
-
thoughtType: 'generation'
|
|
62
|
-
};
|
|
63
|
-
const result2 = await server.processThought(branch2Input);
|
|
64
|
-
const content2 = JSON.parse(result2.content[0].text);
|
|
65
|
-
expect(content2.branches).toContain("1-branch-B");
|
|
66
|
-
expect(content2.branches.length).toBe(2);
|
|
67
|
-
});
|
|
68
|
-
it('should handle evaluation with scores', async () => {
|
|
69
|
-
const input = {
|
|
70
|
-
thought: "Evaluating option X",
|
|
71
|
-
thoughtNumber: 3,
|
|
72
|
-
totalThoughts: 5,
|
|
73
|
-
nextThoughtNeeded: true,
|
|
74
|
-
thoughtType: 'evaluation',
|
|
75
|
-
score: 8,
|
|
76
|
-
options: ['Option X', 'Option Y']
|
|
77
|
-
};
|
|
78
|
-
const result = await server.processThought(input);
|
|
79
|
-
expect(result.isError).toBeUndefined();
|
|
80
|
-
// Since we don't return the score in the simple JSON response (only in logs or history),
|
|
81
|
-
// we mainly check that it doesn't crash and processes correctly.
|
|
82
|
-
// If we exposed history in the response, we could check that too.
|
|
83
|
-
});
|
|
84
|
-
it('should adjust totalThoughts if thoughtNumber exceeds it', async () => {
|
|
85
|
-
const input = {
|
|
86
|
-
thought: "Unexpected long process",
|
|
87
|
-
thoughtNumber: 6,
|
|
88
|
-
totalThoughts: 5,
|
|
89
|
-
nextThoughtNeeded: true
|
|
90
|
-
};
|
|
91
|
-
const result = await server.processThought(input);
|
|
92
|
-
const content = JSON.parse(result.content[0].text);
|
|
93
|
-
expect(content.totalThoughts).toBe(6);
|
|
94
|
-
});
|
|
95
|
-
it('should clear thought history', async () => {
|
|
96
|
-
await server.processThought({
|
|
97
|
-
thought: "To be forgotten",
|
|
98
|
-
thoughtNumber: 1,
|
|
99
|
-
totalThoughts: 1,
|
|
100
|
-
nextThoughtNeeded: false
|
|
101
|
-
});
|
|
102
|
-
await server.clearHistory();
|
|
103
|
-
// Since we can't easily peek into private state, we'll process a new thought
|
|
104
|
-
// and check if thoughtHistoryLength is 1 (meaning it started over or is just this one)
|
|
105
|
-
const result = await server.processThought({
|
|
106
|
-
thought: "Fresh start",
|
|
107
|
-
thoughtNumber: 1,
|
|
108
|
-
totalThoughts: 1,
|
|
109
|
-
nextThoughtNeeded: false
|
|
110
|
-
});
|
|
111
|
-
const content = JSON.parse(result.content[0].text);
|
|
112
|
-
expect(content.thoughtHistoryLength).toBe(1);
|
|
113
|
-
});
|
|
114
|
-
it('should summarize history correctly', async () => {
|
|
115
|
-
// Add 3 thoughts
|
|
116
|
-
await server.processThought({ thought: "T1", thoughtNumber: 1, totalThoughts: 3, nextThoughtNeeded: true });
|
|
117
|
-
await server.processThought({ thought: "T2", thoughtNumber: 2, totalThoughts: 3, nextThoughtNeeded: true });
|
|
118
|
-
await server.processThought({ thought: "T3", thoughtNumber: 3, totalThoughts: 3, nextThoughtNeeded: false });
|
|
119
|
-
const result = await server.archiveHistory(1, 2, "Summary of T1 and T2");
|
|
120
|
-
expect(result.newHistoryLength).toBe(2); // Summary + T3
|
|
121
|
-
expect(result.summaryInsertedAt).toBe(1);
|
|
122
|
-
});
|
|
123
|
-
it('should throw error when summarizing invalid range', async () => {
|
|
124
|
-
await server.processThought({ thought: "T1", thoughtNumber: 1, totalThoughts: 1, nextThoughtNeeded: false });
|
|
125
|
-
await expect(server.archiveHistory(1, 5, "Invalid")).rejects.toThrow();
|
|
126
|
-
});
|
|
127
|
-
});
|
package/dist/stress.test.js
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
-
import { SequentialThinkingServer } from './lib.js';
|
|
3
|
-
import { ProjectKnowledgeGraph } from './graph.js';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
// Mock fs for graph test
|
|
7
|
-
vi.mock('fs/promises', async (importOriginal) => {
|
|
8
|
-
const actual = await importOriginal();
|
|
9
|
-
return {
|
|
10
|
-
...actual,
|
|
11
|
-
readdir: vi.fn(),
|
|
12
|
-
readFile: vi.fn(),
|
|
13
|
-
writeFile: vi.fn(),
|
|
14
|
-
rename: vi.fn(),
|
|
15
|
-
unlink: vi.fn()
|
|
16
|
-
};
|
|
17
|
-
});
|
|
18
|
-
// Mock existsSync and readFileSync from 'fs' (non-promise) for SequentialThinkingServer
|
|
19
|
-
vi.mock('fs', async () => {
|
|
20
|
-
return {
|
|
21
|
-
existsSync: () => false,
|
|
22
|
-
readFileSync: () => '[]',
|
|
23
|
-
unlinkSync: () => { }
|
|
24
|
-
};
|
|
25
|
-
});
|
|
26
|
-
describe('Stress Testing', () => {
|
|
27
|
-
it('should handle 1000 sequential thoughts', async () => {
|
|
28
|
-
// We use a real instance but mocked fs
|
|
29
|
-
const server = new SequentialThinkingServer('stress_thoughts.json');
|
|
30
|
-
const startTime = Date.now();
|
|
31
|
-
for (let i = 1; i <= 1000; i++) {
|
|
32
|
-
await server.processThought({
|
|
33
|
-
thought: `Thought ${i}`,
|
|
34
|
-
thoughtNumber: i,
|
|
35
|
-
totalThoughts: 1000,
|
|
36
|
-
nextThoughtNeeded: i < 1000
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
const duration = Date.now() - startTime;
|
|
40
|
-
console.log(`Processed 1000 thoughts in ${duration}ms`);
|
|
41
|
-
expect(duration).toBeLessThan(10000); // Should be fast enough (< 10s)
|
|
42
|
-
});
|
|
43
|
-
it('should handle large graph construction', async () => {
|
|
44
|
-
const graph = new ProjectKnowledgeGraph();
|
|
45
|
-
// Mock 1000 files
|
|
46
|
-
const files = Array.from({ length: 1000 }, (_, i) => `file${i}.ts`);
|
|
47
|
-
// Mock readdir to return these files (recursively? no, just flat for stress test)
|
|
48
|
-
// logic in getAllFiles is recursive. We need to mock it to return all at once or handle recursion.
|
|
49
|
-
// Actually, ProjectKnowledgeGraph.getAllFiles calls readdir.
|
|
50
|
-
// Let's mock readdir to return files for root, and then nothing for subdirs.
|
|
51
|
-
fs.readdir.mockImplementation(async (dir) => {
|
|
52
|
-
if (dir === path.resolve('.')) {
|
|
53
|
-
return files.map(f => ({
|
|
54
|
-
name: f,
|
|
55
|
-
isDirectory: () => false
|
|
56
|
-
}));
|
|
57
|
-
}
|
|
58
|
-
return [];
|
|
59
|
-
});
|
|
60
|
-
fs.readFile.mockResolvedValue("import { x } from './file0';");
|
|
61
|
-
const startTime = Date.now();
|
|
62
|
-
const result = await graph.build('.');
|
|
63
|
-
const duration = Date.now() - startTime;
|
|
64
|
-
console.log(`Built graph of ${result.totalFiles} files in ${duration}ms`);
|
|
65
|
-
expect(result.totalFiles).toBe(1000);
|
|
66
|
-
expect(duration).toBeLessThan(5000);
|
|
67
|
-
});
|
|
68
|
-
});
|
package/dist/utils.test.js
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest';
|
|
2
|
-
import { validatePath } from './utils.js';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
describe('Utils: validatePath', () => {
|
|
5
|
-
// Mock process.cwd to be a known fixed path
|
|
6
|
-
const mockCwd = '/app/project';
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.spyOn(process, 'cwd').mockReturnValue(mockCwd);
|
|
9
|
-
});
|
|
10
|
-
afterEach(() => {
|
|
11
|
-
vi.restoreAllMocks();
|
|
12
|
-
});
|
|
13
|
-
it('should allow paths within the project root', () => {
|
|
14
|
-
const p = validatePath('src/index.ts');
|
|
15
|
-
expect(p).toBe(path.resolve(mockCwd, 'src/index.ts'));
|
|
16
|
-
});
|
|
17
|
-
it('should allow explicit ./ paths', () => {
|
|
18
|
-
const p = validatePath('./package.json');
|
|
19
|
-
expect(p).toBe(path.resolve(mockCwd, 'package.json'));
|
|
20
|
-
});
|
|
21
|
-
it('should block traversal to parent directory', () => {
|
|
22
|
-
expect(() => {
|
|
23
|
-
validatePath('../outside.txt');
|
|
24
|
-
}).toThrow(/Access denied/);
|
|
25
|
-
});
|
|
26
|
-
it('should block multiple level traversal', () => {
|
|
27
|
-
expect(() => {
|
|
28
|
-
validatePath('src/../../etc/passwd');
|
|
29
|
-
}).toThrow(/Access denied/);
|
|
30
|
-
});
|
|
31
|
-
it('should block absolute paths outside root', () => {
|
|
32
|
-
// Only run this check if we can reliably simulate absolute paths
|
|
33
|
-
// For now, let's assume standard unix paths for the test logic
|
|
34
|
-
if (path.sep === '/') {
|
|
35
|
-
expect(() => {
|
|
36
|
-
validatePath('/etc/passwd');
|
|
37
|
-
}).toThrow(/Access denied/);
|
|
38
|
-
}
|
|
39
|
-
});
|
|
40
|
-
});
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
describe('Graph Caching Verification', () => {
|
|
6
|
-
it('should use cache on second run', async () => {
|
|
7
|
-
const graph = new ProjectKnowledgeGraph();
|
|
8
|
-
const root = process.cwd();
|
|
9
|
-
// Cleanup existing cache
|
|
10
|
-
const cachePath = path.join(root, '.gemini_graph_cache.json');
|
|
11
|
-
try {
|
|
12
|
-
await fs.unlink(cachePath);
|
|
13
|
-
}
|
|
14
|
-
catch (e) { }
|
|
15
|
-
console.log('--- Run 1 (Fresh) ---');
|
|
16
|
-
const res1 = await graph.build(root);
|
|
17
|
-
console.log('Result 1:', res1);
|
|
18
|
-
expect(res1.parsedFiles).toBeGreaterThan(0);
|
|
19
|
-
expect(res1.cachedFiles).toBe(0);
|
|
20
|
-
console.log('--- Run 2 (Cached) ---');
|
|
21
|
-
const res2 = await graph.build(root);
|
|
22
|
-
console.log('Result 2:', res2);
|
|
23
|
-
expect(res2.parsedFiles).toBe(0);
|
|
24
|
-
expect(res2.cachedFiles).toBeGreaterThan(0);
|
|
25
|
-
expect(res2.nodeCount).toBe(res1.nodeCount);
|
|
26
|
-
});
|
|
27
|
-
});
|
package/dist/verify_edit.test.js
DELETED
|
@@ -1,66 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import * as fs from 'fs/promises';
|
|
3
|
-
import * as path from 'path';
|
|
4
|
-
// Replicating the logic from src/index.ts for testing
|
|
5
|
-
async function editFileLogic(path, oldText, newText, allowMultiple = false) {
|
|
6
|
-
try {
|
|
7
|
-
const content = await fs.readFile(path, 'utf-8');
|
|
8
|
-
const escapeRegExp = (string) => string.replace(/[.*+?^${}()|[\\]/g, '\\$&');
|
|
9
|
-
const regex = new RegExp(escapeRegExp(oldText), 'g');
|
|
10
|
-
const matchCount = (content.match(regex) || []).length;
|
|
11
|
-
if (matchCount === 0) {
|
|
12
|
-
return { error: "Error: 'oldText' not found" };
|
|
13
|
-
}
|
|
14
|
-
if (matchCount > 1 && !allowMultiple) {
|
|
15
|
-
return { error: `Error: Found ${matchCount} occurrences` };
|
|
16
|
-
}
|
|
17
|
-
const newContent = content.replace(allowMultiple ? regex : oldText, newText);
|
|
18
|
-
await fs.writeFile(path, newContent, 'utf-8');
|
|
19
|
-
return { success: true };
|
|
20
|
-
}
|
|
21
|
-
catch (error) {
|
|
22
|
-
return { error: String(error) };
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
describe('edit_file logic', () => {
|
|
26
|
-
const testFile = path.join(__dirname, 'test_edit.txt');
|
|
27
|
-
beforeEach(async () => {
|
|
28
|
-
await fs.writeFile(testFile, 'Line 1\nTarget\nLine 3\nTarget again');
|
|
29
|
-
});
|
|
30
|
-
afterEach(async () => {
|
|
31
|
-
try {
|
|
32
|
-
await fs.unlink(testFile);
|
|
33
|
-
}
|
|
34
|
-
catch { }
|
|
35
|
-
});
|
|
36
|
-
it('should replace single occurrence', async () => {
|
|
37
|
-
await fs.writeFile(testFile, 'Line 1\nUnique\nLine 3');
|
|
38
|
-
const result = await editFileLogic(testFile, 'Unique', 'Replaced');
|
|
39
|
-
expect(result.error).toBeUndefined();
|
|
40
|
-
const content = await fs.readFile(testFile, 'utf-8');
|
|
41
|
-
expect(content).toContain('Line 1\nReplaced\nLine 3');
|
|
42
|
-
});
|
|
43
|
-
it('should fail if text not found', async () => {
|
|
44
|
-
const result = await editFileLogic(testFile, 'Missing', 'New');
|
|
45
|
-
expect(result.error).toContain("not found");
|
|
46
|
-
});
|
|
47
|
-
it('should fail if multiple found and allowMultiple=false', async () => {
|
|
48
|
-
const result = await editFileLogic(testFile, 'Target', 'New');
|
|
49
|
-
expect(result.error).toContain("Found 2 occurrences");
|
|
50
|
-
});
|
|
51
|
-
it('should replace multiple if allowMultiple=true', async () => {
|
|
52
|
-
const result = await editFileLogic(testFile, 'Target', 'New', true);
|
|
53
|
-
expect(result.error).toBeUndefined();
|
|
54
|
-
const content = await fs.readFile(testFile, 'utf-8');
|
|
55
|
-
expect(content).toBe('Line 1\nNew\nLine 3\nNew again');
|
|
56
|
-
});
|
|
57
|
-
it('should handle special regex characters in text', async () => {
|
|
58
|
-
await fs.writeFile(testFile, 'func(a, b) { return a+b; }');
|
|
59
|
-
const oldText = 'func(a, b) { return a+b; }';
|
|
60
|
-
const newText = 'replacement';
|
|
61
|
-
const result = await editFileLogic(testFile, oldText, newText);
|
|
62
|
-
expect(result.error).toBeUndefined();
|
|
63
|
-
const content = await fs.readFile(testFile, 'utf-8');
|
|
64
|
-
expect(content).toBe('replacement');
|
|
65
|
-
});
|
|
66
|
-
});
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { NotesManager } from './notes.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
describe('Notes Manager', () => {
|
|
6
|
-
const testFile = path.join(__dirname, 'test_notes.json');
|
|
7
|
-
let manager;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
manager = new NotesManager(testFile);
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
try {
|
|
13
|
-
await fs.unlink(testFile);
|
|
14
|
-
}
|
|
15
|
-
catch { }
|
|
16
|
-
});
|
|
17
|
-
it('should add and list notes', async () => {
|
|
18
|
-
await manager.addNote("My Note", "Content", ["tag1"]);
|
|
19
|
-
const notes = await manager.listNotes();
|
|
20
|
-
expect(notes.length).toBe(1);
|
|
21
|
-
expect(notes[0].title).toBe("My Note");
|
|
22
|
-
});
|
|
23
|
-
it('should search notes', async () => {
|
|
24
|
-
await manager.addNote("React Tips", "Use hooks", ["react"]);
|
|
25
|
-
await manager.addNote("Vue Tips", "Use composition", ["vue"]);
|
|
26
|
-
const results = await manager.searchNotes("hooks");
|
|
27
|
-
expect(results.length).toBe(1);
|
|
28
|
-
expect(results[0].title).toBe("React Tips");
|
|
29
|
-
});
|
|
30
|
-
it('should delete note', async () => {
|
|
31
|
-
const note = await manager.addNote("To Delete", "...");
|
|
32
|
-
await manager.deleteNote(note.id);
|
|
33
|
-
const notes = await manager.listNotes();
|
|
34
|
-
expect(notes.length).toBe(0);
|
|
35
|
-
});
|
|
36
|
-
});
|
package/dist/verify_viz.test.js
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
describe('Graph Visualization', () => {
|
|
6
|
-
const testDir = path.join(__dirname, 'test_viz_env');
|
|
7
|
-
const graph = new ProjectKnowledgeGraph();
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
await fs.mkdir(testDir, { recursive: true });
|
|
10
|
-
await fs.writeFile(path.join(testDir, 'a.ts'), 'import { b } from "./b";');
|
|
11
|
-
await fs.writeFile(path.join(testDir, 'b.ts'), 'export const b = 1;');
|
|
12
|
-
});
|
|
13
|
-
afterEach(async () => {
|
|
14
|
-
await fs.rm(testDir, { recursive: true, force: true });
|
|
15
|
-
});
|
|
16
|
-
it('should generate mermaid diagram', async () => {
|
|
17
|
-
await graph.build(testDir);
|
|
18
|
-
const mermaid = graph.toMermaid();
|
|
19
|
-
console.log("Generated Mermaid:", mermaid);
|
|
20
|
-
expect(mermaid).toContain('graph TD');
|
|
21
|
-
expect(mermaid).toMatch(/N\d+\["a\.ts"\]/);
|
|
22
|
-
expect(mermaid).toMatch(/N\d+\["b\.ts"\]/);
|
|
23
|
-
expect(mermaid).toContain('-->');
|
|
24
|
-
});
|
|
25
|
-
});
|
|
@@ -1,103 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { registerWebTools } from './tools/web.js';
|
|
3
|
-
import * as utils from './utils.js';
|
|
4
|
-
// Mock utils
|
|
5
|
-
vi.mock('./utils.js', async (importOriginal) => {
|
|
6
|
-
const actual = await importOriginal();
|
|
7
|
-
return {
|
|
8
|
-
...actual,
|
|
9
|
-
fetchWithRetry: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
});
|
|
12
|
-
describe('web_search fallback', () => {
|
|
13
|
-
let mockToolCallback;
|
|
14
|
-
const mockServer = {
|
|
15
|
-
tool: vi.fn((name, desc, schema, callback) => {
|
|
16
|
-
if (name === 'web_search') {
|
|
17
|
-
mockToolCallback = callback;
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
};
|
|
21
|
-
const originalEnv = process.env;
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
process.env = { ...originalEnv };
|
|
24
|
-
vi.clearAllMocks();
|
|
25
|
-
});
|
|
26
|
-
afterEach(() => {
|
|
27
|
-
process.env = originalEnv;
|
|
28
|
-
});
|
|
29
|
-
it('should use Brave if configured and no provider specified', async () => {
|
|
30
|
-
process.env.BRAVE_API_KEY = 'test-brave-key';
|
|
31
|
-
delete process.env.EXA_API_KEY;
|
|
32
|
-
delete process.env.GOOGLE_SEARCH_API_KEY;
|
|
33
|
-
registerWebTools(mockServer);
|
|
34
|
-
const mockResponse = {
|
|
35
|
-
ok: true,
|
|
36
|
-
json: async () => ({ web: { results: ['brave result'] } })
|
|
37
|
-
};
|
|
38
|
-
utils.fetchWithRetry.mockResolvedValue(mockResponse);
|
|
39
|
-
const result = await mockToolCallback({ query: 'test' });
|
|
40
|
-
expect(utils.fetchWithRetry).toHaveBeenCalledWith(expect.stringContaining('api.search.brave.com'), expect.anything());
|
|
41
|
-
expect(result.isError).toBeUndefined();
|
|
42
|
-
expect(JSON.parse(result.content[0].text)).toEqual(['brave result']);
|
|
43
|
-
});
|
|
44
|
-
it('should fallback to Exa if Brave fails', async () => {
|
|
45
|
-
process.env.BRAVE_API_KEY = 'test-brave-key';
|
|
46
|
-
process.env.EXA_API_KEY = 'test-exa-key';
|
|
47
|
-
registerWebTools(mockServer);
|
|
48
|
-
// First call (Brave) fails
|
|
49
|
-
utils.fetchWithRetry
|
|
50
|
-
.mockResolvedValueOnce({ ok: false, statusText: 'Brave Error', status: 500 })
|
|
51
|
-
// Second call (Exa) succeeds
|
|
52
|
-
.mockResolvedValueOnce({
|
|
53
|
-
ok: true,
|
|
54
|
-
json: async () => ({ results: ['exa result'] })
|
|
55
|
-
});
|
|
56
|
-
const result = await mockToolCallback({ query: 'test' });
|
|
57
|
-
expect(utils.fetchWithRetry).toHaveBeenCalledTimes(2);
|
|
58
|
-
// 1. Brave
|
|
59
|
-
expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(1, expect.stringContaining('api.search.brave.com'), expect.anything());
|
|
60
|
-
// 2. Exa
|
|
61
|
-
expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(2, expect.stringContaining('api.exa.ai'), expect.anything());
|
|
62
|
-
expect(result.isError).toBeUndefined();
|
|
63
|
-
expect(JSON.parse(result.content[0].text)).toEqual(['exa result']);
|
|
64
|
-
});
|
|
65
|
-
it('should respect requested provider and verify its availability', async () => {
|
|
66
|
-
process.env.BRAVE_API_KEY = 'test-brave-key';
|
|
67
|
-
// Exa not configured
|
|
68
|
-
delete process.env.EXA_API_KEY;
|
|
69
|
-
registerWebTools(mockServer);
|
|
70
|
-
const result = await mockToolCallback({ query: 'test', provider: 'exa' });
|
|
71
|
-
expect(result.isError).toBe(true);
|
|
72
|
-
expect(result.content[0].text).toContain("Requested provider 'exa' is not configured");
|
|
73
|
-
});
|
|
74
|
-
it('should try requested provider first, then fallback', async () => {
|
|
75
|
-
process.env.BRAVE_API_KEY = 'test-brave-key';
|
|
76
|
-
process.env.EXA_API_KEY = 'test-exa-key';
|
|
77
|
-
registerWebTools(mockServer);
|
|
78
|
-
// Request Exa
|
|
79
|
-
// Mock Exa fail, Brave success
|
|
80
|
-
utils.fetchWithRetry
|
|
81
|
-
.mockResolvedValueOnce({ ok: false, statusText: 'Exa Error', status: 500 })
|
|
82
|
-
.mockResolvedValueOnce({
|
|
83
|
-
ok: true,
|
|
84
|
-
json: async () => ({ web: { results: ['brave result'] } })
|
|
85
|
-
});
|
|
86
|
-
const result = await mockToolCallback({ query: 'test', provider: 'exa' });
|
|
87
|
-
expect(utils.fetchWithRetry).toHaveBeenCalledTimes(2);
|
|
88
|
-
// 1. Exa (requested)
|
|
89
|
-
expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(1, expect.stringContaining('api.exa.ai'), expect.anything());
|
|
90
|
-
// 2. Brave (fallback)
|
|
91
|
-
expect(utils.fetchWithRetry).toHaveBeenNthCalledWith(2, expect.stringContaining('api.search.brave.com'), expect.anything());
|
|
92
|
-
expect(result.isError).toBeUndefined();
|
|
93
|
-
});
|
|
94
|
-
it('should return error if all fail', async () => {
|
|
95
|
-
process.env.BRAVE_API_KEY = 'test-brave-key';
|
|
96
|
-
registerWebTools(mockServer);
|
|
97
|
-
utils.fetchWithRetry.mockResolvedValue({ ok: false, statusText: 'Some Error', status: 500 });
|
|
98
|
-
const result = await mockToolCallback({ query: 'test' });
|
|
99
|
-
expect(result.isError).toBe(true);
|
|
100
|
-
expect(result.content[0].text).toContain("All search providers failed");
|
|
101
|
-
expect(result.content[0].text).toContain("Brave API error");
|
|
102
|
-
});
|
|
103
|
-
});
|
package/dist/web_read.test.js
DELETED
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { registerWebTools } from './tools/web.js';
|
|
3
|
-
import * as utils from './utils.js';
|
|
4
|
-
vi.mock('./utils.js', async (importOriginal) => {
|
|
5
|
-
const actual = await importOriginal();
|
|
6
|
-
return {
|
|
7
|
-
...actual,
|
|
8
|
-
fetchWithRetry: vi.fn(),
|
|
9
|
-
validatePublicUrl: vi.fn(),
|
|
10
|
-
};
|
|
11
|
-
});
|
|
12
|
-
describe('read_webpage tool', () => {
|
|
13
|
-
let mockToolCallback;
|
|
14
|
-
const mockServer = {
|
|
15
|
-
tool: vi.fn((name, desc, schema, callback) => {
|
|
16
|
-
if (name === 'read_webpage') {
|
|
17
|
-
mockToolCallback = callback;
|
|
18
|
-
}
|
|
19
|
-
})
|
|
20
|
-
};
|
|
21
|
-
beforeEach(() => {
|
|
22
|
-
vi.clearAllMocks();
|
|
23
|
-
registerWebTools(mockServer);
|
|
24
|
-
});
|
|
25
|
-
it('should convert HTML to Markdown', async () => {
|
|
26
|
-
const mockHtml = `
|
|
27
|
-
<html>
|
|
28
|
-
<head><title>Test Article</title></head>
|
|
29
|
-
<body>
|
|
30
|
-
<h1>Main Header</h1>
|
|
31
|
-
<p>Paragraph <b>bold</b>.</p>
|
|
32
|
-
</body>
|
|
33
|
-
</html>
|
|
34
|
-
`;
|
|
35
|
-
utils.fetchWithRetry.mockResolvedValue({
|
|
36
|
-
ok: true,
|
|
37
|
-
text: async () => mockHtml
|
|
38
|
-
});
|
|
39
|
-
utils.validatePublicUrl.mockResolvedValue(undefined);
|
|
40
|
-
const result = await mockToolCallback({ url: 'https://example.com' });
|
|
41
|
-
expect(result.isError).toBeUndefined();
|
|
42
|
-
const content = result.content[0].text;
|
|
43
|
-
expect(content).toContain("Title: Test Article");
|
|
44
|
-
expect(content).toContain("Main Header");
|
|
45
|
-
expect(content).toContain("**bold**"); // Markdown bold
|
|
46
|
-
});
|
|
47
|
-
it('should handle private URL validation error', async () => {
|
|
48
|
-
utils.validatePublicUrl.mockRejectedValue(new Error("Access denied: Private IP"));
|
|
49
|
-
const result = await mockToolCallback({ url: 'http://localhost' });
|
|
50
|
-
expect(result.isError).toBe(true);
|
|
51
|
-
expect(result.content[0].text).toContain("Access denied");
|
|
52
|
-
});
|
|
53
|
-
it('should handle fetch errors', async () => {
|
|
54
|
-
utils.validatePublicUrl.mockResolvedValue(undefined);
|
|
55
|
-
utils.fetchWithRetry.mockRejectedValue(new Error("Network Error"));
|
|
56
|
-
const result = await mockToolCallback({ url: 'https://example.com' });
|
|
57
|
-
expect(result.isError).toBe(true);
|
|
58
|
-
expect(result.content[0].text).toContain("Network Error");
|
|
59
|
-
});
|
|
60
|
-
});
|