@gotza02/sequential-thinking 10000.0.0 → 10000.0.2
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 +6 -0
- package/dist/chaos.test.d.ts +1 -0
- package/dist/chaos.test.js +73 -0
- package/dist/codestore.test.d.ts +1 -0
- package/dist/codestore.test.js +65 -0
- package/dist/coding.test.d.ts +1 -0
- package/dist/coding.test.js +140 -0
- package/dist/e2e.test.d.ts +1 -0
- package/dist/e2e.test.js +122 -0
- package/dist/filesystem.test.d.ts +1 -0
- package/dist/filesystem.test.js +190 -0
- package/dist/graph.test.d.ts +1 -0
- package/dist/graph.test.js +150 -0
- package/dist/graph_extra.test.d.ts +1 -0
- package/dist/graph_extra.test.js +93 -0
- package/dist/graph_repro.test.d.ts +1 -0
- package/dist/graph_repro.test.js +50 -0
- package/dist/human.test.d.ts +1 -0
- package/dist/human.test.js +221 -0
- package/dist/integration.test.d.ts +1 -0
- package/dist/integration.test.js +58 -0
- package/dist/knowledge.test.d.ts +1 -0
- package/dist/knowledge.test.js +105 -0
- package/dist/lib.js +1 -0
- package/dist/notes.test.d.ts +1 -0
- package/dist/notes.test.js +84 -0
- package/dist/registration.test.d.ts +1 -0
- package/dist/registration.test.js +39 -0
- package/dist/server.test.d.ts +1 -0
- package/dist/server.test.js +127 -0
- package/dist/stress.test.d.ts +1 -0
- package/dist/stress.test.js +72 -0
- package/dist/tools/codestore_tools.test.d.ts +1 -0
- package/dist/tools/codestore_tools.test.js +115 -0
- package/dist/tools/filesystem.js +1 -0
- package/dist/tools/sports/core/constants.d.ts +2 -1
- package/dist/tools/sports/core/constants.js +18 -6
- package/dist/tools/sports/providers/scraper.d.ts +6 -1
- package/dist/tools/sports/providers/scraper.js +63 -8
- package/dist/tools/sports/tools/match.js +44 -21
- package/dist/tools/sports/tracker.test.d.ts +1 -0
- package/dist/tools/sports/tracker.test.js +100 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +40 -0
- package/dist/verify_cache.test.d.ts +1 -0
- package/dist/verify_cache.test.js +185 -0
- package/dist/web_fallback.test.d.ts +1 -0
- package/dist/web_fallback.test.js +103 -0
- package/dist/web_read.test.d.ts +1 -0
- package/dist/web_read.test.js +60 -0
- package/package.json +7 -6
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('ProjectKnowledgeGraph', () => {
|
|
6
|
+
let graph;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
graph = new ProjectKnowledgeGraph();
|
|
9
|
+
vi.resetAllMocks();
|
|
10
|
+
// Default mocks for stat and writeFile
|
|
11
|
+
fs.stat.mockResolvedValue({
|
|
12
|
+
isDirectory: () => true,
|
|
13
|
+
mtimeMs: 1000
|
|
14
|
+
});
|
|
15
|
+
fs.writeFile.mockResolvedValue(undefined);
|
|
16
|
+
});
|
|
17
|
+
it('should ignore imports in comments', async () => {
|
|
18
|
+
const mockFiles = ['/app/index.ts', '/app/utils.ts', '/app/oldUtils.ts'];
|
|
19
|
+
const mockContentIndex = `
|
|
20
|
+
import { something } from './utils';
|
|
21
|
+
// import { oldThing } from './oldUtils';
|
|
22
|
+
/* import { other } from './other' */
|
|
23
|
+
`;
|
|
24
|
+
const mockContentUtils = 'export const something = 1;';
|
|
25
|
+
fs.readdir.mockResolvedValue([
|
|
26
|
+
{ name: 'index.ts', isDirectory: () => false },
|
|
27
|
+
{ name: 'utils.ts', isDirectory: () => false },
|
|
28
|
+
{ name: 'oldUtils.ts', isDirectory: () => false }
|
|
29
|
+
]);
|
|
30
|
+
fs.readFile.mockImplementation(async (path) => {
|
|
31
|
+
if (path.includes('graph_cache.json'))
|
|
32
|
+
return '{}'; // Mock empty cache
|
|
33
|
+
if (path.includes('index.ts'))
|
|
34
|
+
return mockContentIndex;
|
|
35
|
+
return '';
|
|
36
|
+
});
|
|
37
|
+
// Mock resolvePath behavior indirectly by mocking existing files check in graph.build logic
|
|
38
|
+
// But since graph.ts uses fs.readdir recursively, we need to mock that structure.
|
|
39
|
+
// For simplicity in this unit test, we'll mock 'getAllFiles' if it were public,
|
|
40
|
+
// but since it's private, we have to mock fs structure carefully or rely on the implementation.
|
|
41
|
+
// Let's rely on the fact that build calls getAllFiles which calls readdir.
|
|
42
|
+
// We need to ensure 'utils.ts' and 'oldUtils.ts' resolution is tested.
|
|
43
|
+
// Actually, since we mock readFile, the file existence check in resolvePath uses "this.nodes.has".
|
|
44
|
+
// "this.nodes" is populated by getAllFiles.
|
|
45
|
+
// So we need getAllFiles to return both index.ts and utils.ts.
|
|
46
|
+
// And NOT oldUtils.ts so we can see if it tries to resolve it?
|
|
47
|
+
// Actually, if it tries to resolve 'oldUtils', it might fail if not in nodes.
|
|
48
|
+
// But the bug is that it SHOULD NOT even try to resolve 'oldUtils' because it's commented out.
|
|
49
|
+
await graph.build('/app');
|
|
50
|
+
const relationships = graph.getRelationships('/app/index.ts');
|
|
51
|
+
expect(relationships?.imports).toContain('utils.ts');
|
|
52
|
+
expect(relationships?.imports).not.toContain('oldUtils.ts');
|
|
53
|
+
});
|
|
54
|
+
it('should resolve .js imports to .ts files', async () => {
|
|
55
|
+
const mockContentIndex = `import { something } from './lib.js';`;
|
|
56
|
+
fs.readdir.mockResolvedValue([
|
|
57
|
+
{ name: 'index.ts', isDirectory: () => false },
|
|
58
|
+
{ name: 'lib.ts', isDirectory: () => false }
|
|
59
|
+
]);
|
|
60
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
61
|
+
if (filePath.includes('graph_cache.json'))
|
|
62
|
+
return '{}';
|
|
63
|
+
if (filePath.endsWith('index.ts'))
|
|
64
|
+
return mockContentIndex;
|
|
65
|
+
return '';
|
|
66
|
+
});
|
|
67
|
+
await graph.build('/app');
|
|
68
|
+
const relationships = graph.getRelationships('/app/index.ts');
|
|
69
|
+
// The output of getRelationships.imports is relative paths.
|
|
70
|
+
// If imports ./lib.js, and we have lib.ts, it should resolve to /app/lib.ts
|
|
71
|
+
// And path.relative('/app', '/app/lib.ts') is 'lib.ts'
|
|
72
|
+
expect(relationships?.imports).toContain('lib.ts');
|
|
73
|
+
});
|
|
74
|
+
it('should resolve .js imports to .jsx files', async () => {
|
|
75
|
+
const mockContentIndex = `import { Button } from './Button.js';`;
|
|
76
|
+
fs.readdir.mockResolvedValue([
|
|
77
|
+
{ name: 'index.js', isDirectory: () => false },
|
|
78
|
+
{ name: 'Button.jsx', isDirectory: () => false }
|
|
79
|
+
]);
|
|
80
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
81
|
+
if (filePath.includes('graph_cache.json'))
|
|
82
|
+
return '{}';
|
|
83
|
+
if (filePath.endsWith('index.js'))
|
|
84
|
+
return mockContentIndex;
|
|
85
|
+
return '';
|
|
86
|
+
});
|
|
87
|
+
await graph.build('/app');
|
|
88
|
+
const relationships = graph.getRelationships('/app/index.js');
|
|
89
|
+
expect(relationships?.imports).toContain('Button.jsx');
|
|
90
|
+
});
|
|
91
|
+
it('should handle circular dependencies', async () => {
|
|
92
|
+
const contentA = `import { b } from './b'; export const a = 1;`;
|
|
93
|
+
const contentB = `import { a } from './a'; export const b = 2;`;
|
|
94
|
+
fs.readdir.mockResolvedValue([
|
|
95
|
+
{ name: 'a.ts', isDirectory: () => false },
|
|
96
|
+
{ name: 'b.ts', isDirectory: () => false }
|
|
97
|
+
]);
|
|
98
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
99
|
+
if (filePath.includes('graph_cache.json'))
|
|
100
|
+
return '{}';
|
|
101
|
+
if (filePath.endsWith('a.ts'))
|
|
102
|
+
return contentA;
|
|
103
|
+
if (filePath.endsWith('b.ts'))
|
|
104
|
+
return contentB;
|
|
105
|
+
return '';
|
|
106
|
+
});
|
|
107
|
+
await graph.build('/app');
|
|
108
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
109
|
+
const relB = graph.getRelationships('/app/b.ts');
|
|
110
|
+
expect(relA?.imports).toContain('b.ts');
|
|
111
|
+
expect(relA?.importedBy).toContain('b.ts');
|
|
112
|
+
expect(relB?.imports).toContain('a.ts');
|
|
113
|
+
expect(relB?.importedBy).toContain('a.ts');
|
|
114
|
+
});
|
|
115
|
+
it('should gracefully handle missing imports', async () => {
|
|
116
|
+
const contentA = `import { ghost } from './ghost';`;
|
|
117
|
+
fs.readdir.mockResolvedValue([
|
|
118
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
119
|
+
]);
|
|
120
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
121
|
+
if (filePath.includes('graph_cache.json'))
|
|
122
|
+
return '{}';
|
|
123
|
+
if (filePath.endsWith('a.ts'))
|
|
124
|
+
return contentA;
|
|
125
|
+
return '';
|
|
126
|
+
});
|
|
127
|
+
await graph.build('/app');
|
|
128
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
129
|
+
// ghost.ts doesn't exist, so imports should be empty (filtered out)
|
|
130
|
+
expect(relA?.imports).toHaveLength(0);
|
|
131
|
+
});
|
|
132
|
+
it('should ignore directory traversal attempts outside root', async () => {
|
|
133
|
+
// If we pretend root is /app, and we try to import ../outside
|
|
134
|
+
const contentA = `import { secret } from '../secret';`;
|
|
135
|
+
fs.readdir.mockResolvedValue([
|
|
136
|
+
{ name: 'a.ts', isDirectory: () => false }
|
|
137
|
+
]);
|
|
138
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
139
|
+
if (filePath.includes('graph_cache.json'))
|
|
140
|
+
return '{}';
|
|
141
|
+
if (filePath.endsWith('a.ts'))
|
|
142
|
+
return contentA;
|
|
143
|
+
return '';
|
|
144
|
+
});
|
|
145
|
+
await graph.build('/app');
|
|
146
|
+
const relA = graph.getRelationships('/app/a.ts');
|
|
147
|
+
// Should be empty because '../secret' is not in the scanned file list (nodes)
|
|
148
|
+
expect(relA?.imports).toHaveLength(0);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,93 @@
|
|
|
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
|
+
import * as os from 'os';
|
|
6
|
+
describe('ProjectKnowledgeGraph - Extra Coverage', () => {
|
|
7
|
+
let tempDir;
|
|
8
|
+
let graph;
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'graph-extra-test-'));
|
|
11
|
+
graph = new ProjectKnowledgeGraph();
|
|
12
|
+
});
|
|
13
|
+
afterEach(async () => {
|
|
14
|
+
await fs.rm(tempDir, { recursive: true, force: true });
|
|
15
|
+
});
|
|
16
|
+
it('should parse Python files correctly', async () => {
|
|
17
|
+
const pyFile = path.join(tempDir, 'script.py');
|
|
18
|
+
const content = `
|
|
19
|
+
import os
|
|
20
|
+
import sys, time
|
|
21
|
+
from .local_mod import func
|
|
22
|
+
from package.module import Class
|
|
23
|
+
|
|
24
|
+
class MyClass:
|
|
25
|
+
pass
|
|
26
|
+
|
|
27
|
+
def my_func():
|
|
28
|
+
pass
|
|
29
|
+
`;
|
|
30
|
+
await fs.writeFile(pyFile, content);
|
|
31
|
+
await fs.writeFile(path.join(tempDir, 'local_mod.py'), 'def func(): pass');
|
|
32
|
+
await graph.build(tempDir);
|
|
33
|
+
const rel = graph.getRelationships('script.py');
|
|
34
|
+
expect(rel?.symbols).toContain('class:MyClass');
|
|
35
|
+
expect(rel?.symbols).toContain('def:my_func');
|
|
36
|
+
expect(rel?.imports).toContain('os');
|
|
37
|
+
expect(rel?.imports).toContain('sys');
|
|
38
|
+
expect(rel?.imports).toContain('local_mod.py');
|
|
39
|
+
});
|
|
40
|
+
it('should parse Go files correctly', async () => {
|
|
41
|
+
const goFile = path.join(tempDir, 'main.go');
|
|
42
|
+
const content = `
|
|
43
|
+
package main
|
|
44
|
+
import "fmt"
|
|
45
|
+
import (
|
|
46
|
+
"os"
|
|
47
|
+
"net/http"
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
type MyStruct struct {}
|
|
51
|
+
type MyInterface interface {}
|
|
52
|
+
|
|
53
|
+
func main() {}
|
|
54
|
+
`;
|
|
55
|
+
await fs.writeFile(goFile, content);
|
|
56
|
+
await graph.build(tempDir);
|
|
57
|
+
const rel = graph.getRelationships('main.go');
|
|
58
|
+
expect(rel?.symbols).toContain('type:MyStruct');
|
|
59
|
+
expect(rel?.symbols).toContain('type:MyInterface');
|
|
60
|
+
expect(rel?.symbols).toContain('func:main');
|
|
61
|
+
expect(rel?.imports).toContain('fmt');
|
|
62
|
+
expect(rel?.imports).toContain('os');
|
|
63
|
+
expect(rel?.imports).toContain('net/http');
|
|
64
|
+
});
|
|
65
|
+
it('should handle broken relative imports gracefully', async () => {
|
|
66
|
+
const tsFile = path.join(tempDir, 'index.ts');
|
|
67
|
+
await fs.writeFile(tsFile, "import { x } from './non_existent';");
|
|
68
|
+
await graph.build(tempDir);
|
|
69
|
+
const rel = graph.getRelationships('index.ts');
|
|
70
|
+
// Should not contain the broken path since it's relative but missing
|
|
71
|
+
expect(rel?.imports).not.toContain('./non_existent');
|
|
72
|
+
});
|
|
73
|
+
it('should generate Mermaid visualization', async () => {
|
|
74
|
+
await fs.writeFile(path.join(tempDir, 'a.ts'), "import './b';");
|
|
75
|
+
await fs.writeFile(path.join(tempDir, 'b.ts'), "export const b = 1;");
|
|
76
|
+
await graph.build(tempDir);
|
|
77
|
+
const mermaid = graph.toMermaid();
|
|
78
|
+
expect(mermaid).toContain('graph TD');
|
|
79
|
+
expect(mermaid).toContain('["a.ts"]');
|
|
80
|
+
expect(mermaid).toContain('["b.ts"]');
|
|
81
|
+
expect(mermaid).toContain('-->');
|
|
82
|
+
});
|
|
83
|
+
it('should provide deep context', async () => {
|
|
84
|
+
await fs.writeFile(path.join(tempDir, 'util.ts'), "export const u = 1;");
|
|
85
|
+
await fs.writeFile(path.join(tempDir, 'main.ts'), "import {u} from './util'; export const m = 2;");
|
|
86
|
+
await fs.writeFile(path.join(tempDir, 'app.ts'), "import {m} from './main';");
|
|
87
|
+
await graph.build(tempDir);
|
|
88
|
+
const context = graph.getDeepContext('main.ts');
|
|
89
|
+
expect(context?.targetFile.path).toBe('main.ts');
|
|
90
|
+
expect(context?.dependencies[0].path).toBe('util.ts');
|
|
91
|
+
expect(context?.dependents[0].path).toBe('app.ts');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
+
import { ProjectKnowledgeGraph } from './graph.js';
|
|
3
|
+
import * as fs from 'fs/promises';
|
|
4
|
+
vi.mock('fs/promises');
|
|
5
|
+
describe('ProjectKnowledgeGraph Reproduction', () => {
|
|
6
|
+
let graph;
|
|
7
|
+
beforeEach(() => {
|
|
8
|
+
graph = new ProjectKnowledgeGraph();
|
|
9
|
+
vi.resetAllMocks();
|
|
10
|
+
// Default mock for stat
|
|
11
|
+
fs.stat.mockResolvedValue({
|
|
12
|
+
isDirectory: () => true,
|
|
13
|
+
mtimeMs: Date.now()
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
it('should resolve absolute imports from project root', async () => {
|
|
17
|
+
// Scenario: /app/src/feature/a.ts imports 'src/shared/b.ts'
|
|
18
|
+
const rootDir = '/root';
|
|
19
|
+
const indexFile = '/root/src/index.ts';
|
|
20
|
+
const utilsFile = '/root/src/utils.ts';
|
|
21
|
+
// Mock readdir to simulate:
|
|
22
|
+
// /root -> [src]
|
|
23
|
+
// /root/src -> [index.ts, utils.ts]
|
|
24
|
+
fs.readdir.mockImplementation(async (dir) => {
|
|
25
|
+
if (dir === '/root')
|
|
26
|
+
return [{ name: 'src', isDirectory: () => true }];
|
|
27
|
+
if (dir === '/root/src')
|
|
28
|
+
return [
|
|
29
|
+
{ name: 'index.ts', isDirectory: () => false },
|
|
30
|
+
{ name: 'utils.ts', isDirectory: () => false }
|
|
31
|
+
];
|
|
32
|
+
return [];
|
|
33
|
+
});
|
|
34
|
+
fs.readFile.mockImplementation(async (filePath) => {
|
|
35
|
+
if (filePath === indexFile)
|
|
36
|
+
return `import { u } from 'src/utils';`;
|
|
37
|
+
return '';
|
|
38
|
+
});
|
|
39
|
+
// Mock stat for specific files
|
|
40
|
+
fs.stat.mockImplementation(async (filePath) => {
|
|
41
|
+
if (filePath === rootDir)
|
|
42
|
+
return { isDirectory: () => true, mtimeMs: 100 };
|
|
43
|
+
return { isDirectory: () => false, mtimeMs: 100 };
|
|
44
|
+
});
|
|
45
|
+
await graph.build(rootDir);
|
|
46
|
+
const relationships = graph.getRelationships(indexFile);
|
|
47
|
+
// We expect 'src/utils.ts' to be in imports.
|
|
48
|
+
expect(relationships?.imports).toContain('src/utils.ts');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
+
import { HumanInteractionManager, getHumanManager } from './tools/human.js';
|
|
3
|
+
describe('Human-in-the-Loop Manager', () => {
|
|
4
|
+
let manager;
|
|
5
|
+
beforeEach(() => {
|
|
6
|
+
manager = new HumanInteractionManager();
|
|
7
|
+
});
|
|
8
|
+
describe('createInteraction', () => {
|
|
9
|
+
it('should create a confirmation interaction', async () => {
|
|
10
|
+
const interaction = await manager.createInteraction({
|
|
11
|
+
questionType: 'confirmation',
|
|
12
|
+
urgency: 'high',
|
|
13
|
+
question: 'ต้องการลบ Database จริงหรือไม่?',
|
|
14
|
+
context: 'Database มี 1000 records',
|
|
15
|
+
defaultOption: 'No'
|
|
16
|
+
});
|
|
17
|
+
expect(interaction.id).toMatch(/^hitl_\d+_[a-z0-9]+$/);
|
|
18
|
+
expect(interaction.questionType).toBe('confirmation');
|
|
19
|
+
expect(interaction.urgency).toBe('high');
|
|
20
|
+
expect(interaction.question).toBe('ต้องการลบ Database จริงหรือไม่?');
|
|
21
|
+
expect(interaction.status).toBe('pending');
|
|
22
|
+
expect(interaction.defaultOption).toBe('No');
|
|
23
|
+
});
|
|
24
|
+
it('should create a choice interaction with options', async () => {
|
|
25
|
+
const interaction = await manager.createInteraction({
|
|
26
|
+
questionType: 'choice',
|
|
27
|
+
urgency: 'medium',
|
|
28
|
+
question: 'เลือก Framework ที่ต้องการใช้',
|
|
29
|
+
options: ['React', 'Vue', 'Angular', 'Svelte']
|
|
30
|
+
});
|
|
31
|
+
expect(interaction.questionType).toBe('choice');
|
|
32
|
+
expect(interaction.options).toEqual(['React', 'Vue', 'Angular', 'Svelte']);
|
|
33
|
+
});
|
|
34
|
+
it('should create an input interaction', async () => {
|
|
35
|
+
const interaction = await manager.createInteraction({
|
|
36
|
+
questionType: 'input',
|
|
37
|
+
urgency: 'low',
|
|
38
|
+
question: 'ต้องการตั้งชื่อ function ว่าอะไร?'
|
|
39
|
+
});
|
|
40
|
+
expect(interaction.questionType).toBe('input');
|
|
41
|
+
expect(interaction.urgency).toBe('low');
|
|
42
|
+
});
|
|
43
|
+
it('should create a review interaction', async () => {
|
|
44
|
+
const interaction = await manager.createInteraction({
|
|
45
|
+
questionType: 'review',
|
|
46
|
+
urgency: 'critical',
|
|
47
|
+
question: 'กรุณาตรวจสอบ PR นี้ก่อน merge',
|
|
48
|
+
context: 'PR #123: Refactor authentication module'
|
|
49
|
+
});
|
|
50
|
+
expect(interaction.questionType).toBe('review');
|
|
51
|
+
expect(interaction.urgency).toBe('critical');
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
describe('recordResponse', () => {
|
|
55
|
+
it('should record response for existing interaction', async () => {
|
|
56
|
+
const interaction = await manager.createInteraction({
|
|
57
|
+
questionType: 'confirmation',
|
|
58
|
+
urgency: 'high',
|
|
59
|
+
question: 'Proceed?'
|
|
60
|
+
});
|
|
61
|
+
const updated = manager.recordResponse(interaction.id, 'Yes');
|
|
62
|
+
expect(updated).not.toBeNull();
|
|
63
|
+
expect(updated?.status).toBe('answered');
|
|
64
|
+
expect(updated?.response).toBe('Yes');
|
|
65
|
+
expect(updated?.respondedAt).toBeDefined();
|
|
66
|
+
});
|
|
67
|
+
it('should return null for non-existent interaction', () => {
|
|
68
|
+
const result = manager.recordResponse('invalid_id', 'response');
|
|
69
|
+
expect(result).toBeNull();
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
describe('getPendingInteractions', () => {
|
|
73
|
+
it('should return only pending interactions', async () => {
|
|
74
|
+
const i1 = await manager.createInteraction({
|
|
75
|
+
questionType: 'confirmation',
|
|
76
|
+
urgency: 'low',
|
|
77
|
+
question: 'Question 1'
|
|
78
|
+
});
|
|
79
|
+
const i2 = await manager.createInteraction({
|
|
80
|
+
questionType: 'input',
|
|
81
|
+
urgency: 'high',
|
|
82
|
+
question: 'Question 2'
|
|
83
|
+
});
|
|
84
|
+
// Answer first one
|
|
85
|
+
manager.recordResponse(i1.id, 'Yes');
|
|
86
|
+
const pending = manager.getPendingInteractions();
|
|
87
|
+
expect(pending.length).toBe(1);
|
|
88
|
+
expect(pending[0].id).toBe(i2.id);
|
|
89
|
+
});
|
|
90
|
+
it('should sort by urgency (critical first)', async () => {
|
|
91
|
+
await manager.createInteraction({
|
|
92
|
+
questionType: 'input',
|
|
93
|
+
urgency: 'low',
|
|
94
|
+
question: 'Low priority'
|
|
95
|
+
});
|
|
96
|
+
await manager.createInteraction({
|
|
97
|
+
questionType: 'confirmation',
|
|
98
|
+
urgency: 'critical',
|
|
99
|
+
question: 'Critical priority'
|
|
100
|
+
});
|
|
101
|
+
await manager.createInteraction({
|
|
102
|
+
questionType: 'choice',
|
|
103
|
+
urgency: 'medium',
|
|
104
|
+
question: 'Medium priority',
|
|
105
|
+
options: ['A', 'B']
|
|
106
|
+
});
|
|
107
|
+
const pending = manager.getPendingInteractions();
|
|
108
|
+
expect(pending[0].urgency).toBe('critical');
|
|
109
|
+
expect(pending[1].urgency).toBe('medium');
|
|
110
|
+
expect(pending[2].urgency).toBe('low');
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
describe('getHistory', () => {
|
|
114
|
+
it('should return all interactions sorted by time (newest first)', async () => {
|
|
115
|
+
await manager.createInteraction({
|
|
116
|
+
questionType: 'input',
|
|
117
|
+
urgency: 'low',
|
|
118
|
+
question: 'First'
|
|
119
|
+
});
|
|
120
|
+
await new Promise(resolve => setTimeout(resolve, 10));
|
|
121
|
+
await manager.createInteraction({
|
|
122
|
+
questionType: 'input',
|
|
123
|
+
urgency: 'low',
|
|
124
|
+
question: 'Second'
|
|
125
|
+
});
|
|
126
|
+
const history = manager.getHistory();
|
|
127
|
+
expect(history.length).toBe(2);
|
|
128
|
+
expect(history[0].question).toBe('Second');
|
|
129
|
+
expect(history[1].question).toBe('First');
|
|
130
|
+
});
|
|
131
|
+
it('should respect limit parameter', async () => {
|
|
132
|
+
for (let i = 0; i < 5; i++) {
|
|
133
|
+
await manager.createInteraction({
|
|
134
|
+
questionType: 'input',
|
|
135
|
+
urgency: 'low',
|
|
136
|
+
question: `Question ${i}`
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const history = manager.getHistory(3);
|
|
140
|
+
expect(history.length).toBe(3);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
describe('formatInteraction', () => {
|
|
144
|
+
it('should format interaction with all details', async () => {
|
|
145
|
+
const interaction = await manager.createInteraction({
|
|
146
|
+
questionType: 'choice',
|
|
147
|
+
urgency: 'high',
|
|
148
|
+
question: 'Which option?',
|
|
149
|
+
context: 'This is important',
|
|
150
|
+
options: ['Option A', 'Option B'],
|
|
151
|
+
defaultOption: 'Option A'
|
|
152
|
+
});
|
|
153
|
+
const formatted = manager.formatInteraction(interaction);
|
|
154
|
+
expect(formatted).toContain('HUMAN INPUT REQUIRED');
|
|
155
|
+
expect(formatted).toContain('HIGH');
|
|
156
|
+
expect(formatted).toContain('Which option?');
|
|
157
|
+
expect(formatted).toContain('This is important');
|
|
158
|
+
expect(formatted).toContain('Option A');
|
|
159
|
+
expect(formatted).toContain('Option B');
|
|
160
|
+
expect(formatted).toContain('(default)');
|
|
161
|
+
expect(formatted).toContain(interaction.id);
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
describe('clearOldInteractions', () => {
|
|
165
|
+
it('should clear interactions older than specified hours', async () => {
|
|
166
|
+
// Create interaction with old timestamp (manually set)
|
|
167
|
+
const interaction = await manager.createInteraction({
|
|
168
|
+
questionType: 'input',
|
|
169
|
+
urgency: 'low',
|
|
170
|
+
question: 'Old question'
|
|
171
|
+
});
|
|
172
|
+
// Since we can't easily mock time, just verify the method doesn't crash
|
|
173
|
+
const cleared = manager.clearOldInteractions(0); // Clear everything older than 0 hours
|
|
174
|
+
// The interaction we just created should still be there (it's not old enough)
|
|
175
|
+
// This is more of a smoke test
|
|
176
|
+
expect(typeof cleared).toBe('number');
|
|
177
|
+
});
|
|
178
|
+
});
|
|
179
|
+
describe('Collaborative Workflow', () => {
|
|
180
|
+
it('should support full collaborative workflow', async () => {
|
|
181
|
+
// Step 1: AI asks for confirmation before destructive action
|
|
182
|
+
const deleteConfirm = await manager.createInteraction({
|
|
183
|
+
questionType: 'confirmation',
|
|
184
|
+
urgency: 'critical',
|
|
185
|
+
question: 'กำลังจะลบ production database ทั้งหมด ต้องการดำเนินการต่อหรือไม่?',
|
|
186
|
+
context: 'Database: prod-db-001, Size: 500GB, Records: 10M',
|
|
187
|
+
defaultOption: 'No'
|
|
188
|
+
});
|
|
189
|
+
expect(deleteConfirm.status).toBe('pending');
|
|
190
|
+
// Step 2: Human responds "No"
|
|
191
|
+
manager.recordResponse(deleteConfirm.id, 'No');
|
|
192
|
+
// Step 3: AI asks for alternative approach
|
|
193
|
+
const alternative = await manager.createInteraction({
|
|
194
|
+
questionType: 'choice',
|
|
195
|
+
urgency: 'medium',
|
|
196
|
+
question: 'ต้องการดำเนินการอย่างไรแทน?',
|
|
197
|
+
options: [
|
|
198
|
+
'Backup แล้วค่อยลบ',
|
|
199
|
+
'ลบเฉพาะ records ที่เก่ากว่า 1 ปี',
|
|
200
|
+
'ยกเลิกทั้งหมด'
|
|
201
|
+
]
|
|
202
|
+
});
|
|
203
|
+
// Step 4: Human selects option
|
|
204
|
+
manager.recordResponse(alternative.id, 'Backup แล้วค่อยลบ');
|
|
205
|
+
// Verify workflow
|
|
206
|
+
const history = manager.getHistory();
|
|
207
|
+
expect(history.length).toBe(2);
|
|
208
|
+
const answered = history.filter(i => i.status === 'answered');
|
|
209
|
+
expect(answered.length).toBe(2);
|
|
210
|
+
expect(answered.find(i => i.id === deleteConfirm.id)?.response).toBe('No');
|
|
211
|
+
expect(answered.find(i => i.id === alternative.id)?.response).toBe('Backup แล้วค่อยลบ');
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
describe('getHumanManager singleton', () => {
|
|
216
|
+
it('should return the same instance', () => {
|
|
217
|
+
const manager1 = getHumanManager();
|
|
218
|
+
const manager2 = getHumanManager();
|
|
219
|
+
expect(manager1).toBe(manager2);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
// We need to import the register functions to get the callbacks,
|
|
3
|
+
// but we can just use the classes directly for this integration logic test.
|
|
4
|
+
// Actually, testing the *interaction* via the tool layer is better.
|
|
5
|
+
import { registerThinkingTools } from './tools/thinking.js';
|
|
6
|
+
import { registerGraphTools } from './tools/graph.js';
|
|
7
|
+
import { registerNoteTools } from './tools/notes.js';
|
|
8
|
+
describe('Integration Workflow', () => {
|
|
9
|
+
let toolCallbacks = {};
|
|
10
|
+
const mockServer = {
|
|
11
|
+
tool: (name, desc, schema, cb) => {
|
|
12
|
+
toolCallbacks[name] = cb;
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
// Mocks
|
|
16
|
+
const mockThinking = { processThought: vi.fn(), clearHistory: vi.fn() };
|
|
17
|
+
const mockGraph = { build: vi.fn(), getRelationships: vi.fn() };
|
|
18
|
+
const mockNotes = { addNote: vi.fn() };
|
|
19
|
+
beforeEach(() => {
|
|
20
|
+
toolCallbacks = {};
|
|
21
|
+
registerThinkingTools(mockServer, mockThinking);
|
|
22
|
+
registerGraphTools(mockServer, mockGraph);
|
|
23
|
+
registerNoteTools(mockServer, mockNotes);
|
|
24
|
+
vi.clearAllMocks();
|
|
25
|
+
});
|
|
26
|
+
it('should support a full analysis workflow', async () => {
|
|
27
|
+
// Step 1: Start Thinking
|
|
28
|
+
mockThinking.processThought.mockResolvedValue({
|
|
29
|
+
content: [{ type: "text", text: "Thought processed" }]
|
|
30
|
+
});
|
|
31
|
+
await toolCallbacks['sequentialthinking']({
|
|
32
|
+
thought: "Analyze architecture",
|
|
33
|
+
thoughtNumber: 1,
|
|
34
|
+
totalThoughts: 5,
|
|
35
|
+
nextThoughtNeeded: true,
|
|
36
|
+
thoughtType: 'analysis'
|
|
37
|
+
});
|
|
38
|
+
expect(mockThinking.processThought).toHaveBeenCalledWith(expect.objectContaining({
|
|
39
|
+
thought: "Analyze architecture"
|
|
40
|
+
}));
|
|
41
|
+
// Step 2: Build Graph
|
|
42
|
+
mockGraph.build.mockResolvedValue({ nodeCount: 10, totalFiles: 20 });
|
|
43
|
+
await toolCallbacks['build_project_graph']({ path: '.' });
|
|
44
|
+
expect(mockGraph.build).toHaveBeenCalled();
|
|
45
|
+
// Step 3: Get Relationships
|
|
46
|
+
mockGraph.getRelationships.mockReturnValue({ imports: ['utils.ts'] });
|
|
47
|
+
const relResult = await toolCallbacks['get_file_relationships']({ filePath: 'src/index.ts' });
|
|
48
|
+
expect(JSON.parse(relResult.content[0].text).imports).toContain('utils.ts');
|
|
49
|
+
// Step 4: Add Note
|
|
50
|
+
mockNotes.addNote.mockResolvedValue({ id: '123', title: 'Arch Note' });
|
|
51
|
+
await toolCallbacks['manage_notes']({
|
|
52
|
+
action: 'add',
|
|
53
|
+
title: 'Architecture Review',
|
|
54
|
+
content: 'Found circular deps'
|
|
55
|
+
});
|
|
56
|
+
expect(mockNotes.addNote).toHaveBeenCalledWith('Architecture Review', 'Found circular deps', undefined, undefined, undefined);
|
|
57
|
+
});
|
|
58
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|