@softerist/heuristic-mcp 2.1.46 → 3.0.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/.agent/workflows/code-review.md +60 -0
- package/.prettierrc +7 -0
- package/ARCHITECTURE.md +105 -170
- package/CONTRIBUTING.md +32 -113
- package/GEMINI.md +73 -0
- package/LICENSE +21 -21
- package/README.md +161 -54
- package/config.json +876 -76
- package/debug-pids.js +27 -0
- package/eslint.config.js +36 -0
- package/features/ann-config.js +37 -26
- package/features/clear-cache.js +28 -19
- package/features/find-similar-code.js +142 -66
- package/features/hybrid-search.js +253 -93
- package/features/index-codebase.js +1455 -394
- package/features/lifecycle.js +813 -180
- package/features/register.js +58 -52
- package/index.js +450 -306
- package/lib/cache-ops.js +22 -0
- package/lib/cache-utils.js +68 -0
- package/lib/cache.js +1392 -587
- package/lib/call-graph.js +165 -50
- package/lib/cli.js +154 -0
- package/lib/config.js +462 -121
- package/lib/embedding-process.js +77 -0
- package/lib/embedding-worker.js +545 -30
- package/lib/ignore-patterns.js +61 -59
- package/lib/json-worker.js +14 -0
- package/lib/json-writer.js +344 -0
- package/lib/logging.js +88 -0
- package/lib/memory-logger.js +13 -0
- package/lib/project-detector.js +13 -17
- package/lib/server-lifecycle.js +38 -0
- package/lib/settings-editor.js +645 -0
- package/lib/tokenizer.js +207 -104
- package/lib/utils.js +273 -198
- package/lib/vector-store-binary.js +592 -0
- package/mcp_config.example.json +13 -0
- package/package.json +13 -2
- package/scripts/clear-cache.js +6 -17
- package/scripts/download-model.js +14 -9
- package/scripts/postinstall.js +5 -5
- package/search-configs.js +36 -0
- package/test/ann-config.test.js +179 -0
- package/test/ann-fallback.test.js +6 -6
- package/test/binary-store.test.js +69 -0
- package/test/cache-branches.test.js +120 -0
- package/test/cache-errors.test.js +264 -0
- package/test/cache-extra.test.js +300 -0
- package/test/cache-helpers.test.js +205 -0
- package/test/cache-hnsw-failure.test.js +40 -0
- package/test/cache-json-worker.test.js +190 -0
- package/test/cache-worker.test.js +102 -0
- package/test/cache.test.js +443 -0
- package/test/call-graph.test.js +103 -4
- package/test/clear-cache.test.js +69 -68
- package/test/code-review-workflow.test.js +50 -0
- package/test/config.test.js +418 -0
- package/test/coverage-gap.test.js +497 -0
- package/test/coverage-maximizer.test.js +236 -0
- package/test/debug-analysis.js +107 -0
- package/test/embedding-model.test.js +173 -103
- package/test/embedding-worker-extra.test.js +272 -0
- package/test/embedding-worker.test.js +158 -0
- package/test/features.test.js +139 -0
- package/test/final-boost.test.js +271 -0
- package/test/final-polish.test.js +183 -0
- package/test/final.test.js +95 -0
- package/test/find-similar-code.test.js +191 -0
- package/test/helpers.js +92 -11
- package/test/helpers.test.js +46 -0
- package/test/hybrid-search-basic.test.js +62 -0
- package/test/hybrid-search-branch.test.js +202 -0
- package/test/hybrid-search-callgraph.test.js +229 -0
- package/test/hybrid-search-extra.test.js +81 -0
- package/test/hybrid-search.test.js +484 -71
- package/test/index-cli.test.js +520 -0
- package/test/index-codebase-batch.test.js +119 -0
- package/test/index-codebase-branches.test.js +585 -0
- package/test/index-codebase-core.test.js +1032 -0
- package/test/index-codebase-edge-cases.test.js +254 -0
- package/test/index-codebase-errors.test.js +132 -0
- package/test/index-codebase-gap.test.js +239 -0
- package/test/index-codebase-lines.test.js +151 -0
- package/test/index-codebase-watcher.test.js +259 -0
- package/test/index-codebase-zone.test.js +259 -0
- package/test/index-codebase.test.js +371 -69
- package/test/index-memory.test.js +220 -0
- package/test/indexer-detailed.test.js +176 -0
- package/test/integration.test.js +148 -92
- package/test/json-worker.test.js +50 -0
- package/test/lifecycle.test.js +541 -0
- package/test/master.test.js +198 -0
- package/test/perfection.test.js +349 -0
- package/test/project-detector.test.js +65 -0
- package/test/register.test.js +262 -0
- package/test/tokenizer.test.js +55 -93
- package/test/ultra-maximizer.test.js +116 -0
- package/test/utils-branches.test.js +161 -0
- package/test/utils-extra.test.js +116 -0
- package/test/utils.test.js +131 -0
- package/test/verify_fixes.js +76 -0
- package/test/worker-errors.test.js +96 -0
- package/test/worker-init.test.js +102 -0
- package/test/worker_throttling.test.js +93 -0
- package/tools/scripts/benchmark-search.js +95 -0
- package/tools/scripts/cache-stats.js +71 -0
- package/tools/scripts/manual-search.js +34 -0
- package/vitest.config.js +19 -9
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for configuration loading and environment overrides
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { describe, it, expect, afterEach, vi } from 'vitest';
|
|
6
|
+
import fs from 'fs/promises';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import os from 'os';
|
|
9
|
+
import { loadConfig, getGlobalCacheDir, getConfig, DEFAULT_CONFIG } from '../lib/config.js';
|
|
10
|
+
|
|
11
|
+
const ORIGINAL_ENV = { ...process.env };
|
|
12
|
+
|
|
13
|
+
function resetEnv() {
|
|
14
|
+
for (const key of Object.keys(process.env)) {
|
|
15
|
+
if (!(key in ORIGINAL_ENV)) {
|
|
16
|
+
delete process.env[key];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
Object.assign(process.env, ORIGINAL_ENV);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function withTempDir(testFn) {
|
|
23
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'heuristic-config-'));
|
|
24
|
+
try {
|
|
25
|
+
await testFn(dir);
|
|
26
|
+
} finally {
|
|
27
|
+
await fs.rm(dir, { recursive: true, force: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
afterEach(() => {
|
|
32
|
+
resetEnv();
|
|
33
|
+
vi.restoreAllMocks(); // Restore mocks after each test
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
describe('Configuration Loading', () => {
|
|
37
|
+
it('loads workspace config and resolves relative cache directory', async () => {
|
|
38
|
+
await withTempDir(async (dir) => {
|
|
39
|
+
const configData = {
|
|
40
|
+
cacheDirectory: 'cache',
|
|
41
|
+
excludePatterns: ['**/custom/**'],
|
|
42
|
+
smartIndexing: false,
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify(configData));
|
|
46
|
+
|
|
47
|
+
const config = await loadConfig(dir);
|
|
48
|
+
expect(config.searchDirectory).toBe(path.resolve(dir));
|
|
49
|
+
expect(config.cacheDirectory).toBe(path.join(path.resolve(dir), 'cache'));
|
|
50
|
+
expect(config.excludePatterns).toEqual(configData.excludePatterns);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('loads workspace config with absolute cache directory', async () => {
|
|
55
|
+
await withTempDir(async (dir) => {
|
|
56
|
+
const absoluteCache = path.join(dir, 'abs-cache');
|
|
57
|
+
const configData = {
|
|
58
|
+
cacheDirectory: absoluteCache,
|
|
59
|
+
smartIndexing: false,
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify(configData));
|
|
63
|
+
|
|
64
|
+
const config = await loadConfig(dir);
|
|
65
|
+
expect(config.cacheDirectory).toBe(absoluteCache);
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
it('loads default config when file missing', async () => {
|
|
70
|
+
await withTempDir(async (dir) => {
|
|
71
|
+
const config = await loadConfig(dir);
|
|
72
|
+
expect(config.embeddingModel).toBe(DEFAULT_CONFIG.embeddingModel);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('loads local config in server mode when server config is missing', async () => {
|
|
77
|
+
await withTempDir(async (dir) => {
|
|
78
|
+
const originalCwd = process.cwd();
|
|
79
|
+
process.chdir(dir);
|
|
80
|
+
|
|
81
|
+
const repoConfig = path.resolve(originalCwd, 'config.json');
|
|
82
|
+
const repoBackup = path.resolve(originalCwd, 'config.json.bak');
|
|
83
|
+
await fs.rename(repoConfig, repoBackup);
|
|
84
|
+
await fs.writeFile(
|
|
85
|
+
path.join(dir, 'config.json'),
|
|
86
|
+
JSON.stringify({ smartIndexing: false, maxResults: 7 })
|
|
87
|
+
);
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const config = await loadConfig();
|
|
91
|
+
expect(config.searchDirectory).toBe(path.resolve(dir));
|
|
92
|
+
expect(config.maxResults).toBe(7);
|
|
93
|
+
} finally {
|
|
94
|
+
await fs.rename(repoBackup, repoConfig);
|
|
95
|
+
process.chdir(originalCwd);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('applies environment overrides with validation and locks ANN metric', async () => {
|
|
101
|
+
await withTempDir(async (dir) => {
|
|
102
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
103
|
+
|
|
104
|
+
process.env.SMART_CODING_MAX_RESULTS = '0';
|
|
105
|
+
process.env.SMART_CODING_EXACT_MATCH_BOOST = '2';
|
|
106
|
+
process.env.SMART_CODING_EMBEDDING_MODEL = 'custom-model';
|
|
107
|
+
process.env.SMART_CODING_ANN_METRIC = 'ip';
|
|
108
|
+
process.env.SMART_CODING_ANN_M = '128';
|
|
109
|
+
process.env.SMART_CODING_WORKER_THREADS = 'auto';
|
|
110
|
+
|
|
111
|
+
const config = await loadConfig(dir);
|
|
112
|
+
|
|
113
|
+
expect(config.maxResults).toBe(DEFAULT_CONFIG.maxResults);
|
|
114
|
+
expect(config.exactMatchBoost).toBe(2);
|
|
115
|
+
expect(config.embeddingModel).toBe('custom-model');
|
|
116
|
+
expect(config.annMetric).toBe('cosine');
|
|
117
|
+
expect(config.annM).toBe(DEFAULT_CONFIG.annM);
|
|
118
|
+
expect(config.workerThreads).toBe('auto');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('applies valid environment overrides', async () => {
|
|
123
|
+
await withTempDir(async (dir) => {
|
|
124
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
125
|
+
|
|
126
|
+
process.env.SMART_CODING_VERBOSE = 'true';
|
|
127
|
+
process.env.SMART_CODING_BATCH_SIZE = '200';
|
|
128
|
+
process.env.SMART_CODING_MAX_FILE_SIZE = '2048';
|
|
129
|
+
process.env.SMART_CODING_CHUNK_SIZE = '42';
|
|
130
|
+
process.env.SMART_CODING_MAX_RESULTS = '9';
|
|
131
|
+
process.env.SMART_CODING_SMART_INDEXING = 'true';
|
|
132
|
+
process.env.SMART_CODING_RECENCY_BOOST = '0.5';
|
|
133
|
+
process.env.SMART_CODING_RECENCY_DECAY_DAYS = '10';
|
|
134
|
+
process.env.SMART_CODING_WATCH_FILES = 'false';
|
|
135
|
+
process.env.SMART_CODING_SEMANTIC_WEIGHT = '0.3';
|
|
136
|
+
process.env.SMART_CODING_EXACT_MATCH_BOOST = '2';
|
|
137
|
+
process.env.SMART_CODING_EMBEDDING_MODEL = 'custom-embedder';
|
|
138
|
+
process.env.SMART_CODING_PRELOAD_EMBEDDING_MODEL = 'false';
|
|
139
|
+
process.env.SMART_CODING_VECTOR_STORE_FORMAT = 'binary';
|
|
140
|
+
process.env.SMART_CODING_VECTOR_STORE_CONTENT_MODE = 'external';
|
|
141
|
+
process.env.SMART_CODING_VECTOR_STORE_LOAD_MODE = 'disk';
|
|
142
|
+
process.env.SMART_CODING_CONTENT_CACHE_ENTRIES = '512';
|
|
143
|
+
process.env.SMART_CODING_VECTOR_CACHE_ENTRIES = '64';
|
|
144
|
+
process.env.SMART_CODING_CLEAR_CACHE_AFTER_INDEX = 'true';
|
|
145
|
+
process.env.SMART_CODING_WORKER_THREADS = '3';
|
|
146
|
+
process.env.SMART_CODING_ANN_ENABLED = 'false';
|
|
147
|
+
process.env.SMART_CODING_ANN_MIN_CHUNKS = '123';
|
|
148
|
+
process.env.SMART_CODING_ANN_MIN_CANDIDATES = '10';
|
|
149
|
+
process.env.SMART_CODING_ANN_MAX_CANDIDATES = '100';
|
|
150
|
+
process.env.SMART_CODING_ANN_CANDIDATE_MULTIPLIER = '4';
|
|
151
|
+
process.env.SMART_CODING_ANN_EF_CONSTRUCTION = '64';
|
|
152
|
+
process.env.SMART_CODING_ANN_EF_SEARCH = '32';
|
|
153
|
+
process.env.SMART_CODING_ANN_M = '32';
|
|
154
|
+
process.env.SMART_CODING_ANN_INDEX_CACHE = 'false';
|
|
155
|
+
process.env.SMART_CODING_ANN_METRIC = 'l2';
|
|
156
|
+
|
|
157
|
+
const config = await loadConfig(dir);
|
|
158
|
+
|
|
159
|
+
expect(config.verbose).toBe(true);
|
|
160
|
+
expect(config.batchSize).toBe(200);
|
|
161
|
+
expect(config.maxFileSize).toBe(2048);
|
|
162
|
+
expect(config.chunkSize).toBe(42);
|
|
163
|
+
expect(config.maxResults).toBe(9);
|
|
164
|
+
expect(config.smartIndexing).toBe(true);
|
|
165
|
+
expect(config.recencyBoost).toBe(0.5);
|
|
166
|
+
expect(config.recencyDecayDays).toBe(10);
|
|
167
|
+
expect(config.watchFiles).toBe(false);
|
|
168
|
+
expect(config.semanticWeight).toBe(0.3);
|
|
169
|
+
expect(config.exactMatchBoost).toBe(2);
|
|
170
|
+
expect(config.embeddingModel).toBe('custom-embedder');
|
|
171
|
+
expect(config.preloadEmbeddingModel).toBe(false);
|
|
172
|
+
expect(config.vectorStoreFormat).toBe('binary');
|
|
173
|
+
expect(config.vectorStoreContentMode).toBe('external');
|
|
174
|
+
expect(config.vectorStoreLoadMode).toBe('disk');
|
|
175
|
+
expect(config.contentCacheEntries).toBe(512);
|
|
176
|
+
expect(config.vectorCacheEntries).toBe(64);
|
|
177
|
+
expect(config.clearCacheAfterIndex).toBe(true);
|
|
178
|
+
expect(config.workerThreads).toBe(3);
|
|
179
|
+
expect(config.annEnabled).toBe(false);
|
|
180
|
+
expect(config.annMinChunks).toBe(123);
|
|
181
|
+
expect(config.annMinCandidates).toBe(10);
|
|
182
|
+
expect(config.annMaxCandidates).toBe(100);
|
|
183
|
+
expect(config.annCandidateMultiplier).toBe(4);
|
|
184
|
+
expect(config.annEfConstruction).toBe(64);
|
|
185
|
+
expect(config.annEfSearch).toBe(32);
|
|
186
|
+
expect(config.annM).toBe(32);
|
|
187
|
+
expect(config.annIndexCache).toBe(false);
|
|
188
|
+
expect(config.annMetric).toBe('cosine');
|
|
189
|
+
});
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('ignores invalid environment overrides', async () => {
|
|
193
|
+
await withTempDir(async (dir) => {
|
|
194
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
195
|
+
|
|
196
|
+
process.env.SMART_CODING_BATCH_SIZE = '-1';
|
|
197
|
+
process.env.SMART_CODING_MAX_FILE_SIZE = '0';
|
|
198
|
+
process.env.SMART_CODING_CHUNK_SIZE = '1000';
|
|
199
|
+
process.env.SMART_CODING_RECENCY_BOOST = '2';
|
|
200
|
+
process.env.SMART_CODING_RECENCY_DECAY_DAYS = '400';
|
|
201
|
+
process.env.SMART_CODING_SEMANTIC_WEIGHT = '-1';
|
|
202
|
+
process.env.SMART_CODING_EXACT_MATCH_BOOST = 'nope';
|
|
203
|
+
process.env.SMART_CODING_WORKER_THREADS = '0';
|
|
204
|
+
process.env.SMART_CODING_ANN_MIN_CHUNKS = '-5';
|
|
205
|
+
process.env.SMART_CODING_ANN_MIN_CANDIDATES = '-1';
|
|
206
|
+
process.env.SMART_CODING_ANN_MAX_CANDIDATES = '0';
|
|
207
|
+
process.env.SMART_CODING_ANN_CANDIDATE_MULTIPLIER = '0';
|
|
208
|
+
process.env.SMART_CODING_ANN_EF_CONSTRUCTION = '0';
|
|
209
|
+
process.env.SMART_CODING_ANN_EF_SEARCH = '0';
|
|
210
|
+
process.env.SMART_CODING_ANN_M = '128';
|
|
211
|
+
process.env.SMART_CODING_ANN_METRIC = 'invalid';
|
|
212
|
+
|
|
213
|
+
const config = await loadConfig(dir);
|
|
214
|
+
|
|
215
|
+
expect(config.batchSize).toBe(DEFAULT_CONFIG.batchSize);
|
|
216
|
+
expect(config.maxFileSize).toBe(DEFAULT_CONFIG.maxFileSize);
|
|
217
|
+
expect(config.chunkSize).toBe(DEFAULT_CONFIG.chunkSize);
|
|
218
|
+
expect(config.recencyBoost).toBe(DEFAULT_CONFIG.recencyBoost);
|
|
219
|
+
expect(config.recencyDecayDays).toBe(DEFAULT_CONFIG.recencyDecayDays);
|
|
220
|
+
expect(config.semanticWeight).toBe(DEFAULT_CONFIG.semanticWeight);
|
|
221
|
+
expect(config.exactMatchBoost).toBe(DEFAULT_CONFIG.exactMatchBoost);
|
|
222
|
+
expect(config.workerThreads).toBe(DEFAULT_CONFIG.workerThreads);
|
|
223
|
+
expect(config.annMinChunks).toBe(DEFAULT_CONFIG.annMinChunks);
|
|
224
|
+
expect(config.annMinCandidates).toBe(DEFAULT_CONFIG.annMinCandidates);
|
|
225
|
+
expect(config.annMaxCandidates).toBe(DEFAULT_CONFIG.annMaxCandidates);
|
|
226
|
+
expect(config.annCandidateMultiplier).toBe(DEFAULT_CONFIG.annCandidateMultiplier);
|
|
227
|
+
expect(config.annEfConstruction).toBe(DEFAULT_CONFIG.annEfConstruction);
|
|
228
|
+
expect(config.annEfSearch).toBe(DEFAULT_CONFIG.annEfSearch);
|
|
229
|
+
expect(config.annM).toBe(DEFAULT_CONFIG.annM);
|
|
230
|
+
expect(config.annMetric).toBe('cosine');
|
|
231
|
+
});
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it('ignores invalid boolean environment overrides and empty strings', async () => {
|
|
235
|
+
await withTempDir(async (dir) => {
|
|
236
|
+
// Set values opposite to defaults in config.json to ensure env var doesn't revert them or mess them up
|
|
237
|
+
await fs.writeFile(
|
|
238
|
+
path.join(dir, 'config.json'),
|
|
239
|
+
JSON.stringify({
|
|
240
|
+
smartIndexing: false,
|
|
241
|
+
watchFiles: false,
|
|
242
|
+
verbose: true,
|
|
243
|
+
annEnabled: false,
|
|
244
|
+
annIndexCache: false,
|
|
245
|
+
})
|
|
246
|
+
);
|
|
247
|
+
|
|
248
|
+
process.env.SMART_CODING_VERBOSE = 'invalid';
|
|
249
|
+
process.env.SMART_CODING_SMART_INDEXING = 'maybe';
|
|
250
|
+
process.env.SMART_CODING_WATCH_FILES = 'sometimes';
|
|
251
|
+
process.env.SMART_CODING_EMBEDDING_MODEL = ' ';
|
|
252
|
+
process.env.SMART_CODING_ANN_ENABLED = 'nope';
|
|
253
|
+
process.env.SMART_CODING_ANN_INDEX_CACHE = 'idk';
|
|
254
|
+
|
|
255
|
+
const config = await loadConfig(dir);
|
|
256
|
+
|
|
257
|
+
expect(config.verbose).toBe(true); // Should stay as configured in file
|
|
258
|
+
expect(config.smartIndexing).toBe(false);
|
|
259
|
+
expect(config.watchFiles).toBe(false);
|
|
260
|
+
expect(config.embeddingModel).toBe(DEFAULT_CONFIG.embeddingModel);
|
|
261
|
+
expect(config.annEnabled).toBe(false);
|
|
262
|
+
expect(config.annIndexCache).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('should configure smart indexing when project detected', async () => {
|
|
267
|
+
await withTempDir(async (dir) => {
|
|
268
|
+
await fs.writeFile(path.join(dir, 'package.json'), JSON.stringify({ name: 'test' }));
|
|
269
|
+
const config = await loadConfig(dir);
|
|
270
|
+
expect(config.excludePatterns.length).toBeGreaterThan(0);
|
|
271
|
+
});
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('logs when no project markers are detected', async () => {
|
|
275
|
+
await withTempDir(async (dir) => {
|
|
276
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: true }));
|
|
277
|
+
await loadConfig(dir);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
it('should respect legacy .smart-coding-cache directory', async () => {
|
|
282
|
+
await withTempDir(async (dir) => {
|
|
283
|
+
const legacyDir = path.join(dir, '.smart-coding-cache');
|
|
284
|
+
await fs.mkdir(legacyDir, { recursive: true });
|
|
285
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
286
|
+
|
|
287
|
+
const config = await loadConfig(dir);
|
|
288
|
+
expect(config.cacheDirectory).toContain('.smart-coding-cache');
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
|
|
292
|
+
it('logs legacy cache usage when verbose is enabled', async () => {
|
|
293
|
+
await withTempDir(async (dir) => {
|
|
294
|
+
const legacyDir = path.join(dir, '.smart-coding-cache');
|
|
295
|
+
await fs.mkdir(legacyDir, { recursive: true });
|
|
296
|
+
await fs.writeFile(
|
|
297
|
+
path.join(dir, 'config.json'),
|
|
298
|
+
JSON.stringify({ smartIndexing: false, verbose: true })
|
|
299
|
+
);
|
|
300
|
+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
|
301
|
+
|
|
302
|
+
await loadConfig(dir);
|
|
303
|
+
|
|
304
|
+
const called = errorSpy.mock.calls.some(
|
|
305
|
+
(call) => typeof call[0] === 'string' && call[0].includes('Using existing local cache')
|
|
306
|
+
);
|
|
307
|
+
expect(called).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('ignores legacy cache path when it is not a directory', async () => {
|
|
312
|
+
await withTempDir(async (dir) => {
|
|
313
|
+
const legacyPath = path.join(dir, '.smart-coding-cache');
|
|
314
|
+
await fs.writeFile(legacyPath, 'not-a-dir');
|
|
315
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
316
|
+
|
|
317
|
+
const config = await loadConfig(dir);
|
|
318
|
+
expect(config.cacheDirectory).not.toContain('.smart-coding-cache');
|
|
319
|
+
});
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('uses default configuration when detection fails unexpectedly', async () => {
|
|
323
|
+
const { ProjectDetector } = await import('../lib/project-detector.js');
|
|
324
|
+
const detectSpy = vi
|
|
325
|
+
.spyOn(ProjectDetector.prototype, 'detectProjectTypes')
|
|
326
|
+
.mockRejectedValueOnce(new Error('boom'));
|
|
327
|
+
|
|
328
|
+
const config = await loadConfig();
|
|
329
|
+
expect(config).toBeDefined();
|
|
330
|
+
detectSpy.mockRestore();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('exposes loaded config via getConfig', async () => {
|
|
334
|
+
await withTempDir(async (dir) => {
|
|
335
|
+
await fs.writeFile(path.join(dir, 'config.json'), JSON.stringify({ smartIndexing: false }));
|
|
336
|
+
|
|
337
|
+
const loaded = await loadConfig(dir);
|
|
338
|
+
const current = getConfig();
|
|
339
|
+
|
|
340
|
+
expect(current).toBe(loaded);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
describe('Global Cache Directory', () => {
|
|
346
|
+
it('uses LOCALAPPDATA on Windows when set', () => {
|
|
347
|
+
if (process.platform !== 'win32') return;
|
|
348
|
+
|
|
349
|
+
const originalLocalAppData = process.env.LOCALAPPDATA;
|
|
350
|
+
process.env.LOCALAPPDATA = 'C:\\Temp\\LocalAppData';
|
|
351
|
+
|
|
352
|
+
expect(getGlobalCacheDir()).toBe('C:\\Temp\\LocalAppData');
|
|
353
|
+
|
|
354
|
+
if (originalLocalAppData === undefined) {
|
|
355
|
+
delete process.env.LOCALAPPDATA;
|
|
356
|
+
} else {
|
|
357
|
+
process.env.LOCALAPPDATA = originalLocalAppData;
|
|
358
|
+
}
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('falls back to default Windows cache path when LOCALAPPDATA is unset', () => {
|
|
362
|
+
if (process.platform !== 'win32') return;
|
|
363
|
+
|
|
364
|
+
const originalLocalAppData = process.env.LOCALAPPDATA;
|
|
365
|
+
delete process.env.LOCALAPPDATA;
|
|
366
|
+
|
|
367
|
+
expect(getGlobalCacheDir()).toBe(path.join(os.homedir(), 'AppData', 'Local'));
|
|
368
|
+
|
|
369
|
+
if (originalLocalAppData !== undefined) {
|
|
370
|
+
process.env.LOCALAPPDATA = originalLocalAppData;
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
|
|
374
|
+
it('uses macOS cache path on darwin', () => {
|
|
375
|
+
const originalPlatform = process.platform;
|
|
376
|
+
Object.defineProperty(process, 'platform', { value: 'darwin' });
|
|
377
|
+
|
|
378
|
+
const result = getGlobalCacheDir();
|
|
379
|
+
expect(result).toContain(path.join(os.homedir(), 'Library', 'Caches'));
|
|
380
|
+
|
|
381
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('uses XDG cache path on linux', () => {
|
|
385
|
+
const originalPlatform = process.platform;
|
|
386
|
+
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
387
|
+
|
|
388
|
+
const originalXdg = process.env.XDG_CACHE_HOME;
|
|
389
|
+
process.env.XDG_CACHE_HOME = '/tmp/xdg-cache';
|
|
390
|
+
|
|
391
|
+
const result = getGlobalCacheDir();
|
|
392
|
+
expect(result).toBe('/tmp/xdg-cache');
|
|
393
|
+
|
|
394
|
+
if (originalXdg === undefined) {
|
|
395
|
+
delete process.env.XDG_CACHE_HOME;
|
|
396
|
+
} else {
|
|
397
|
+
process.env.XDG_CACHE_HOME = originalXdg;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it('falls back to default linux cache path when XDG cache is unset', () => {
|
|
404
|
+
const originalPlatform = process.platform;
|
|
405
|
+
Object.defineProperty(process, 'platform', { value: 'linux' });
|
|
406
|
+
|
|
407
|
+
const originalXdg = process.env.XDG_CACHE_HOME;
|
|
408
|
+
delete process.env.XDG_CACHE_HOME;
|
|
409
|
+
|
|
410
|
+
const result = getGlobalCacheDir();
|
|
411
|
+
expect(result).toBe(path.join(os.homedir(), '.cache'));
|
|
412
|
+
|
|
413
|
+
if (originalXdg !== undefined) {
|
|
414
|
+
process.env.XDG_CACHE_HOME = originalXdg;
|
|
415
|
+
}
|
|
416
|
+
Object.defineProperty(process, 'platform', { value: originalPlatform });
|
|
417
|
+
});
|
|
418
|
+
});
|