nano-brain 2026.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS_SNIPPET.md +36 -0
- package/CHANGELOG.md +68 -0
- package/README.md +281 -0
- package/SKILL.md +153 -0
- package/bin/cli.js +18 -0
- package/index.html +929 -0
- package/nano-brain +4 -0
- package/opencode-mcp.json +9 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/design.md +68 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/proposal.md +27 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/mcp-server/spec.md +40 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/specs/search-pipeline/spec.md +29 -0
- package/openspec/changes/archive/2026-02-16-fix-mcp-server-bugs/tasks.md +37 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/.openspec.yaml +2 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/design.md +111 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/proposal.md +30 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/mcp-server/spec.md +33 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/storage-limits/spec.md +90 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/specs/workspace-scoping/spec.md +66 -0
- package/openspec/changes/archive/2026-02-23-workspace-scoped-memory-and-storage-limits/tasks.md +199 -0
- package/openspec/changes/codebase-indexing/.openspec.yaml +2 -0
- package/openspec/changes/codebase-indexing/design.md +169 -0
- package/openspec/changes/codebase-indexing/proposal.md +30 -0
- package/openspec/changes/codebase-indexing/specs/codebase-collection/spec.md +187 -0
- package/openspec/changes/codebase-indexing/specs/mcp-server/spec.md +36 -0
- package/openspec/changes/codebase-indexing/tasks.md +56 -0
- package/openspec/specs/mcp-integration-testing/spec.md +50 -0
- package/openspec/specs/mcp-server/spec.md +75 -0
- package/openspec/specs/search-pipeline/spec.md +29 -0
- package/openspec/specs/storage-limits/spec.md +94 -0
- package/openspec/specs/workspace-scoping/spec.md +70 -0
- package/package.json +34 -0
- package/site/build.js +66 -0
- package/site/partials/_api.html +83 -0
- package/site/partials/_compare.html +100 -0
- package/site/partials/_config.html +23 -0
- package/site/partials/_features.html +43 -0
- package/site/partials/_footer.html +6 -0
- package/site/partials/_hero.html +9 -0
- package/site/partials/_how-it-works.html +26 -0
- package/site/partials/_models.html +18 -0
- package/site/partials/_quick-start.html +15 -0
- package/site/partials/_stats.html +1 -0
- package/site/partials/_tech-stack.html +13 -0
- package/site/script.js +12 -0
- package/site/shell.html +44 -0
- package/site/styles.css +548 -0
- package/src/chunker.ts +427 -0
- package/src/codebase.ts +331 -0
- package/src/collections.ts +192 -0
- package/src/embeddings.ts +293 -0
- package/src/expansion.ts +79 -0
- package/src/harvester.ts +306 -0
- package/src/index.ts +503 -0
- package/src/reranker.ts +103 -0
- package/src/search.ts +294 -0
- package/src/server.ts +664 -0
- package/src/storage.ts +221 -0
- package/src/store.ts +623 -0
- package/src/types.ts +202 -0
- package/src/watcher.ts +384 -0
- package/test/chunker.test.ts +479 -0
- package/test/cli.test.ts +309 -0
- package/test/codebase-chunker.test.ts +446 -0
- package/test/codebase.test.ts +678 -0
- package/test/collections.test.ts +571 -0
- package/test/harvester.test.ts +636 -0
- package/test/integration.test.ts +150 -0
- package/test/llm.test.ts +322 -0
- package/test/search.test.ts +572 -0
- package/test/server.test.ts +541 -0
- package/test/storage.test.ts +302 -0
- package/test/store.test.ts +465 -0
- package/test/watcher.test.ts +656 -0
- package/test/workspace.test.ts +239 -0
- package/tsconfig.json +19 -0
- package/vitest.config.ts +16 -0
|
@@ -0,0 +1,678 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import {
|
|
3
|
+
detectProjectType,
|
|
4
|
+
loadGitignorePatterns,
|
|
5
|
+
mergeExcludePatterns,
|
|
6
|
+
resolveExtensions,
|
|
7
|
+
scanCodebaseFiles,
|
|
8
|
+
indexCodebase,
|
|
9
|
+
getCodebaseStats,
|
|
10
|
+
} from '../src/codebase.js';
|
|
11
|
+
import { createStore, computeHash } from '../src/store.js';
|
|
12
|
+
import type { Store, CodebaseConfig } from '../src/types.js';
|
|
13
|
+
import * as fs from 'fs';
|
|
14
|
+
import * as path from 'path';
|
|
15
|
+
import * as os from 'os';
|
|
16
|
+
|
|
17
|
+
describe('detectProjectType', () => {
|
|
18
|
+
let tmpDir: string;
|
|
19
|
+
|
|
20
|
+
beforeEach(() => {
|
|
21
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-codebase-test-'));
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
if (fs.existsSync(tmpDir)) {
|
|
26
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should detect Node.js project from package.json', () => {
|
|
31
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
32
|
+
const extensions = detectProjectType(tmpDir);
|
|
33
|
+
expect(extensions).toContain('.ts');
|
|
34
|
+
expect(extensions).toContain('.tsx');
|
|
35
|
+
expect(extensions).toContain('.js');
|
|
36
|
+
expect(extensions).toContain('.jsx');
|
|
37
|
+
expect(extensions).toContain('.md');
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should detect Python project from pyproject.toml', () => {
|
|
41
|
+
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '');
|
|
42
|
+
const extensions = detectProjectType(tmpDir);
|
|
43
|
+
expect(extensions).toContain('.py');
|
|
44
|
+
expect(extensions).toContain('.pyi');
|
|
45
|
+
expect(extensions).toContain('.md');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it('should detect Python project from requirements.txt', () => {
|
|
49
|
+
fs.writeFileSync(path.join(tmpDir, 'requirements.txt'), '');
|
|
50
|
+
const extensions = detectProjectType(tmpDir);
|
|
51
|
+
expect(extensions).toContain('.py');
|
|
52
|
+
expect(extensions).toContain('.md');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it('should detect Go project from go.mod', () => {
|
|
56
|
+
fs.writeFileSync(path.join(tmpDir, 'go.mod'), '');
|
|
57
|
+
const extensions = detectProjectType(tmpDir);
|
|
58
|
+
expect(extensions).toContain('.go');
|
|
59
|
+
expect(extensions).toContain('.md');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should detect Rust project from Cargo.toml', () => {
|
|
63
|
+
fs.writeFileSync(path.join(tmpDir, 'Cargo.toml'), '');
|
|
64
|
+
const extensions = detectProjectType(tmpDir);
|
|
65
|
+
expect(extensions).toContain('.rs');
|
|
66
|
+
expect(extensions).toContain('.md');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('should detect Java project from pom.xml', () => {
|
|
70
|
+
fs.writeFileSync(path.join(tmpDir, 'pom.xml'), '');
|
|
71
|
+
const extensions = detectProjectType(tmpDir);
|
|
72
|
+
expect(extensions).toContain('.java');
|
|
73
|
+
expect(extensions).toContain('.kt');
|
|
74
|
+
expect(extensions).toContain('.md');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should detect Ruby project from Gemfile', () => {
|
|
78
|
+
fs.writeFileSync(path.join(tmpDir, 'Gemfile'), '');
|
|
79
|
+
const extensions = detectProjectType(tmpDir);
|
|
80
|
+
expect(extensions).toContain('.rb');
|
|
81
|
+
expect(extensions).toContain('.erb');
|
|
82
|
+
expect(extensions).toContain('.md');
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should detect multiple project types', () => {
|
|
86
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
87
|
+
fs.writeFileSync(path.join(tmpDir, 'pyproject.toml'), '');
|
|
88
|
+
const extensions = detectProjectType(tmpDir);
|
|
89
|
+
expect(extensions).toContain('.ts');
|
|
90
|
+
expect(extensions).toContain('.py');
|
|
91
|
+
expect(extensions).toContain('.md');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should return default extensions when no markers found', () => {
|
|
95
|
+
const extensions = detectProjectType(tmpDir);
|
|
96
|
+
expect(extensions).toContain('.ts');
|
|
97
|
+
expect(extensions).toContain('.py');
|
|
98
|
+
expect(extensions).toContain('.go');
|
|
99
|
+
expect(extensions).toContain('.rs');
|
|
100
|
+
expect(extensions).toContain('.md');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should always include .md extension', () => {
|
|
104
|
+
const extensions = detectProjectType(tmpDir);
|
|
105
|
+
expect(extensions).toContain('.md');
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
describe('loadGitignorePatterns', () => {
|
|
110
|
+
let tmpDir: string;
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-gitignore-test-'));
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
if (fs.existsSync(tmpDir)) {
|
|
118
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return empty array when no .gitignore exists', () => {
|
|
123
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
124
|
+
expect(patterns).toEqual([]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should parse simple patterns', () => {
|
|
128
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules\ndist\n*.log');
|
|
129
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
130
|
+
expect(patterns).toContain('node_modules');
|
|
131
|
+
expect(patterns).toContain('dist');
|
|
132
|
+
expect(patterns).toContain('*.log');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('should ignore comments', () => {
|
|
136
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), '# This is a comment\nnode_modules\n# Another comment');
|
|
137
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
138
|
+
expect(patterns).toEqual(['node_modules']);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should ignore empty lines', () => {
|
|
142
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules\n\n\ndist\n');
|
|
143
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
144
|
+
expect(patterns).toEqual(['node_modules', 'dist']);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should trim whitespace', () => {
|
|
148
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), ' node_modules \n dist ');
|
|
149
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
150
|
+
expect(patterns).toEqual(['node_modules', 'dist']);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it('should handle complex .gitignore', () => {
|
|
154
|
+
const gitignore = `
|
|
155
|
+
# Dependencies
|
|
156
|
+
node_modules/
|
|
157
|
+
.pnp
|
|
158
|
+
.pnp.js
|
|
159
|
+
|
|
160
|
+
# Build
|
|
161
|
+
dist/
|
|
162
|
+
build/
|
|
163
|
+
*.min.js
|
|
164
|
+
|
|
165
|
+
# IDE
|
|
166
|
+
.idea/
|
|
167
|
+
.vscode/
|
|
168
|
+
*.swp
|
|
169
|
+
`.trim();
|
|
170
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), gitignore);
|
|
171
|
+
const patterns = loadGitignorePatterns(tmpDir);
|
|
172
|
+
expect(patterns).toContain('node_modules/');
|
|
173
|
+
expect(patterns).toContain('dist/');
|
|
174
|
+
expect(patterns).toContain('.idea/');
|
|
175
|
+
expect(patterns).not.toContain('# Dependencies');
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
describe('mergeExcludePatterns', () => {
|
|
180
|
+
let tmpDir: string;
|
|
181
|
+
|
|
182
|
+
beforeEach(() => {
|
|
183
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-merge-test-'));
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
afterEach(() => {
|
|
187
|
+
if (fs.existsSync(tmpDir)) {
|
|
188
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
189
|
+
}
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('should include builtin patterns', () => {
|
|
193
|
+
const config: CodebaseConfig = { enabled: true };
|
|
194
|
+
const patterns = mergeExcludePatterns(config, tmpDir);
|
|
195
|
+
expect(patterns).toContain('node_modules');
|
|
196
|
+
expect(patterns).toContain('.git');
|
|
197
|
+
expect(patterns).toContain('dist');
|
|
198
|
+
expect(patterns).toContain('__pycache__');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should include gitignore patterns', () => {
|
|
202
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'custom_ignore\n*.custom');
|
|
203
|
+
const config: CodebaseConfig = { enabled: true };
|
|
204
|
+
const patterns = mergeExcludePatterns(config, tmpDir);
|
|
205
|
+
expect(patterns).toContain('custom_ignore');
|
|
206
|
+
expect(patterns).toContain('*.custom');
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
it('should include config exclude patterns', () => {
|
|
210
|
+
const config: CodebaseConfig = { enabled: true, exclude: ['my_exclude', '*.test.ts'] };
|
|
211
|
+
const patterns = mergeExcludePatterns(config, tmpDir);
|
|
212
|
+
expect(patterns).toContain('my_exclude');
|
|
213
|
+
expect(patterns).toContain('*.test.ts');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should deduplicate patterns', () => {
|
|
217
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'node_modules\ndist');
|
|
218
|
+
const config: CodebaseConfig = { enabled: true, exclude: ['node_modules', 'custom'] };
|
|
219
|
+
const patterns = mergeExcludePatterns(config, tmpDir);
|
|
220
|
+
const nodeModulesCount = patterns.filter(p => p === 'node_modules').length;
|
|
221
|
+
expect(nodeModulesCount).toBe(1);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('should merge all sources', () => {
|
|
225
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'gitignore_pattern');
|
|
226
|
+
const config: CodebaseConfig = { enabled: true, exclude: ['config_pattern'] };
|
|
227
|
+
const patterns = mergeExcludePatterns(config, tmpDir);
|
|
228
|
+
expect(patterns).toContain('node_modules');
|
|
229
|
+
expect(patterns).toContain('gitignore_pattern');
|
|
230
|
+
expect(patterns).toContain('config_pattern');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
describe('resolveExtensions', () => {
|
|
235
|
+
let tmpDir: string;
|
|
236
|
+
|
|
237
|
+
beforeEach(() => {
|
|
238
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-resolve-test-'));
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
afterEach(() => {
|
|
242
|
+
if (fs.existsSync(tmpDir)) {
|
|
243
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should use config extensions when provided', () => {
|
|
248
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.custom', '.ext'] };
|
|
249
|
+
const extensions = resolveExtensions(config, tmpDir);
|
|
250
|
+
expect(extensions).toEqual(['.custom', '.ext']);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should detect project type when no config extensions', () => {
|
|
254
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
255
|
+
const config: CodebaseConfig = { enabled: true };
|
|
256
|
+
const extensions = resolveExtensions(config, tmpDir);
|
|
257
|
+
expect(extensions).toContain('.ts');
|
|
258
|
+
expect(extensions).toContain('.js');
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
it('should use empty array config as empty', () => {
|
|
262
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
263
|
+
const config: CodebaseConfig = { enabled: true, extensions: [] };
|
|
264
|
+
const extensions = resolveExtensions(config, tmpDir);
|
|
265
|
+
expect(extensions).toContain('.ts');
|
|
266
|
+
});
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
describe('scanCodebaseFiles', () => {
|
|
270
|
+
let tmpDir: string;
|
|
271
|
+
|
|
272
|
+
beforeEach(() => {
|
|
273
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-scan-test-'));
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
afterEach(() => {
|
|
277
|
+
if (fs.existsSync(tmpDir)) {
|
|
278
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('should find files matching extensions', async () => {
|
|
283
|
+
fs.writeFileSync(path.join(tmpDir, 'file.ts'), 'const x = 1;');
|
|
284
|
+
fs.writeFileSync(path.join(tmpDir, 'file.js'), 'const y = 2;');
|
|
285
|
+
fs.writeFileSync(path.join(tmpDir, 'file.txt'), 'text');
|
|
286
|
+
|
|
287
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts', '.js'] };
|
|
288
|
+
const { files, skippedTooLarge } = await scanCodebaseFiles(tmpDir, config);
|
|
289
|
+
|
|
290
|
+
expect(files.length).toBe(2);
|
|
291
|
+
expect(files.some(f => f.endsWith('file.ts'))).toBe(true);
|
|
292
|
+
expect(files.some(f => f.endsWith('file.js'))).toBe(true);
|
|
293
|
+
expect(files.some(f => f.endsWith('file.txt'))).toBe(false);
|
|
294
|
+
expect(skippedTooLarge).toBe(0);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('should find files in subdirectories', async () => {
|
|
298
|
+
fs.mkdirSync(path.join(tmpDir, 'src'));
|
|
299
|
+
fs.mkdirSync(path.join(tmpDir, 'src', 'utils'));
|
|
300
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'index.ts'), 'export {};');
|
|
301
|
+
fs.writeFileSync(path.join(tmpDir, 'src', 'utils', 'helper.ts'), 'export {};');
|
|
302
|
+
|
|
303
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
304
|
+
const { files } = await scanCodebaseFiles(tmpDir, config);
|
|
305
|
+
|
|
306
|
+
expect(files.length).toBe(2);
|
|
307
|
+
expect(files.some(f => f.includes('index.ts'))).toBe(true);
|
|
308
|
+
expect(files.some(f => f.includes('helper.ts'))).toBe(true);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should exclude node_modules by default', async () => {
|
|
312
|
+
fs.mkdirSync(path.join(tmpDir, 'node_modules'));
|
|
313
|
+
fs.writeFileSync(path.join(tmpDir, 'node_modules', 'dep.ts'), 'export {};');
|
|
314
|
+
fs.writeFileSync(path.join(tmpDir, 'src.ts'), 'export {};');
|
|
315
|
+
|
|
316
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
317
|
+
const { files } = await scanCodebaseFiles(tmpDir, config);
|
|
318
|
+
|
|
319
|
+
expect(files.length).toBe(1);
|
|
320
|
+
expect(files[0]).toContain('src.ts');
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
it('should exclude .git by default', async () => {
|
|
324
|
+
fs.mkdirSync(path.join(tmpDir, '.git'));
|
|
325
|
+
fs.writeFileSync(path.join(tmpDir, '.git', 'config.ts'), 'export {};');
|
|
326
|
+
fs.writeFileSync(path.join(tmpDir, 'main.ts'), 'export {};');
|
|
327
|
+
|
|
328
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
329
|
+
const { files } = await scanCodebaseFiles(tmpDir, config);
|
|
330
|
+
|
|
331
|
+
expect(files.length).toBe(1);
|
|
332
|
+
expect(files[0]).toContain('main.ts');
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it('should respect custom exclude patterns', async () => {
|
|
336
|
+
fs.mkdirSync(path.join(tmpDir, 'tests'));
|
|
337
|
+
fs.writeFileSync(path.join(tmpDir, 'tests', 'test.ts'), 'export {};');
|
|
338
|
+
fs.writeFileSync(path.join(tmpDir, 'main.ts'), 'export {};');
|
|
339
|
+
|
|
340
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'], exclude: ['tests'] };
|
|
341
|
+
const { files } = await scanCodebaseFiles(tmpDir, config);
|
|
342
|
+
|
|
343
|
+
expect(files.length).toBe(1);
|
|
344
|
+
expect(files[0]).toContain('main.ts');
|
|
345
|
+
});
|
|
346
|
+
|
|
347
|
+
it('should skip files larger than maxFileSize', async () => {
|
|
348
|
+
fs.writeFileSync(path.join(tmpDir, 'small.ts'), 'const x = 1;');
|
|
349
|
+
fs.writeFileSync(path.join(tmpDir, 'large.ts'), 'x'.repeat(1000));
|
|
350
|
+
|
|
351
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'], maxFileSize: '500' };
|
|
352
|
+
const { files, skippedTooLarge } = await scanCodebaseFiles(tmpDir, config);
|
|
353
|
+
|
|
354
|
+
expect(files.length).toBe(1);
|
|
355
|
+
expect(files[0]).toContain('small.ts');
|
|
356
|
+
expect(skippedTooLarge).toBe(1);
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it('should return empty array for empty directory', async () => {
|
|
360
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
361
|
+
const { files, skippedTooLarge } = await scanCodebaseFiles(tmpDir, config);
|
|
362
|
+
|
|
363
|
+
expect(files).toEqual([]);
|
|
364
|
+
expect(skippedTooLarge).toBe(0);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it('should handle non-existent directory gracefully', async () => {
|
|
368
|
+
const nonExistent = path.join(tmpDir, 'does-not-exist');
|
|
369
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
370
|
+
const { files } = await scanCodebaseFiles(nonExistent, config);
|
|
371
|
+
|
|
372
|
+
expect(files).toEqual([]);
|
|
373
|
+
});
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
describe('indexCodebase - integration', () => {
|
|
377
|
+
let tmpDir: string;
|
|
378
|
+
let dbPath: string;
|
|
379
|
+
let store: Store;
|
|
380
|
+
|
|
381
|
+
beforeEach(() => {
|
|
382
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-index-test-'));
|
|
383
|
+
dbPath = path.join(tmpDir, 'test.db');
|
|
384
|
+
store = createStore(dbPath);
|
|
385
|
+
});
|
|
386
|
+
|
|
387
|
+
afterEach(() => {
|
|
388
|
+
store.close();
|
|
389
|
+
if (fs.existsSync(tmpDir)) {
|
|
390
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
391
|
+
}
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it('should index source files', async () => {
|
|
395
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
396
|
+
fs.mkdirSync(srcDir);
|
|
397
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
398
|
+
fs.writeFileSync(path.join(srcDir, 'utils.ts'), 'export const util = () => {};');
|
|
399
|
+
|
|
400
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
401
|
+
const result = await indexCodebase(store, srcDir, config, 'test-project-hash');
|
|
402
|
+
|
|
403
|
+
expect(result.filesScanned).toBe(2);
|
|
404
|
+
expect(result.filesIndexed).toBe(2);
|
|
405
|
+
expect(result.filesSkippedUnchanged).toBe(0);
|
|
406
|
+
expect(result.chunksCreated).toBeGreaterThan(0);
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
it('should skip unchanged files on re-index', async () => {
|
|
410
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
411
|
+
fs.mkdirSync(srcDir);
|
|
412
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
413
|
+
|
|
414
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
415
|
+
|
|
416
|
+
await indexCodebase(store, srcDir, config, 'test-hash');
|
|
417
|
+
const result = await indexCodebase(store, srcDir, config, 'test-hash');
|
|
418
|
+
|
|
419
|
+
expect(result.filesScanned).toBe(1);
|
|
420
|
+
expect(result.filesIndexed).toBe(0);
|
|
421
|
+
expect(result.filesSkippedUnchanged).toBe(1);
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
it('should re-index modified files', async () => {
|
|
425
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
426
|
+
fs.mkdirSync(srcDir);
|
|
427
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
428
|
+
|
|
429
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
430
|
+
|
|
431
|
+
await indexCodebase(store, srcDir, config, 'test-hash');
|
|
432
|
+
|
|
433
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => { return 42; };');
|
|
434
|
+
const result = await indexCodebase(store, srcDir, config, 'test-hash');
|
|
435
|
+
|
|
436
|
+
expect(result.filesIndexed).toBe(1);
|
|
437
|
+
expect(result.filesSkippedUnchanged).toBe(0);
|
|
438
|
+
});
|
|
439
|
+
|
|
440
|
+
it('should deactivate deleted files', async () => {
|
|
441
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
442
|
+
fs.mkdirSync(srcDir);
|
|
443
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
444
|
+
fs.writeFileSync(path.join(srcDir, 'delete-me.ts'), 'export const x = 1;');
|
|
445
|
+
|
|
446
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
447
|
+
|
|
448
|
+
await indexCodebase(store, srcDir, config, 'test-hash');
|
|
449
|
+
|
|
450
|
+
const docBefore = store.findDocument(path.join(srcDir, 'delete-me.ts'));
|
|
451
|
+
expect(docBefore).not.toBeNull();
|
|
452
|
+
expect(docBefore?.active).toBe(true);
|
|
453
|
+
|
|
454
|
+
fs.unlinkSync(path.join(srcDir, 'delete-me.ts'));
|
|
455
|
+
await indexCodebase(store, srcDir, config, 'test-hash');
|
|
456
|
+
|
|
457
|
+
const docAfter = store.findDocument(path.join(srcDir, 'delete-me.ts'));
|
|
458
|
+
expect(docAfter).toBeNull();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
it('should set correct projectHash on documents', async () => {
|
|
462
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
463
|
+
fs.mkdirSync(srcDir);
|
|
464
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
465
|
+
|
|
466
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
467
|
+
await indexCodebase(store, srcDir, config, 'my-project-hash');
|
|
468
|
+
|
|
469
|
+
const doc = store.findDocument(path.join(srcDir, 'main.ts'));
|
|
470
|
+
expect(doc?.projectHash).toBe('my-project-hash');
|
|
471
|
+
expect(doc?.collection).toBe('codebase');
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('should enforce storage budget', async () => {
|
|
475
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
476
|
+
fs.mkdirSync(srcDir);
|
|
477
|
+
fs.writeFileSync(path.join(srcDir, 'file1.ts'), 'x'.repeat(500));
|
|
478
|
+
fs.writeFileSync(path.join(srcDir, 'file2.ts'), 'y'.repeat(500));
|
|
479
|
+
fs.writeFileSync(path.join(srcDir, 'file3.ts'), 'z'.repeat(500));
|
|
480
|
+
|
|
481
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'], maxSize: '1000' };
|
|
482
|
+
const result = await indexCodebase(store, srcDir, config, 'test-hash');
|
|
483
|
+
|
|
484
|
+
expect(result.filesSkippedBudget).toBeGreaterThan(0);
|
|
485
|
+
expect(result.filesIndexed).toBeLessThan(3);
|
|
486
|
+
});
|
|
487
|
+
|
|
488
|
+
it('should return storage usage info', async () => {
|
|
489
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
490
|
+
fs.mkdirSync(srcDir);
|
|
491
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
492
|
+
|
|
493
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'] };
|
|
494
|
+
const result = await indexCodebase(store, srcDir, config, 'test-hash');
|
|
495
|
+
|
|
496
|
+
expect(result.storageUsedBytes).toBeGreaterThan(0);
|
|
497
|
+
expect(result.maxSizeBytes).toBe(2 * 1024 * 1024 * 1024);
|
|
498
|
+
});
|
|
499
|
+
|
|
500
|
+
it('should use custom maxSize from config', async () => {
|
|
501
|
+
const srcDir = path.join(tmpDir, 'workspace');
|
|
502
|
+
fs.mkdirSync(srcDir);
|
|
503
|
+
fs.writeFileSync(path.join(srcDir, 'main.ts'), 'export const main = () => {};');
|
|
504
|
+
|
|
505
|
+
const config: CodebaseConfig = { enabled: true, extensions: ['.ts'], maxSize: '500MB' };
|
|
506
|
+
const result = await indexCodebase(store, srcDir, config, 'test-hash');
|
|
507
|
+
|
|
508
|
+
expect(result.maxSizeBytes).toBe(500 * 1024 * 1024);
|
|
509
|
+
});
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
describe('getCollectionStorageSize - integration', () => {
|
|
513
|
+
let tmpDir: string;
|
|
514
|
+
let dbPath: string;
|
|
515
|
+
let store: Store;
|
|
516
|
+
|
|
517
|
+
beforeEach(() => {
|
|
518
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-storage-size-test-'));
|
|
519
|
+
dbPath = path.join(tmpDir, 'test.db');
|
|
520
|
+
store = createStore(dbPath);
|
|
521
|
+
});
|
|
522
|
+
|
|
523
|
+
afterEach(() => {
|
|
524
|
+
store.close();
|
|
525
|
+
if (fs.existsSync(tmpDir)) {
|
|
526
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
it('should return 0 for empty collection', () => {
|
|
531
|
+
const size = store.getCollectionStorageSize('codebase');
|
|
532
|
+
expect(size).toBe(0);
|
|
533
|
+
});
|
|
534
|
+
|
|
535
|
+
it('should return correct size for documents', () => {
|
|
536
|
+
const content1 = 'Hello World';
|
|
537
|
+
const content2 = 'Another document';
|
|
538
|
+
const hash1 = computeHash(content1);
|
|
539
|
+
const hash2 = computeHash(content2);
|
|
540
|
+
|
|
541
|
+
store.insertContent(hash1, content1);
|
|
542
|
+
store.insertContent(hash2, content2);
|
|
543
|
+
|
|
544
|
+
store.insertDocument({
|
|
545
|
+
collection: 'codebase',
|
|
546
|
+
path: '/test/file1.ts',
|
|
547
|
+
title: 'file1.ts',
|
|
548
|
+
hash: hash1,
|
|
549
|
+
createdAt: new Date().toISOString(),
|
|
550
|
+
modifiedAt: new Date().toISOString(),
|
|
551
|
+
active: true,
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
store.insertDocument({
|
|
555
|
+
collection: 'codebase',
|
|
556
|
+
path: '/test/file2.ts',
|
|
557
|
+
title: 'file2.ts',
|
|
558
|
+
hash: hash2,
|
|
559
|
+
createdAt: new Date().toISOString(),
|
|
560
|
+
modifiedAt: new Date().toISOString(),
|
|
561
|
+
active: true,
|
|
562
|
+
});
|
|
563
|
+
|
|
564
|
+
const size = store.getCollectionStorageSize('codebase');
|
|
565
|
+
expect(size).toBe(content1.length + content2.length);
|
|
566
|
+
});
|
|
567
|
+
|
|
568
|
+
it('should only count active documents', () => {
|
|
569
|
+
const content = 'Test content';
|
|
570
|
+
const hash = computeHash(content);
|
|
571
|
+
|
|
572
|
+
store.insertContent(hash, content);
|
|
573
|
+
store.insertDocument({
|
|
574
|
+
collection: 'codebase',
|
|
575
|
+
path: '/test/file.ts',
|
|
576
|
+
title: 'file.ts',
|
|
577
|
+
hash,
|
|
578
|
+
createdAt: new Date().toISOString(),
|
|
579
|
+
modifiedAt: new Date().toISOString(),
|
|
580
|
+
active: true,
|
|
581
|
+
});
|
|
582
|
+
|
|
583
|
+
const sizeBefore = store.getCollectionStorageSize('codebase');
|
|
584
|
+
expect(sizeBefore).toBe(content.length);
|
|
585
|
+
|
|
586
|
+
store.deactivateDocument('codebase', '/test/file.ts');
|
|
587
|
+
|
|
588
|
+
const sizeAfter = store.getCollectionStorageSize('codebase');
|
|
589
|
+
expect(sizeAfter).toBe(0);
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
it('should only count specified collection', () => {
|
|
593
|
+
const content = 'Test content';
|
|
594
|
+
const hash = computeHash(content);
|
|
595
|
+
|
|
596
|
+
store.insertContent(hash, content);
|
|
597
|
+
store.insertDocument({
|
|
598
|
+
collection: 'other-collection',
|
|
599
|
+
path: '/test/file.ts',
|
|
600
|
+
title: 'file.ts',
|
|
601
|
+
hash,
|
|
602
|
+
createdAt: new Date().toISOString(),
|
|
603
|
+
modifiedAt: new Date().toISOString(),
|
|
604
|
+
active: true,
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
const codebaseSize = store.getCollectionStorageSize('codebase');
|
|
608
|
+
const otherSize = store.getCollectionStorageSize('other-collection');
|
|
609
|
+
|
|
610
|
+
expect(codebaseSize).toBe(0);
|
|
611
|
+
expect(otherSize).toBe(content.length);
|
|
612
|
+
});
|
|
613
|
+
});
|
|
614
|
+
|
|
615
|
+
describe('getCodebaseStats', () => {
|
|
616
|
+
let tmpDir: string;
|
|
617
|
+
let dbPath: string;
|
|
618
|
+
let store: Store;
|
|
619
|
+
|
|
620
|
+
beforeEach(() => {
|
|
621
|
+
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'nano-brain-stats-test-'));
|
|
622
|
+
dbPath = path.join(tmpDir, 'test.db');
|
|
623
|
+
store = createStore(dbPath);
|
|
624
|
+
});
|
|
625
|
+
|
|
626
|
+
afterEach(() => {
|
|
627
|
+
store.close();
|
|
628
|
+
if (fs.existsSync(tmpDir)) {
|
|
629
|
+
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
630
|
+
}
|
|
631
|
+
});
|
|
632
|
+
|
|
633
|
+
it('should return undefined when codebase not enabled', () => {
|
|
634
|
+
const config: CodebaseConfig = { enabled: false };
|
|
635
|
+
const stats = getCodebaseStats(store, config, tmpDir);
|
|
636
|
+
expect(stats).toBeUndefined();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
it('should return undefined when config is undefined', () => {
|
|
640
|
+
const stats = getCodebaseStats(store, undefined, tmpDir);
|
|
641
|
+
expect(stats).toBeUndefined();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
it('should return stats when enabled', () => {
|
|
645
|
+
const config: CodebaseConfig = { enabled: true };
|
|
646
|
+
const stats = getCodebaseStats(store, config, tmpDir);
|
|
647
|
+
|
|
648
|
+
expect(stats).toBeDefined();
|
|
649
|
+
expect(stats?.enabled).toBe(true);
|
|
650
|
+
expect(stats?.documents).toBe(0);
|
|
651
|
+
expect(stats?.storageUsed).toBe(0);
|
|
652
|
+
expect(stats?.maxSize).toBe(2 * 1024 * 1024 * 1024);
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
it('should include resolved extensions', () => {
|
|
656
|
+
fs.writeFileSync(path.join(tmpDir, 'package.json'), '{}');
|
|
657
|
+
const config: CodebaseConfig = { enabled: true };
|
|
658
|
+
const stats = getCodebaseStats(store, config, tmpDir);
|
|
659
|
+
|
|
660
|
+
expect(stats?.extensions).toContain('.ts');
|
|
661
|
+
expect(stats?.extensions).toContain('.js');
|
|
662
|
+
});
|
|
663
|
+
|
|
664
|
+
it('should include exclude pattern count', () => {
|
|
665
|
+
fs.writeFileSync(path.join(tmpDir, '.gitignore'), 'custom1\ncustom2');
|
|
666
|
+
const config: CodebaseConfig = { enabled: true, exclude: ['extra'] };
|
|
667
|
+
const stats = getCodebaseStats(store, config, tmpDir);
|
|
668
|
+
|
|
669
|
+
expect(stats?.excludeCount).toBeGreaterThan(0);
|
|
670
|
+
});
|
|
671
|
+
|
|
672
|
+
it('should use custom maxSize from config', () => {
|
|
673
|
+
const config: CodebaseConfig = { enabled: true, maxSize: '1GB' };
|
|
674
|
+
const stats = getCodebaseStats(store, config, tmpDir);
|
|
675
|
+
|
|
676
|
+
expect(stats?.maxSize).toBe(1024 * 1024 * 1024);
|
|
677
|
+
});
|
|
678
|
+
});
|