@sdsrs/code-graph 0.93.1 → 0.95.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/claude-plugin/.claude-plugin/plugin.json +1 -1
- package/claude-plugin/scripts/auto-update.js +69 -9
- package/package.json +8 -7
- package/claude-plugin/scripts/adopt.test.js +0 -679
- package/claude-plugin/scripts/auto-update.test.js +0 -474
- package/claude-plugin/scripts/cg-answer.test.js +0 -309
- package/claude-plugin/scripts/claude-config.test.js +0 -58
- package/claude-plugin/scripts/covering-tests.test.js +0 -78
- package/claude-plugin/scripts/doctor.test.js +0 -215
- package/claude-plugin/scripts/find-binary.test.js +0 -246
- package/claude-plugin/scripts/hook-fire.test.js +0 -117
- package/claude-plugin/scripts/hooks.test.js +0 -230
- package/claude-plugin/scripts/incremental-index.test.js +0 -102
- package/claude-plugin/scripts/lifecycle.e2e.test.js +0 -179
- package/claude-plugin/scripts/lifecycle.test.js +0 -786
- package/claude-plugin/scripts/mcp-launcher.test.js +0 -162
- package/claude-plugin/scripts/mcp-stub.test.js +0 -207
- package/claude-plugin/scripts/post-grep-inject.test.js +0 -531
- package/claude-plugin/scripts/pr-impact-comment.test.js +0 -110
- package/claude-plugin/scripts/pre-edit-guide.test.js +0 -218
- package/claude-plugin/scripts/pre-grep-guide.test.js +0 -1682
- package/claude-plugin/scripts/pre-read-guide.test.js +0 -363
- package/claude-plugin/scripts/project-detect.test.js +0 -95
- package/claude-plugin/scripts/recommendation-log.test.js +0 -79
- package/claude-plugin/scripts/session-init.test.js +0 -479
- package/claude-plugin/scripts/statusline-composite.test.js +0 -65
- package/claude-plugin/scripts/statusline.test.js +0 -235
- package/claude-plugin/scripts/tmp-dir.test.js +0 -50
- package/claude-plugin/scripts/user-prompt-context.test.js +0 -743
- package/claude-plugin/scripts/version-utils.test.js +0 -141
|
@@ -1,309 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const fs = require('fs');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const path = require('path');
|
|
7
|
-
const { runGrepAnswer, runShowAnswer, runOverviewAnswer, runCallgraphAnswer, truncateAtLine } = require('./cg-answer');
|
|
8
|
-
|
|
9
|
-
// Stub "binary": a node script that reacts to its first real arg so one stub
|
|
10
|
-
// covers hits / no-hits / error / timeout cases.
|
|
11
|
-
let stubDir;
|
|
12
|
-
let stubPath;
|
|
13
|
-
|
|
14
|
-
test.before(() => {
|
|
15
|
-
stubDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-answer-test-'));
|
|
16
|
-
stubPath = path.join(stubDir, 'cg-stub.js');
|
|
17
|
-
fs.writeFileSync(stubPath, `#!/usr/bin/env node
|
|
18
|
-
'use strict';
|
|
19
|
-
const pattern = process.argv[3] || '';
|
|
20
|
-
if (pattern === 'HangForever') { setTimeout(() => {}, 60000); }
|
|
21
|
-
else if (pattern === 'ExplodePlease') { process.exit(3); }
|
|
22
|
-
else if (pattern === 'NothingHere') {
|
|
23
|
-
process.stdout.write('[code-graph] No matches for: NothingHere\\n');
|
|
24
|
-
} else if (pattern === 'NothingHereExit1') {
|
|
25
|
-
// v0.50 grep-parity binary: no match → empty stdout + exit 1
|
|
26
|
-
process.exit(1);
|
|
27
|
-
} else if (pattern === 'HasCallers') {
|
|
28
|
-
// callgraph with real edges → runCallgraphAnswer 'hits'
|
|
29
|
-
process.stdout.write(
|
|
30
|
-
'HasCallers (src/a.rs)\\n' +
|
|
31
|
-
' \\u2190 called by: alpha (src/b.rs)\\n' +
|
|
32
|
-
' \\u2192 calls: beta (src/c.rs)\\n');
|
|
33
|
-
} else if (pattern === 'LeafSymbol') {
|
|
34
|
-
// callgraph with a bare header, no edge lines → 'no-hits' (no marginal value)
|
|
35
|
-
process.stdout.write('LeafSymbol (src/a.rs)\\n');
|
|
36
|
-
} else {
|
|
37
|
-
process.stdout.write(
|
|
38
|
-
'src/storage/db.rs:42 fn ' + pattern + '() {\\n' +
|
|
39
|
-
' -> fn ' + pattern + ' (lines 42-60)\\n' +
|
|
40
|
-
'args=' + JSON.stringify(process.argv.slice(2)) + '\\n');
|
|
41
|
-
}
|
|
42
|
-
`);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
test.after(() => {
|
|
46
|
-
fs.rmSync(stubDir, { recursive: true, force: true });
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Wrap the stub so spawnSync can exec it directly: binary = node, leading arg
|
|
50
|
-
// trick is not possible (runGrepAnswer controls args), so expose via a shim
|
|
51
|
-
// shell-free approach: point binary at node and prepend the script through
|
|
52
|
-
// _CG_ANSWER_BINARY handling is binary-only. Instead make the stub itself
|
|
53
|
-
// executable with a node shebang and rely on exec.
|
|
54
|
-
function stubBinary() {
|
|
55
|
-
fs.chmodSync(stubPath, 0o755);
|
|
56
|
-
return stubPath;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
test('runGrepAnswer: hits → status hits with stdout text', () => {
|
|
60
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'fts5_search', binary: stubBinary() });
|
|
61
|
-
assert.equal(r.status, 'hits');
|
|
62
|
-
assert.match(r.text, /fn fts5_search/);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
test('runGrepAnswer: passes grep subcommand, pattern and path as argv', () => {
|
|
66
|
-
const r = runGrepAnswer({
|
|
67
|
-
cwd: stubDir, pattern: 'fts5_search', searchPath: 'src/storage/', binary: stubBinary(),
|
|
68
|
-
});
|
|
69
|
-
assert.equal(r.status, 'hits');
|
|
70
|
-
assert.match(r.text, /args=\["grep","fts5_search","src\/storage\/"\]/);
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
test('runGrepAnswer: child env carries CODE_GRAPH_INTERNAL=1 (not a funnel conversion)', () => {
|
|
74
|
-
// Stub variant that echoes the marker back in its output.
|
|
75
|
-
const envStub = path.join(stubDir, 'cg-env-stub.js');
|
|
76
|
-
fs.writeFileSync(envStub, `#!/usr/bin/env node
|
|
77
|
-
process.stdout.write('internal=' + (process.env.CODE_GRAPH_INTERNAL || '') + '\\n');
|
|
78
|
-
`);
|
|
79
|
-
fs.chmodSync(envStub, 0o755);
|
|
80
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'whatever', binary: envStub });
|
|
81
|
-
assert.equal(r.status, 'hits');
|
|
82
|
-
assert.match(r.text, /internal=1/,
|
|
83
|
-
'hook-internal CLI runs must be marked so record_cli_use skips them');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
test('runGrepAnswer: omits path argv when no searchPath', () => {
|
|
87
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'fts5_search', binary: stubBinary() });
|
|
88
|
-
assert.match(r.text, /args=\["grep","fts5_search"\]/);
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
test('runGrepAnswer: CLI "[code-graph] No matches" → status no-hits', () => {
|
|
92
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'NothingHere', binary: stubBinary() });
|
|
93
|
-
assert.equal(r.status, 'no-hits');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
test('runGrepAnswer: exit 1 (v0.50 grep-parity no-match) → status no-hits', () => {
|
|
97
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'NothingHereExit1', binary: stubBinary() });
|
|
98
|
-
assert.equal(r.status, 'no-hits',
|
|
99
|
-
'grep-parity exit 1 means no match, not a failed binary');
|
|
100
|
-
});
|
|
101
|
-
|
|
102
|
-
test('runGrepAnswer: exit >1 → unavailable', () => {
|
|
103
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'ExplodePlease', binary: stubBinary() });
|
|
104
|
-
assert.equal(r.status, 'unavailable');
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('runGrepAnswer: missing binary → no-binary (distinct from runtime unavailable)', () => {
|
|
108
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'fts5_search', binary: null });
|
|
109
|
-
assert.equal(r.status, 'no-binary',
|
|
110
|
-
'a null binary is the flagship-dark case and must be distinguishable from a runtime failure');
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
test('runGrepAnswer: nonexistent binary path → unavailable (spawn failure, not no-binary)', () => {
|
|
114
|
-
// A non-null path that fails to spawn is a runtime failure, NOT a missing
|
|
115
|
-
// binary — `no-binary` is reserved for findBinary() returning falsy.
|
|
116
|
-
const r = runGrepAnswer({
|
|
117
|
-
cwd: stubDir, pattern: 'fts5_search', binary: path.join(stubDir, 'nope-bin'),
|
|
118
|
-
});
|
|
119
|
-
assert.equal(r.status, 'unavailable');
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
test('runGrepAnswer: timeout → unavailable', () => {
|
|
123
|
-
const r = runGrepAnswer({
|
|
124
|
-
cwd: stubDir, pattern: 'HangForever', binary: stubBinary(), timeoutMs: 300,
|
|
125
|
-
});
|
|
126
|
-
assert.equal(r.status, 'unavailable');
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
test('runGrepAnswer: empty pattern → unavailable (never spawns)', () => {
|
|
130
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: '', binary: stubBinary() });
|
|
131
|
-
assert.equal(r.status, 'unavailable');
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
test('runGrepAnswer: oversized pattern (>200ch) → unavailable (never spawns)', () => {
|
|
135
|
-
const r = runGrepAnswer({ cwd: stubDir, pattern: 'A'.repeat(201), binary: stubBinary() });
|
|
136
|
-
assert.equal(r.status, 'unavailable');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
test('runGrepAnswer: long output is truncated with marker', () => {
|
|
140
|
-
// Stub echoes args= line; force truncation via tiny maxBytes
|
|
141
|
-
const r = runGrepAnswer({
|
|
142
|
-
cwd: stubDir, pattern: 'fts5_search', binary: stubBinary(), maxBytes: 30,
|
|
143
|
-
});
|
|
144
|
-
assert.equal(r.status, 'hits');
|
|
145
|
-
assert.equal(r.truncated, true);
|
|
146
|
-
assert.ok(Buffer.byteLength(r.text, 'utf8') <= 30);
|
|
147
|
-
});
|
|
148
|
-
|
|
149
|
-
// ── truncateAtLine (pure) ───────────────────────────────────────────
|
|
150
|
-
|
|
151
|
-
test('truncateAtLine: under limit → unchanged, not truncated', () => {
|
|
152
|
-
const { text, truncated } = truncateAtLine('a\nb\nc', 100);
|
|
153
|
-
assert.equal(text, 'a\nb\nc');
|
|
154
|
-
assert.equal(truncated, false);
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
test('truncateAtLine: cuts at a line boundary', () => {
|
|
158
|
-
const input = 'line-one\nline-two\nline-three\n';
|
|
159
|
-
const { text, truncated } = truncateAtLine(input, 20);
|
|
160
|
-
assert.equal(truncated, true);
|
|
161
|
-
// 20-byte budget fits 'line-one\nline-two' (17B); the half-cut 'li' is dropped
|
|
162
|
-
assert.equal(text, 'line-one\nline-two');
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
test('truncateAtLine: single oversized line → hard cut', () => {
|
|
166
|
-
const { text, truncated } = truncateAtLine('x'.repeat(50), 10);
|
|
167
|
-
assert.equal(truncated, true);
|
|
168
|
-
assert.equal(Buffer.byteLength(text, 'utf8'), 10);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// ── v0.48 sanitizeSearchPath: glob args reach rg literally (no shell) ──
|
|
172
|
-
|
|
173
|
-
test('sanitizeSearchPath: truncates at first glob segment (daagu denied command)', () => {
|
|
174
|
-
const { sanitizeSearchPath } = require('./cg-answer');
|
|
175
|
-
assert.equal(
|
|
176
|
-
sanitizeSearchPath('backend/app/services/llm_engine/*.py'),
|
|
177
|
-
'backend/app/services/llm_engine');
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
test('sanitizeSearchPath: clean path unchanged; leading glob drops scope; falsy → undefined', () => {
|
|
181
|
-
const { sanitizeSearchPath } = require('./cg-answer');
|
|
182
|
-
assert.equal(sanitizeSearchPath('src/storage/'), 'src/storage/');
|
|
183
|
-
assert.equal(sanitizeSearchPath('*.py'), undefined);
|
|
184
|
-
assert.equal(sanitizeSearchPath('src/**/x.rs'), 'src');
|
|
185
|
-
assert.equal(sanitizeSearchPath('src/file[1].rs'), 'src');
|
|
186
|
-
assert.equal(sanitizeSearchPath(''), undefined);
|
|
187
|
-
assert.equal(sanitizeSearchPath(undefined), undefined);
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
test('runGrepAnswer: glob searchPath is truncated before spawn (defensive layer)', () => {
|
|
191
|
-
const r = runGrepAnswer({
|
|
192
|
-
cwd: stubDir, pattern: 'fts5_search', searchPath: 'src/storage/*.rs', binary: stubBinary(),
|
|
193
|
-
});
|
|
194
|
-
assert.equal(r.status, 'hits');
|
|
195
|
-
assert.match(r.text, /args=\["grep","fts5_search","src\/storage"\]/);
|
|
196
|
-
});
|
|
197
|
-
|
|
198
|
-
// ── runShowAnswer (v0.49) — show-mode deny bodies ────────────────────
|
|
199
|
-
|
|
200
|
-
test('runShowAnswer: concatenates per-symbol show output with $ headers', () => {
|
|
201
|
-
const r = runShowAnswer({ cwd: stubDir, symbols: ['alpha_one', 'beta_two'], binary: stubBinary() });
|
|
202
|
-
assert.equal(r.status, 'hits');
|
|
203
|
-
assert.match(r.text, /\$ code-graph-mcp show alpha_one/);
|
|
204
|
-
assert.match(r.text, /\$ code-graph-mcp show beta_two/);
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
test('runShowAnswer: skips non-identifier symbols, all-skipped → unavailable-safe no-hits', () => {
|
|
208
|
-
const r = runShowAnswer({ cwd: stubDir, symbols: ['$(rm -rf)', 'a|b'], binary: stubBinary() });
|
|
209
|
-
assert.equal(r.status, 'no-hits');
|
|
210
|
-
});
|
|
211
|
-
|
|
212
|
-
test('runShowAnswer: caps at 3 symbols', () => {
|
|
213
|
-
const r = runShowAnswer({
|
|
214
|
-
cwd: stubDir, symbols: ['s_one', 's_two', 's_three', 's_four'], binary: stubBinary(),
|
|
215
|
-
});
|
|
216
|
-
assert.equal(r.status, 'hits');
|
|
217
|
-
assert.doesNotMatch(r.text, /show s_four/);
|
|
218
|
-
});
|
|
219
|
-
|
|
220
|
-
test('runShowAnswer: empty symbol list → unavailable', () => {
|
|
221
|
-
assert.equal(runShowAnswer({ cwd: stubDir, symbols: [], binary: stubBinary() }).status, 'unavailable');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
test('runShowAnswer: failing binary → no-hits (caller falls back to grep answer)', () => {
|
|
225
|
-
const r = runShowAnswer({ cwd: stubDir, symbols: ['ExplodePlease'], binary: stubBinary() });
|
|
226
|
-
assert.equal(r.status, 'no-hits');
|
|
227
|
-
});
|
|
228
|
-
|
|
229
|
-
test('runShowAnswer: missing binary → no-binary (distinct from runtime no-hits/unavailable)', () => {
|
|
230
|
-
const r = runShowAnswer({ cwd: stubDir, symbols: ['alpha_one'], binary: null });
|
|
231
|
-
assert.equal(r.status, 'no-binary');
|
|
232
|
-
});
|
|
233
|
-
|
|
234
|
-
// ── runOverviewAnswer (v0.49) — read-fanout delivered module map ──────
|
|
235
|
-
|
|
236
|
-
test('runOverviewAnswer: hits → status hits with stdout text', () => {
|
|
237
|
-
const r = runOverviewAnswer({ cwd: stubDir, dir: 'src/storage', binary: stubBinary() });
|
|
238
|
-
assert.equal(r.status, 'hits');
|
|
239
|
-
assert.match(r.text, /args=\["overview","src\/storage"\]/);
|
|
240
|
-
});
|
|
241
|
-
|
|
242
|
-
test('runOverviewAnswer: CLI "No matches" → no-hits', () => {
|
|
243
|
-
const r = runOverviewAnswer({ cwd: stubDir, dir: 'NothingHere', binary: stubBinary() });
|
|
244
|
-
assert.equal(r.status, 'no-hits');
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
test('runOverviewAnswer: failing binary → unavailable', () => {
|
|
248
|
-
const r = runOverviewAnswer({ cwd: stubDir, dir: 'ExplodePlease', binary: stubBinary() });
|
|
249
|
-
assert.equal(r.status, 'unavailable');
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('runOverviewAnswer: missing binary → no-binary (distinct from runtime unavailable)', () => {
|
|
253
|
-
const r = runOverviewAnswer({ cwd: stubDir, dir: 'src/storage', binary: null });
|
|
254
|
-
assert.equal(r.status, 'no-binary');
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
test('runOverviewAnswer: empty/oversized dir → unavailable (never spawns)', () => {
|
|
258
|
-
assert.equal(runOverviewAnswer({ cwd: stubDir, dir: '', binary: stubBinary() }).status, 'unavailable');
|
|
259
|
-
assert.equal(
|
|
260
|
-
runOverviewAnswer({ cwd: stubDir, dir: 'a'.repeat(301), binary: stubBinary() }).status,
|
|
261
|
-
'unavailable');
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
// ── runCallgraphAnswer (v0.75) — cross-file caller/callee tree ────────
|
|
265
|
-
|
|
266
|
-
test('runCallgraphAnswer: edge-bearing tree → hits with caller/callee lines', () => {
|
|
267
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: stubBinary() });
|
|
268
|
-
assert.equal(r.status, 'hits');
|
|
269
|
-
assert.match(r.text, /called by: alpha/);
|
|
270
|
-
assert.match(r.text, /calls: beta/);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
test('runCallgraphAnswer: passes callgraph subcommand + symbol as argv', () => {
|
|
274
|
-
// 'HasCallers' is the only stub branch that emits edges; assert it reached it.
|
|
275
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: stubBinary() });
|
|
276
|
-
assert.equal(r.status, 'hits');
|
|
277
|
-
assert.match(r.text, /← called by/);
|
|
278
|
-
});
|
|
279
|
-
|
|
280
|
-
test('runCallgraphAnswer: bare header with no edges → no-hits (no marginal value)', () => {
|
|
281
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'LeafSymbol', binary: stubBinary() });
|
|
282
|
-
assert.equal(r.status, 'no-hits');
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
test('runCallgraphAnswer: symbol not in graph (exit 1) → no-hits', () => {
|
|
286
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'NothingHereExit1', binary: stubBinary() });
|
|
287
|
-
assert.equal(r.status, 'no-hits');
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
test('runCallgraphAnswer: failing binary → unavailable', () => {
|
|
291
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'ExplodePlease', binary: stubBinary() });
|
|
292
|
-
assert.equal(r.status, 'unavailable');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
test('runCallgraphAnswer: missing binary → no-binary (distinct from runtime unavailable)', () => {
|
|
296
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HasCallers', binary: null });
|
|
297
|
-
assert.equal(r.status, 'no-binary');
|
|
298
|
-
});
|
|
299
|
-
|
|
300
|
-
test('runCallgraphAnswer: non-identifier symbol → unavailable (never spawns)', () => {
|
|
301
|
-
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: 'a|b', binary: stubBinary() }).status, 'unavailable');
|
|
302
|
-
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: '', binary: stubBinary() }).status, 'unavailable');
|
|
303
|
-
assert.equal(runCallgraphAnswer({ cwd: stubDir, symbol: 'def foo', binary: stubBinary() }).status, 'unavailable');
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test('runCallgraphAnswer: timeout → unavailable', () => {
|
|
307
|
-
const r = runCallgraphAnswer({ cwd: stubDir, symbol: 'HangForever', binary: stubBinary(), timeoutMs: 300 });
|
|
308
|
-
assert.equal(r.status, 'unavailable');
|
|
309
|
-
});
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const assert = require('node:assert');
|
|
4
|
-
const path = require('path');
|
|
5
|
-
const os = require('os');
|
|
6
|
-
const { claudeHome } = require('./claude-config');
|
|
7
|
-
|
|
8
|
-
test('claudeHome defaults to ~/.claude when CLAUDE_CONFIG_DIR unset', () => {
|
|
9
|
-
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
10
|
-
delete process.env.CLAUDE_CONFIG_DIR;
|
|
11
|
-
try {
|
|
12
|
-
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
|
|
13
|
-
} finally {
|
|
14
|
-
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
test('claudeHome honors CLAUDE_CONFIG_DIR when set', () => {
|
|
19
|
-
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
20
|
-
process.env.CLAUDE_CONFIG_DIR = '/tmp/work-claude';
|
|
21
|
-
try {
|
|
22
|
-
assert.strictEqual(claudeHome(), '/tmp/work-claude');
|
|
23
|
-
} finally {
|
|
24
|
-
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
25
|
-
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('claudeHome re-reads env on every call (not cached)', () => {
|
|
30
|
-
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
31
|
-
delete process.env.CLAUDE_CONFIG_DIR;
|
|
32
|
-
try {
|
|
33
|
-
const before = claudeHome();
|
|
34
|
-
process.env.CLAUDE_CONFIG_DIR = '/tmp/account-A';
|
|
35
|
-
const during = claudeHome();
|
|
36
|
-
delete process.env.CLAUDE_CONFIG_DIR;
|
|
37
|
-
const after = claudeHome();
|
|
38
|
-
assert.strictEqual(before, path.join(os.homedir(), '.claude'));
|
|
39
|
-
assert.strictEqual(during, '/tmp/account-A');
|
|
40
|
-
assert.strictEqual(after, path.join(os.homedir(), '.claude'));
|
|
41
|
-
} finally {
|
|
42
|
-
if (prev !== undefined) process.env.CLAUDE_CONFIG_DIR = prev;
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test('claudeHome ignores empty CLAUDE_CONFIG_DIR (falls back to ~/.claude)', () => {
|
|
47
|
-
// Empty string is falsy in JS — sanity-check the `||` fallback path so an
|
|
48
|
-
// accidentally `CLAUDE_CONFIG_DIR=` (unset-style) shell line does not strand
|
|
49
|
-
// us writing to the literal repository root `/`.
|
|
50
|
-
const prev = process.env.CLAUDE_CONFIG_DIR;
|
|
51
|
-
process.env.CLAUDE_CONFIG_DIR = '';
|
|
52
|
-
try {
|
|
53
|
-
assert.strictEqual(claudeHome(), path.join(os.homedir(), '.claude'));
|
|
54
|
-
} finally {
|
|
55
|
-
if (prev === undefined) delete process.env.CLAUDE_CONFIG_DIR;
|
|
56
|
-
else process.env.CLAUDE_CONFIG_DIR = prev;
|
|
57
|
-
}
|
|
58
|
-
});
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
const { formatCoveringTests, LIST_CAP } = require('./covering-tests');
|
|
5
|
-
|
|
6
|
-
// ── empty / robust ──────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
test('covering: empty list → no output', () => {
|
|
9
|
-
assert.equal(formatCoveringTests([], 'src/a.rs'), '');
|
|
10
|
-
});
|
|
11
|
-
|
|
12
|
-
test('covering: missing/undefined → no output (never throws)', () => {
|
|
13
|
-
assert.equal(formatCoveringTests(undefined, 'src/a.rs'), '');
|
|
14
|
-
assert.equal(formatCoveringTests(null, 'src/a.rs'), '');
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
test('covering: entries without a name are dropped', () => {
|
|
18
|
-
const out = formatCoveringTests(
|
|
19
|
-
[{ name: 'test_real', file: 'tests/a.rs' }, { file: 'tests/nameless.rs' }],
|
|
20
|
-
'src/a.rs'
|
|
21
|
-
);
|
|
22
|
-
assert.match(out, /Covering tests \(1\)/); // only the named one counts
|
|
23
|
-
assert.match(out, /test_real/);
|
|
24
|
-
});
|
|
25
|
-
|
|
26
|
-
// ── Rust: a real targeted command ───────────────────────
|
|
27
|
-
|
|
28
|
-
test('covering: Rust ≤cap lists names + a targeted `cargo test` command', () => {
|
|
29
|
-
const out = formatCoveringTests(
|
|
30
|
-
[
|
|
31
|
-
{ name: 'test_alpha', file: 'tests/a.rs' },
|
|
32
|
-
{ name: 'test_beta', file: 'src/b.rs' },
|
|
33
|
-
],
|
|
34
|
-
'src/foo.rs'
|
|
35
|
-
);
|
|
36
|
-
assert.match(out, /Covering tests \(2\)/);
|
|
37
|
-
assert.match(out, /test_alpha \(tests\/a\.rs\)/);
|
|
38
|
-
assert.match(out, /test_beta \(src\/b\.rs\)/);
|
|
39
|
-
// The actionable part: a command that runs exactly the covering tests.
|
|
40
|
-
assert.match(out, /Run after editing: cargo test test_alpha test_beta/);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// ── non-Rust: list only, never a fabricated command ─────
|
|
44
|
-
|
|
45
|
-
test('covering: non-Rust ≤cap lists names but emits NO command (no wrong command)', () => {
|
|
46
|
-
const out = formatCoveringTests(
|
|
47
|
-
[{ name: 'testValidate', file: 'src/auth.test.ts' }],
|
|
48
|
-
'src/auth.ts'
|
|
49
|
-
);
|
|
50
|
-
assert.match(out, /Covering tests \(1\): testValidate \(src\/auth\.test\.ts\)/);
|
|
51
|
-
assert.doesNotMatch(out, /cargo test/);
|
|
52
|
-
assert.doesNotMatch(out, /Run after editing/);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
// ── high fan-out: collapse, point at the suite ──────────
|
|
56
|
-
|
|
57
|
-
test('covering: Rust high fan-out (>cap) collapses to a count + suite command, no name list', () => {
|
|
58
|
-
const many = Array.from({ length: LIST_CAP + 1 }, (_, i) => ({
|
|
59
|
-
name: `test_${i}`,
|
|
60
|
-
file: 'tests/wide.rs',
|
|
61
|
-
}));
|
|
62
|
-
const out = formatCoveringTests(many, 'src/hot.rs');
|
|
63
|
-
assert.match(out, new RegExp(`Covering tests: ${LIST_CAP + 1}`));
|
|
64
|
-
assert.match(out, /widely-tested/);
|
|
65
|
-
assert.match(out, /Run the suite after editing: cargo test/);
|
|
66
|
-
// The long per-name list must NOT be inlined when fan-out is high.
|
|
67
|
-
assert.doesNotMatch(out, /test_0 \(/);
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
test('covering: non-Rust high fan-out collapses to a count with no command', () => {
|
|
71
|
-
const many = Array.from({ length: LIST_CAP + 3 }, (_, i) => ({
|
|
72
|
-
name: `test_${i}`,
|
|
73
|
-
file: 'a.test.ts',
|
|
74
|
-
}));
|
|
75
|
-
const out = formatCoveringTests(many, 'src/hot.ts');
|
|
76
|
-
assert.match(out, new RegExp(`Covering tests: ${LIST_CAP + 3}`));
|
|
77
|
-
assert.doesNotMatch(out, /cargo test/);
|
|
78
|
-
});
|
|
@@ -1,215 +0,0 @@
|
|
|
1
|
-
'use strict';
|
|
2
|
-
const test = require('node:test');
|
|
3
|
-
const assert = require('node:assert/strict');
|
|
4
|
-
|
|
5
|
-
const { runDiagnostics, formatReport, surveyHookCoverage } = require('./doctor');
|
|
6
|
-
const { buildSettingsHookEntries } = require('./lifecycle');
|
|
7
|
-
|
|
8
|
-
// Build a settings.json whose hooks exactly mirror what we'd register now.
|
|
9
|
-
function settingsWithCurrentHooks() {
|
|
10
|
-
const desired = buildSettingsHookEntries();
|
|
11
|
-
const hooks = {};
|
|
12
|
-
for (const [event, entries] of Object.entries(desired)) {
|
|
13
|
-
hooks[event] = entries.map(e => JSON.parse(JSON.stringify(e)));
|
|
14
|
-
}
|
|
15
|
-
return { hooks };
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
test('runDiagnostics returns an array of check results', () => {
|
|
19
|
-
const results = runDiagnostics();
|
|
20
|
-
assert.ok(Array.isArray(results));
|
|
21
|
-
assert.ok(results.length > 0, 'should have at least one check result');
|
|
22
|
-
for (const r of results) {
|
|
23
|
-
assert.equal(typeof r.name, 'string');
|
|
24
|
-
assert.ok(['ok', 'warn', 'error', 'skip'].includes(r.status));
|
|
25
|
-
assert.equal(typeof r.detail, 'string');
|
|
26
|
-
}
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
test('formatReport produces readable output', () => {
|
|
30
|
-
const results = [
|
|
31
|
-
{ name: 'Binary version', status: 'ok', detail: 'v0.7.16' },
|
|
32
|
-
{ name: 'Source fresh', status: 'warn', detail: 'src/ modified 3min after binary', fixId: 'binary-stale' },
|
|
33
|
-
{ name: 'Schema', status: 'ok', detail: 'v6' },
|
|
34
|
-
];
|
|
35
|
-
const output = formatReport(results);
|
|
36
|
-
assert.ok(output.includes('Binary version'));
|
|
37
|
-
assert.ok(output.includes('v0.7.16'));
|
|
38
|
-
assert.ok(output.includes('Source fresh'));
|
|
39
|
-
assert.ok(output.includes('3min'));
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
test('formatReport shows issue count when problems exist', () => {
|
|
43
|
-
const results = [
|
|
44
|
-
{ name: 'Test', status: 'warn', detail: 'problem', fixId: 'test-fix' },
|
|
45
|
-
];
|
|
46
|
-
const output = formatReport(results);
|
|
47
|
-
assert.ok(output.includes('1'));
|
|
48
|
-
assert.ok(output.includes('issue'));
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test('formatReport: --check-only never says "Fixing..." (it does not repair)', () => {
|
|
52
|
-
const results = [
|
|
53
|
-
{ name: 'Hook coverage', status: 'warn', detail: 'missing', fixId: 'hooks' },
|
|
54
|
-
];
|
|
55
|
-
// Default (repair mode) announces the fix.
|
|
56
|
-
assert.ok(formatReport(results).includes('Fixing...'),
|
|
57
|
-
'repair mode should announce Fixing...');
|
|
58
|
-
// --check-only is read-only: it must NOT claim to fix, and should point the
|
|
59
|
-
// user at the repair command instead.
|
|
60
|
-
const checkOnly = formatReport(results, { checkOnly: true });
|
|
61
|
-
assert.ok(!checkOnly.includes('Fixing...'),
|
|
62
|
-
`--check-only must not say "Fixing..."; got: ${checkOnly}`);
|
|
63
|
-
assert.ok(checkOnly.includes('--check-only'),
|
|
64
|
-
`--check-only should hint how to fix; got: ${checkOnly}`);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('formatReport shows all-clear when no problems', () => {
|
|
68
|
-
const results = [
|
|
69
|
-
{ name: 'Binary version', status: 'ok', detail: 'v0.7.16' },
|
|
70
|
-
{ name: 'Schema', status: 'ok', detail: 'v6' },
|
|
71
|
-
];
|
|
72
|
-
const output = formatReport(results);
|
|
73
|
-
assert.ok(output.includes('All checks passed') || output.includes('0 issues'));
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
test('surveyHookCoverage reports clean when all entries are current', () => {
|
|
77
|
-
const cov = surveyHookCoverage(settingsWithCurrentHooks());
|
|
78
|
-
assert.equal(cov.missing.length, 0, 'no missing entries');
|
|
79
|
-
assert.equal(cov.stale.length, 0, 'no stale entries');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
test('surveyHookCoverage flags a present-but-stale hook path', () => {
|
|
83
|
-
const settings = settingsWithCurrentHooks();
|
|
84
|
-
// Repoint one PreToolUse entry at an old plugin-cache version dir — present,
|
|
85
|
-
// recognized as ours (description unchanged), but command no longer current.
|
|
86
|
-
const bash = settings.hooks.PreToolUse.find(e => e.matcher === 'Bash');
|
|
87
|
-
bash.hooks[0].command = bash.hooks[0].command.replace('/scripts/', '/0.0.1-old/scripts/');
|
|
88
|
-
const cov = surveyHookCoverage(settings);
|
|
89
|
-
assert.equal(cov.missing.length, 0, 'entry is present, not missing');
|
|
90
|
-
assert.ok(cov.stale.includes('PreToolUse:Bash'),
|
|
91
|
-
`stale Bash path should be flagged; got stale=${JSON.stringify(cov.stale)}`);
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
test('surveyHookCoverage flags missing entries when settings empty', () => {
|
|
95
|
-
const cov = surveyHookCoverage({});
|
|
96
|
-
assert.ok(cov.missing.length === cov.expected.length, 'all expected entries missing');
|
|
97
|
-
assert.equal(cov.stale.length, 0, 'nothing present to be stale');
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
// ── relicRepairGuard (v0.50.0 — doctor twin of the session-init relic guard) ──
|
|
101
|
-
|
|
102
|
-
test('relicRepairGuard blocks settings repair from a relic copy and redirects', () => {
|
|
103
|
-
const { relicRepairGuard } = require('./doctor');
|
|
104
|
-
const lines = [];
|
|
105
|
-
// Relic context → guard fires, prints the redirect, returns true (skip install).
|
|
106
|
-
assert.equal(relicRepairGuard({ relic: true, log: (s) => lines.push(s) }), true);
|
|
107
|
-
assert.ok(lines.some(l => l.includes('not the active install')),
|
|
108
|
-
`guard must explain why repair is skipped, got: ${lines.join(' | ')}`);
|
|
109
|
-
// Active (or dev/npm) context → repair proceeds.
|
|
110
|
-
assert.equal(relicRepairGuard({ relic: false, log: () => {} }), false);
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── classifyEmbeddings (vector-availability — warns on silent FTS5-only) ──
|
|
114
|
-
|
|
115
|
-
test('classifyEmbeddings WARNS when embed-capable but nothing embedded (vector inactive)', () => {
|
|
116
|
-
const { classifyEmbeddings } = require('./doctor');
|
|
117
|
-
// The exact silent-FTS5 gap: model_available compile-flag true, real embeddable
|
|
118
|
-
// nodes exist, but 0 embedded (model never downloaded/loaded).
|
|
119
|
-
const r = classifyEmbeddings({ model_available: true, embedding_progress: '0/2745',
|
|
120
|
-
embedding_status: 'pending', search_mode: 'fts_only' });
|
|
121
|
-
assert.equal(r.status, 'warn', 'must not false-green a vector-inactive index');
|
|
122
|
-
assert.match(r.detail, /FTS5-only|vector INACTIVE/);
|
|
123
|
-
});
|
|
124
|
-
|
|
125
|
-
test('classifyEmbeddings WARNS when binary lacks embed-model feature', () => {
|
|
126
|
-
const { classifyEmbeddings } = require('./doctor');
|
|
127
|
-
const r = classifyEmbeddings({ model_available: false, embedding_progress: '0/0' });
|
|
128
|
-
assert.equal(r.status, 'warn');
|
|
129
|
-
assert.match(r.detail, /without embed-model/);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('classifyEmbeddings OK for hybrid (partial + complete) and no-embeddable', () => {
|
|
133
|
-
const { classifyEmbeddings } = require('./doctor');
|
|
134
|
-
assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '900/2745' }).status, 'ok');
|
|
135
|
-
assert.equal(classifyEmbeddings({ model_available: true, embedding_progress: '2745/2745' }).status, 'ok');
|
|
136
|
-
// total === 0 is a non-code index, genuinely nothing to embed → ok, not a false warn.
|
|
137
|
-
const none = classifyEmbeddings({ model_available: true, embedding_progress: '0/0' });
|
|
138
|
-
assert.equal(none.status, 'ok');
|
|
139
|
-
assert.match(none.detail, /no embeddable nodes/);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
// ── dev-rebuild feature preservation (no silent hybrid→FTS5 downgrade / ping-pong) ──
|
|
143
|
-
test('devBuildCommand preserves feature set: hybrid → --features embed-model, fts → --no-default-features', () => {
|
|
144
|
-
const { devBuildCommand } = require('./doctor');
|
|
145
|
-
assert.match(devBuildCommand(true), /--features embed-model/);
|
|
146
|
-
assert.doesNotMatch(devBuildCommand(true), /--no-default-features/);
|
|
147
|
-
assert.match(devBuildCommand(false), /--no-default-features/);
|
|
148
|
-
assert.doesNotMatch(devBuildCommand(false), /--features embed-model/);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
test('detectEmbedModel reads model_available from `health-check --json`; probe failure → null (never a false downgrade signal)', () => {
|
|
152
|
-
const { detectEmbedModel } = require('./doctor');
|
|
153
|
-
// hybrid binary
|
|
154
|
-
const hybridStub = (_bin, args) => {
|
|
155
|
-
assert.deepEqual(args, ['health-check', '--json']);
|
|
156
|
-
return JSON.stringify({ model_available: true });
|
|
157
|
-
};
|
|
158
|
-
assert.equal(detectEmbedModel('/bin/cg', hybridStub), true);
|
|
159
|
-
// FTS5-only binary
|
|
160
|
-
assert.equal(detectEmbedModel('/bin/cg', () => JSON.stringify({ model_available: false })), false);
|
|
161
|
-
// probe throws (binary broken) → null (caller defaults to FTS5 + note, not a downgrade claim)
|
|
162
|
-
assert.equal(detectEmbedModel('/bin/cg', () => { throw new Error('boom'); }), null);
|
|
163
|
-
// unparseable output → null
|
|
164
|
-
assert.equal(detectEmbedModel('/bin/cg', () => 'not json'), null);
|
|
165
|
-
// no binary → null
|
|
166
|
-
assert.equal(detectEmbedModel(null), null);
|
|
167
|
-
});
|
|
168
|
-
|
|
169
|
-
test('unresolvedCount: repair mode exits 0 iff every found issue was fixed', () => {
|
|
170
|
-
const { unresolvedCount } = require('./doctor');
|
|
171
|
-
// Clean run — nothing found.
|
|
172
|
-
assert.equal(unresolvedCount({ checkOnly: false, issueCount: 0, fixed: 0 }), 0);
|
|
173
|
-
// Repair fixed everything ("N/N addressed") → 0, so `doctor && …` and
|
|
174
|
-
// self-heal automation don't read a successful repair as failure. This is
|
|
175
|
-
// the regression this contract guards: previously exited 1 on any issue found.
|
|
176
|
-
assert.equal(unresolvedCount({ checkOnly: false, issueCount: 3, fixed: 3 }), 0);
|
|
177
|
-
// Partial repair → the remainder is unresolved (nonzero → exit 1).
|
|
178
|
-
assert.equal(unresolvedCount({ checkOnly: false, issueCount: 3, fixed: 1 }), 2);
|
|
179
|
-
// Advisory-only issue with no working auto-repair (fixed stays 0) → unresolved.
|
|
180
|
-
assert.equal(unresolvedCount({ checkOnly: false, issueCount: 1, fixed: 0 }), 1);
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
test('unresolvedCount: --check-only reports every found issue (never repairs)', () => {
|
|
184
|
-
const { unresolvedCount } = require('./doctor');
|
|
185
|
-
// check-only performs no repair, so fixed is 0; a found issue must still
|
|
186
|
-
// surface as unresolved (exit 1) — check mode reports cleanliness.
|
|
187
|
-
assert.equal(unresolvedCount({ checkOnly: true, issueCount: 2, fixed: 0 }), 2);
|
|
188
|
-
assert.equal(unresolvedCount({ checkOnly: true, issueCount: 0, fixed: 0 }), 0);
|
|
189
|
-
});
|
|
190
|
-
|
|
191
|
-
test('runRepairs: hooks-invalid counts fixed only when the post-install re-scan is clean', () => {
|
|
192
|
-
// hooks-invalid is raised only after diagnosis already ran install()+re-scan
|
|
193
|
-
// and paths were STILL broken. The repair arm must re-verify, else it reports
|
|
194
|
-
// a false exit 0 ("healthy") while the hooks stay broken. Stub the lifecycle
|
|
195
|
-
// deps runRepairs pulls via require('./lifecycle') on the shared cached export
|
|
196
|
-
// object; restore in finally so no other test sees the stubs.
|
|
197
|
-
const { runRepairs } = require('./doctor');
|
|
198
|
-
const lc = require('./lifecycle');
|
|
199
|
-
const orig = { install: lc.install, scan: lc.scanForBrokenPaths, relic: lc.isStaleRelicContext };
|
|
200
|
-
const hooksInvalid = [{ name: 'Hooks', status: 'warn', fixId: 'hooks-invalid' }];
|
|
201
|
-
try {
|
|
202
|
-
lc.isStaleRelicContext = () => false; // not a relic → repair proceeds
|
|
203
|
-
lc.install = () => {}; // install() that cannot restore the paths
|
|
204
|
-
// Re-scan still broken → must NOT count as fixed (old code did fixed++ blindly).
|
|
205
|
-
lc.scanForBrokenPaths = () => [{ type: 'hook', event: 'PreToolUse:Edit', path: '/gone.js' }];
|
|
206
|
-
assert.equal(runRepairs(hooksInvalid), 0, 'still-broken after install must not count as fixed');
|
|
207
|
-
// Re-scan clean → the repair took effect → counts as fixed.
|
|
208
|
-
lc.scanForBrokenPaths = () => [];
|
|
209
|
-
assert.equal(runRepairs(hooksInvalid), 1, 'verified-clean after install counts as fixed');
|
|
210
|
-
} finally {
|
|
211
|
-
lc.install = orig.install;
|
|
212
|
-
lc.scanForBrokenPaths = orig.scan;
|
|
213
|
-
lc.isStaleRelicContext = orig.relic;
|
|
214
|
-
}
|
|
215
|
-
});
|