@sdsrs/code-graph 0.94.0 → 0.95.1

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.
Files changed (30) hide show
  1. package/claude-plugin/.claude-plugin/plugin.json +1 -1
  2. package/claude-plugin/scripts/auto-update.js +69 -9
  3. package/package.json +8 -7
  4. package/claude-plugin/scripts/adopt.test.js +0 -679
  5. package/claude-plugin/scripts/auto-update.test.js +0 -515
  6. package/claude-plugin/scripts/cg-answer.test.js +0 -309
  7. package/claude-plugin/scripts/claude-config.test.js +0 -58
  8. package/claude-plugin/scripts/covering-tests.test.js +0 -78
  9. package/claude-plugin/scripts/doctor.test.js +0 -215
  10. package/claude-plugin/scripts/find-binary.test.js +0 -246
  11. package/claude-plugin/scripts/hook-fire.test.js +0 -117
  12. package/claude-plugin/scripts/hooks.test.js +0 -230
  13. package/claude-plugin/scripts/incremental-index.test.js +0 -102
  14. package/claude-plugin/scripts/lifecycle.e2e.test.js +0 -179
  15. package/claude-plugin/scripts/lifecycle.test.js +0 -786
  16. package/claude-plugin/scripts/mcp-launcher.test.js +0 -162
  17. package/claude-plugin/scripts/mcp-stub.test.js +0 -207
  18. package/claude-plugin/scripts/post-grep-inject.test.js +0 -531
  19. package/claude-plugin/scripts/pr-impact-comment.test.js +0 -110
  20. package/claude-plugin/scripts/pre-edit-guide.test.js +0 -218
  21. package/claude-plugin/scripts/pre-grep-guide.test.js +0 -1682
  22. package/claude-plugin/scripts/pre-read-guide.test.js +0 -363
  23. package/claude-plugin/scripts/project-detect.test.js +0 -95
  24. package/claude-plugin/scripts/recommendation-log.test.js +0 -79
  25. package/claude-plugin/scripts/session-init.test.js +0 -479
  26. package/claude-plugin/scripts/statusline-composite.test.js +0 -65
  27. package/claude-plugin/scripts/statusline.test.js +0 -235
  28. package/claude-plugin/scripts/tmp-dir.test.js +0 -50
  29. package/claude-plugin/scripts/user-prompt-context.test.js +0 -743
  30. package/claude-plugin/scripts/version-utils.test.js +0 -141
@@ -1,743 +0,0 @@
1
- 'use strict';
2
- const test = require('node:test');
3
- const assert = require('node:assert/strict');
4
- const path = require('node:path');
5
- const fs = require('node:fs');
6
-
7
- const {
8
- shouldSkip,
9
- extractFilePaths,
10
- extractSymbols,
11
- detectIntents,
12
- scoreIntent,
13
- INTENT_PATTERNS,
14
- INTENT_THRESHOLD,
15
- determineQueryType,
16
- computeQuietHooks,
17
- buildRunEnv,
18
- } = require('./user-prompt-context');
19
-
20
- // ── shouldSkip ──────────────────────────────────────────
21
-
22
- test('shouldSkip: simple confirmations (EN)', () => {
23
- for (const msg of ['yes', 'no', 'ok', 'done', 'y', 'n', 'commit', 'push', 'thanks']) {
24
- assert.ok(shouldSkip(msg), `should skip "${msg}"`);
25
- }
26
- });
27
-
28
- test('shouldSkip: simple confirmations (ZH)', () => {
29
- for (const msg of ['继续', '确认', '好的', '好', '是的', '不', '可以', '行', '对', '提交', '推送', '没问题', '谢谢', '发布', '更新', '清理']) {
30
- assert.ok(shouldSkip(msg), `should skip "${msg}"`);
31
- }
32
- });
33
-
34
- test('shouldSkip: with trailing punctuation', () => {
35
- assert.ok(shouldSkip('好的。'));
36
- assert.ok(shouldSkip('ok!'));
37
- assert.ok(shouldSkip('确认?'));
38
- });
39
-
40
- test('shouldSkip: action-only without code entities', () => {
41
- assert.equal(shouldSkip('修复这些问题'), 'action-only');
42
- assert.equal(shouldSkip('按优先级实施'), 'action-only');
43
- assert.equal(shouldSkip('执行这个方案'), 'action-only');
44
- assert.equal(shouldSkip('开始吧'), 'action-only');
45
- });
46
-
47
- test('shouldSkip: action with 3+ Latin chars passes through', () => {
48
- assert.equal(shouldSkip('修复 parse_code 里的bug'), false);
49
- assert.equal(shouldSkip('修复这段逻辑的bug'), false); // "bug" = 3 chars
50
- assert.equal(shouldSkip('修复 API 的问题'), false); // "API" = 3 chars
51
- });
52
-
53
- test('shouldSkip: NOT skip legitimate code tasks', () => {
54
- assert.equal(shouldSkip('帮我写一个工具函数'), false);
55
- assert.equal(shouldSkip('帮我优化一下这个查询'), false);
56
- assert.equal(shouldSkip('优化 parse_code 的性能'), false);
57
- assert.equal(shouldSkip('看看 src/mcp/ 模块的代码结构'), false);
58
- assert.equal(shouldSkip('重构一下这个模块'), false);
59
- });
60
-
61
- test('shouldSkip: messages below length threshold exit early in main', () => {
62
- // The 8-char minimum is checked in the main block, not in shouldSkip
63
- // shouldSkip itself doesn't enforce length
64
- assert.equal(shouldSkip('短消息很短'), false); // passes shouldSkip but would exit in main
65
- });
66
-
67
- // ── extractFilePaths ────────────────────────────────────
68
-
69
- test('extractFilePaths: extracts src/ paths', () => {
70
- assert.deepEqual(extractFilePaths('看看 src/mcp/server.rs'), ['src/mcp/server.rs']);
71
- assert.deepEqual(extractFilePaths('修改 src/parser/relations.rs 和 src/storage/db.rs'), ['src/parser/relations.rs', 'src/storage/db.rs']);
72
- });
73
-
74
- test('extractFilePaths: extracts lib/test/pkg paths', () => {
75
- assert.deepEqual(extractFilePaths('check lib/utils/helpers.js'), ['lib/utils/helpers.js']);
76
- assert.deepEqual(extractFilePaths('test/integration.rs is failing'), ['test/integration.rs']);
77
- });
78
-
79
- test('extractFilePaths: limits to 2 paths', () => {
80
- const result = extractFilePaths('src/a.rs src/b.rs src/c.rs');
81
- assert.equal(result.length, 2);
82
- });
83
-
84
- test('extractFilePaths: no match for non-code paths', () => {
85
- assert.deepEqual(extractFilePaths('这个函数有问题'), []);
86
- assert.deepEqual(extractFilePaths('update the readme'), []);
87
- });
88
-
89
- // ── extractSymbols ──────────────────────────────────────
90
-
91
- test('extractSymbols: snake_case', () => {
92
- const r = extractSymbols('修改 parse_code 函数');
93
- assert.deepEqual(r.symbols, ['parse_code']);
94
- assert.equal(r.lowConfidence, false);
95
- });
96
-
97
- test('extractSymbols: camelCase', () => {
98
- const r = extractSymbols('fix the handleMessage function');
99
- assert.ok(r.symbols.includes('handleMessage'));
100
- assert.equal(r.lowConfidence, false);
101
- });
102
-
103
- test('extractSymbols: PascalCase compound', () => {
104
- const r = extractSymbols('implement McpServer class');
105
- assert.ok(r.symbols.includes('McpServer'));
106
- });
107
-
108
- test('extractSymbols: qualified names (Foo::bar)', () => {
109
- const r = extractSymbols('check Foo::bar::baz');
110
- assert.ok(r.symbols.some(s => s.includes('::')));
111
- });
112
-
113
- test('extractSymbols: backtick-quoted fallback', () => {
114
- const r = extractSymbols('修改 `parse` 函数');
115
- assert.ok(r.symbols.includes('parse'));
116
- });
117
-
118
- test('extractSymbols: backtick with longer name', () => {
119
- const r = extractSymbols('看看 `fts5_search` 怎么实现的');
120
- assert.ok(r.symbols.includes('fts5_search'));
121
- });
122
-
123
- test('extractSymbols: plain word fallback (low confidence)', () => {
124
- const r = extractSymbols('write tests for the embedding module');
125
- assert.ok(r.symbols.includes('embedding'));
126
- assert.equal(r.lowConfidence, true);
127
- });
128
-
129
- test('extractSymbols: plain words excluded (common English verbs)', () => {
130
- const r = extractSymbols('help me understand the refactor approach');
131
- // "understand" and "refactor" are excluded, "approach" is excluded
132
- assert.equal(r.symbols.length, 0);
133
- });
134
-
135
- test('extractSymbols: stop words filtered', () => {
136
- const r = extractSymbols('fix the default function');
137
- // "default" and "function" are stop words
138
- assert.equal(r.symbols.length, 0);
139
- });
140
-
141
- test('extractSymbols: limits to 3 symbols', () => {
142
- const r = extractSymbols('modify parse_code and run_full_index and extract_relations and hash_file');
143
- assert.ok(r.symbols.length <= 3);
144
- });
145
-
146
- // ── detectIntents ───────────────────────────────────────
147
-
148
- // --- Impact intent ---
149
- test('detectIntents: impact (EN)', () => {
150
- assert.ok(detectIntents('what is the impact of this change').impact);
151
- assert.ok(detectIntents('check the risk of modifying this').impact);
152
- assert.ok(detectIntents('this bug is critical').impact);
153
- });
154
-
155
- test('detectIntents: impact (ZH)', () => {
156
- assert.ok(detectIntents('这个改动有什么影响').impact);
157
- assert.ok(detectIntents('改动范围有多大').impact);
158
- assert.ok(detectIntents('会不会跟其他模块冲突').impact);
159
- assert.ok(detectIntents('修改前先看看').impact);
160
- assert.ok(detectIntents('有什么风险').impact);
161
- assert.ok(detectIntents('这个bug怎么回事').impact);
162
- });
163
-
164
- // --- Modify intent ---
165
- test('detectIntents: modify (EN)', () => {
166
- assert.ok(detectIntents('refactor this module').modify);
167
- assert.ok(detectIntents('rename the function').modify);
168
- assert.ok(detectIntents('fix the broken test').modify);
169
- assert.ok(detectIntents('update the config').modify);
170
- assert.ok(detectIntents('remove deprecated code').modify);
171
- assert.ok(detectIntents('replace with new impl').modify);
172
- });
173
-
174
- test('detectIntents: modify (ZH)', () => {
175
- const words = ['修改', '修复', '重构', '优化', '简化', '精简', '适配', '统一', '修正', '调整', '去掉', '整理', '清理', '解耦', '更新', '升级', '迁移', '拆分', '合并', '提取'];
176
- for (const w of words) {
177
- assert.ok(detectIntents(`${w}这个模块`).modify, `"${w}" should trigger modify`);
178
- }
179
- });
180
-
181
- test('detectIntents: modify (ZH compound)', () => {
182
- assert.ok(detectIntents('把这个函数改成异步的').modify);
183
- assert.ok(detectIntents('把返回值类型换成 Result').modify);
184
- assert.ok(detectIntents('把同步改成异步').modify);
185
- });
186
-
187
- // --- Implement intent ---
188
- test('detectIntents: implement (EN)', () => {
189
- assert.ok(detectIntents('add a new tool').implement);
190
- assert.ok(detectIntents('implement error handling').implement);
191
- assert.ok(detectIntents('create a helper function').implement);
192
- assert.ok(detectIntents('build the CI pipeline').implement);
193
- assert.ok(detectIntents('write unit tests').implement);
194
- });
195
-
196
- test('detectIntents: implement (ZH)', () => {
197
- const words = ['新增', '添加', '实现', '创建', '编写', '开发', '增加', '加上', '加个', '搭建', '补充', '引入', '支持', '封装', '接入', '对接', '配置'];
198
- for (const w of words) {
199
- assert.ok(detectIntents(`${w}一个功能`).implement, `"${w}" should trigger implement`);
200
- }
201
- });
202
-
203
- test('detectIntents: implement - "写" variants', () => {
204
- assert.ok(detectIntents('写个测试').implement);
205
- assert.ok(detectIntents('写一个工具函数').implement);
206
- assert.ok(detectIntents('帮我写一个函数').implement);
207
- });
208
-
209
- // --- Understand intent ---
210
- test('detectIntents: understand (EN)', () => {
211
- assert.ok(detectIntents('how does this module work').understand);
212
- assert.ok(detectIntents('explain the architecture').understand);
213
- });
214
-
215
- test('detectIntents: understand (ZH)', () => {
216
- const words = ['看看', '看一下', '理解', '了解', '分析', '评估', '检查', '审核', '审查', '验证', '诊断', '深入思考'];
217
- for (const w of words) {
218
- assert.ok(detectIntents(`${w}这段代码`).understand, `"${w}" should trigger understand`);
219
- }
220
- });
221
-
222
- test('detectIntents: understand (ZH question patterns)', () => {
223
- assert.ok(detectIntents('这个模块是干什么的').understand);
224
- assert.ok(detectIntents('工作原理是什么').understand);
225
- assert.ok(detectIntents('整个流程是怎么走的').understand);
226
- assert.ok(detectIntents('这个功能怎么实现的').understand);
227
- });
228
-
229
- // --- Callgraph intent ---
230
- test('detectIntents: callgraph (EN)', () => {
231
- assert.ok(detectIntents('who calls this function').callgraph);
232
- assert.ok(detectIntents('what calls parse_code').callgraph);
233
- assert.ok(detectIntents('trace the request flow').callgraph);
234
- });
235
-
236
- test('detectIntents: callgraph (ZH)', () => {
237
- assert.ok(detectIntents('这个函数被谁调了').callgraph);
238
- assert.ok(detectIntents('看看调用链路').callgraph);
239
- assert.ok(detectIntents('追踪一下请求路径').callgraph);
240
- assert.ok(detectIntents('上下游依赖关系是什么').callgraph);
241
- assert.ok(detectIntents('这个事件怎么触发的').callgraph);
242
- });
243
-
244
- // --- Search intent ---
245
- test('detectIntents: search (EN)', () => {
246
- assert.ok(detectIntents('where is the config defined').search);
247
- assert.ok(detectIntents('find the error handling code').search);
248
- assert.ok(detectIntents('search for all usages').search);
249
- });
250
-
251
- test('detectIntents: search (ZH)', () => {
252
- assert.ok(detectIntents('这个函数定义在哪').search);
253
- assert.ok(detectIntents('找一下处理错误的代码').search);
254
- assert.ok(detectIntents('搜索所有用到这个类型的地方').search);
255
- assert.ok(detectIntents('在哪里用了这个常量').search);
256
- });
257
-
258
- // --- Per-keyword scoring (v0.21 weighted-scorer refactor) ---
259
- test('scoreIntent: matched keyword returns its weight, unmatched returns 0', () => {
260
- // Each pattern in INTENT_PATTERNS is testable in isolation now.
261
- assert.equal(scoreIntent('this bug is critical', 'impact'), 1.0);
262
- assert.equal(scoreIntent('hello world', 'impact'), 0);
263
- assert.equal(scoreIntent('refactor this module', 'modify'), 1.0);
264
- assert.equal(scoreIntent('refactor this module', 'implement'), 0);
265
- });
266
-
267
- test('scoreIntent: max weight wins when multiple patterns match', () => {
268
- // "this bug needs a fix and impact analysis" matches `impact`, `bug`,
269
- // `risk`-no, all three impact rows are weight 1.0 currently — score is 1.0.
270
- // Spec: scoreIntent returns max(weight) of matching patterns, never sum.
271
- const score = scoreIntent('this bug needs impact analysis', 'impact');
272
- assert.equal(score, 1.0);
273
- });
274
-
275
- test('scoreIntent: unknown intent returns 0 (no throw)', () => {
276
- assert.equal(scoreIntent('anything', 'nonexistent_intent'), 0);
277
- });
278
-
279
- test('INTENT_PATTERNS: every intent has at least 5 patterns and uniform weights', () => {
280
- // v0.21 starts with uniform weights; future tuning can vary them per-pattern.
281
- // This test guards against regression to the giant single-regex form.
282
- const intents = ['impact', 'modify', 'implement', 'understand', 'callgraph', 'search'];
283
- for (const intent of intents) {
284
- const patterns = INTENT_PATTERNS[intent];
285
- assert.ok(Array.isArray(patterns), `${intent} must have patterns array`);
286
- assert.ok(patterns.length >= 5, `${intent} must have >=5 patterns, got ${patterns.length}`);
287
- for (const [pattern, weight] of patterns) {
288
- assert.ok(pattern instanceof RegExp, `${intent} pattern must be RegExp`);
289
- assert.ok(typeof weight === 'number' && weight > 0 && weight <= 1, `${intent} weight must be (0, 1]`);
290
- }
291
- }
292
- });
293
-
294
- test('INTENT_THRESHOLD is 0.5 — single weight-1.0 match fires the intent', () => {
295
- // Threshold contract: any pattern @ weight >= 0.5 → intent fires.
296
- // If we lower a pattern to weight 0.4, it must NOT fire alone.
297
- assert.equal(INTENT_THRESHOLD, 0.5);
298
- });
299
-
300
- // --- No false positives ---
301
- test('detectIntents: simple confirmations have no code intent', () => {
302
- const r = detectIntents('好的');
303
- // "什么" would match in some words, but "好的" shouldn't trigger understand
304
- assert.equal(r.modify, false);
305
- assert.equal(r.implement, false);
306
- assert.equal(r.callgraph, false);
307
- assert.equal(r.search, false);
308
- });
309
-
310
- // ── determineQueryType (priority logic) ─────────────────
311
-
312
- test('priority: impact/modify + strict symbol → impact', () => {
313
- const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
314
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
315
- const result = determineQueryType(intents, symbols, []);
316
- assert.equal(result.type, 'impact');
317
- assert.equal(result.symbol, 'parse_code');
318
- });
319
-
320
- test('priority: modify + strict symbol → impact', () => {
321
- const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
322
- const symbols = { symbols: ['handleMessage'], lowConfidence: false };
323
- const result = determineQueryType(intents, symbols, []);
324
- assert.equal(result.type, 'impact');
325
- });
326
-
327
- test('priority: modify + low-confidence symbol → NOT impact (falls to overview/search)', () => {
328
- const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
329
- const symbols = { symbols: ['embedding'], lowConfidence: true };
330
- const result = determineQueryType(intents, symbols, ['src/embed/']);
331
- // Should fall through to overview (file paths exist)
332
- assert.equal(result.type, 'overview');
333
- });
334
-
335
- test('priority: callgraph + strict symbol → callgraph', () => {
336
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: true, search: false };
337
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
338
- const result = determineQueryType(intents, symbols, []);
339
- assert.equal(result.type, 'callgraph');
340
- });
341
-
342
- test('priority: file paths → overview (regardless of intent)', () => {
343
- const intents = { impact: false, modify: true, implement: false, understand: false, callgraph: false, search: false };
344
- const symbols = { symbols: [], lowConfidence: false };
345
- const result = determineQueryType(intents, symbols, ['src/storage/queries.rs']);
346
- assert.equal(result.type, 'overview');
347
- assert.equal(result.path, 'src/storage/');
348
- });
349
-
350
- test('priority: search intent + symbol → search', () => {
351
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: true };
352
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
353
- const result = determineQueryType(intents, symbols, []);
354
- assert.equal(result.type, 'search');
355
- });
356
-
357
- test('priority: implement intent + symbol → search', () => {
358
- const intents = { impact: false, modify: false, implement: true, understand: false, callgraph: false, search: false };
359
- const symbols = { symbols: ['embedding'], lowConfidence: true };
360
- const result = determineQueryType(intents, symbols, []);
361
- assert.equal(result.type, 'search');
362
- });
363
-
364
- test('priority: understand + symbol → search', () => {
365
- const intents = { impact: false, modify: false, implement: false, understand: true, callgraph: false, search: false };
366
- const symbols = { symbols: ['pipeline'], lowConfidence: true };
367
- const result = determineQueryType(intents, symbols, []);
368
- assert.equal(result.type, 'search');
369
- });
370
-
371
- test('priority: no intent, no symbol, no path → null', () => {
372
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
373
- const symbols = { symbols: [], lowConfidence: false };
374
- const result = determineQueryType(intents, symbols, []);
375
- assert.equal(result, null);
376
- });
377
-
378
- test('priority: cooldown blocks query', () => {
379
- const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
380
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
381
- const result = determineQueryType(intents, symbols, [], (type) => type === 'impact');
382
- // Impact blocked by cooldown, falls through; no file path, no search intent → try search via understand fallback
383
- // Actually: no understand intent and hasAny=true, so the last condition (!hasAny) is false → null
384
- // But symbol exists and we have filePaths=[] → falls to search via implement/qualified check → no
385
- // Actually it should return null since all fallbacks require conditions not met
386
- assert.equal(result, null);
387
- });
388
-
389
- test('priority: cooldown on impact → falls to overview when file paths exist', () => {
390
- const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
391
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
392
- const result = determineQueryType(intents, symbols, ['src/parser/mod.rs'], (type) => type === 'impact');
393
- assert.equal(result.type, 'overview');
394
- });
395
-
396
- // ── Full integration: message → query type ──────────────
397
-
398
- function analyze(msg) {
399
- if (shouldSkip(msg)) return { skipped: true };
400
- const fp = extractFilePaths(msg);
401
- const sym = extractSymbols(msg);
402
- const intents = detectIntents(msg);
403
- // Phase E: pass message into determineQueryType so symptom-hint fallback fires.
404
- const query = determineQueryType(intents, sym, fp, undefined, msg);
405
- return { query, intents, symbols: sym, filePaths: fp };
406
- }
407
-
408
- test('integration: 修改 parse_code 函数增加错误处理 → impact', () => {
409
- const r = analyze('修改 parse_code 函数增加错误处理');
410
- assert.equal(r.query.type, 'impact');
411
- assert.equal(r.query.symbol, 'parse_code');
412
- });
413
-
414
- test('integration: 看看 src/mcp/ 模块的代码结构 → overview', () => {
415
- const r = analyze('看看 src/mcp/ 模块的代码结构');
416
- assert.equal(r.query.type, 'overview');
417
- });
418
-
419
- test('integration: refactor src/storage/queries.rs → overview (not impact on "refactor")', () => {
420
- const r = analyze('refactor src/storage/queries.rs to use parameterized queries');
421
- assert.equal(r.query.type, 'overview');
422
- assert.ok(r.query.path.includes('src/storage/'));
423
- });
424
-
425
- test('integration: help me understand the indexer pipeline → search', () => {
426
- const r = analyze('help me understand the indexer pipeline');
427
- assert.equal(r.query.type, 'search');
428
- assert.equal(r.query.symbol, 'pipeline');
429
- });
430
-
431
- test('integration: write tests for the embedding module → search', () => {
432
- const r = analyze('write tests for the embedding module');
433
- assert.equal(r.query.type, 'search');
434
- assert.equal(r.query.symbol, 'embedding');
435
- });
436
-
437
- test('integration: 修复这段逻辑的bug → not skipped (bug=3 chars)', () => {
438
- const r = analyze('修复这段逻辑的bug');
439
- assert.ok(!r.skipped);
440
- assert.ok(r.intents.impact); // "bug"
441
- assert.ok(r.intents.modify); // "修复"
442
- });
443
-
444
- test('integration: 按优先级修复这些问题 → skipped (no code entity)', () => {
445
- const r = analyze('按优先级修复这些问题');
446
- assert.ok(r.skipped);
447
- });
448
-
449
- test('integration: 帮我写一个工具函数 → implement intent', () => {
450
- const r = analyze('帮我写一个工具函数');
451
- assert.ok(!r.skipped);
452
- assert.ok(r.intents.implement);
453
- });
454
-
455
- test('integration: 对整个项目进行一次完整的代码审核 → understand', () => {
456
- const r = analyze('对整个项目进行一次完整的代码审核');
457
- assert.ok(r.intents.understand);
458
- });
459
-
460
- test('integration: 更新一下readme.md → modify intent', () => {
461
- const r = analyze('更新一下readme.md这个文件');
462
- assert.ok(r.intents.modify);
463
- });
464
-
465
- test('integration: 配置 pre-commit hook → implement intent', () => {
466
- const r = analyze('配置提交代码时的git pre-commit hook检查');
467
- assert.ok(r.intents.implement);
468
- });
469
-
470
- test('integration: 检查下我们插件上下文token占用情况 → understand', () => {
471
- const r = analyze('检查下我们插件上下文token占用情况');
472
- assert.ok(r.intents.understand);
473
- });
474
-
475
- test('integration: 诊断一下性能问题 → understand', () => {
476
- const r = analyze('诊断一下性能问题');
477
- assert.ok(r.intents.understand);
478
- });
479
-
480
- test('integration: simple confirmation → skipped', () => {
481
- assert.ok(analyze('好的').skipped);
482
- assert.ok(analyze('继续').skipped);
483
- assert.ok(analyze('ok').skipped);
484
- });
485
-
486
- // ── Skill files validation ──────────────────────────────
487
-
488
- test('skills: explore.md has correct frontmatter', () => {
489
- const content = fs.readFileSync(path.join(__dirname, '../skills/explore.md'), 'utf8');
490
- assert.match(content, /^---\nname: explore/);
491
- assert.match(content, /description:/);
492
- });
493
-
494
- test('skills: index.md has correct frontmatter', () => {
495
- const content = fs.readFileSync(path.join(__dirname, '../skills/index.md'), 'utf8');
496
- assert.match(content, /^---\nname: index/);
497
- assert.match(content, /description:/);
498
- });
499
-
500
- test('skills: commands directory is empty (all converted to skills)', () => {
501
- const commandsDir = path.join(__dirname, '../commands');
502
- const exists = fs.existsSync(commandsDir);
503
- if (exists) {
504
- const files = fs.readdirSync(commandsDir).filter(f => f.endsWith('.md'));
505
- assert.equal(files.length, 0, 'commands/ should have no .md files');
506
- }
507
- // Directory not existing is also valid
508
- });
509
-
510
- test('skills: only expected skills exist', () => {
511
- const skillsDir = path.join(__dirname, '../skills');
512
- const files = fs.readdirSync(skillsDir).filter(f => f.endsWith('.md')).sort();
513
- assert.deepEqual(files, ['explore.md', 'index.md']);
514
- });
515
-
516
- // ── computeQuietHooks priority chain (default-noisy flip) ────────
517
-
518
- test('computeQuietHooks: default (no env) is NOISY', () => {
519
- // Default flipped back to push-on. The v0.21 opt-in default relied on
520
- // routing-bench P@1=100% but that measures triage accuracy, not whether
521
- // the agent reaches for a tool at all. pre-grep-guide.js sees 13× raw-grep
522
- // bias on the same source tree — push is the corrective.
523
- assert.equal(computeQuietHooks({}), false);
524
- });
525
-
526
- test('computeQuietHooks: CODE_GRAPH_QUIET_HOOKS=1 forces quiet (escape hatch)', () => {
527
- assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1' }), true);
528
- });
529
-
530
- test('computeQuietHooks: CODE_GRAPH_QUIET_HOOKS=0 stays noisy (back-compat, same as default)', () => {
531
- assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0' }), false);
532
- });
533
-
534
- test('computeQuietHooks: CODE_GRAPH_VERBOSE_HOOKS=1 stays noisy (back-compat, same as default)', () => {
535
- assert.equal(computeQuietHooks({ CODE_GRAPH_VERBOSE_HOOKS: '1' }), false);
536
- });
537
-
538
- test('computeQuietHooks: QUIET_HOOKS=1 wins over VERBOSE_HOOKS=1 (priority chain)', () => {
539
- // Priority: CODE_GRAPH_QUIET_HOOKS=1 (escape) > QUIET_HOOKS=0 / VERBOSE_HOOKS=1 > default.
540
- assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '1', CODE_GRAPH_VERBOSE_HOOKS: '1' }), true);
541
- assert.equal(computeQuietHooks({ CODE_GRAPH_QUIET_HOOKS: '0', CODE_GRAPH_VERBOSE_HOOKS: '0' }), false);
542
- });
543
-
544
- test('CODE_GRAPH_QUIET_HOOKS=1 short-circuits silently on stdout, stderr, exit 0 (escape hatch verified end-to-end)', () => {
545
- // End-to-end: the escape hatch must produce zero stdout/stderr noise
546
- // (any leak would land in Claude's display). Was the only e2e check before
547
- // the default-noisy flip — kept under the new default to guarantee that
548
- // setting the env still fully silences the hook.
549
- const { spawnSync } = require('node:child_process');
550
- const script = path.join(__dirname, 'user-prompt-context.js');
551
- const proc = spawnSync(process.execPath, [script], {
552
- input: JSON.stringify({ message: 'impact of refactoring parse_code function' }),
553
- env: { ...process.env, CODE_GRAPH_QUIET_HOOKS: '1' },
554
- encoding: 'utf8',
555
- timeout: 2000,
556
- });
557
- assert.equal(proc.stdout, '', 'quiet must be silent on stdout');
558
- assert.equal(proc.stderr, '', 'quiet must be silent on stderr');
559
- assert.equal(proc.status, 0, 'quiet must exit 0');
560
- });
561
-
562
- test('CODE_GRAPH_QUIET_HOOKS=1 silences even the fresh-install (no-manifest) notice', () => {
563
- // Regression: the mid-session install notice printed BEFORE the quiet check,
564
- // so on a fresh checkout (no ~/.cache/code-graph manifest) it leaked to stdout
565
- // despite the escape hatch. CI hit this; the dev box has a manifest and masked
566
- // it. Force the no-manifest path with a throwaway HOME so it reproduces locally.
567
- const { spawnSync } = require('node:child_process');
568
- const os = require('node:os');
569
- const fs = require('node:fs');
570
- const home = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-upc-nohome-'));
571
- try {
572
- const script = path.join(__dirname, 'user-prompt-context.js');
573
- const proc = spawnSync(process.execPath, [script], {
574
- input: JSON.stringify({ message: 'impact of refactoring parse_code function' }),
575
- // HOME (POSIX) + USERPROFILE (Windows) → os.homedir() points at an empty
576
- // dir, so MANIFEST_PATH is absent and runMain() enters the install branch.
577
- env: { ...process.env, HOME: home, USERPROFILE: home, CODE_GRAPH_QUIET_HOOKS: '1' },
578
- encoding: 'utf8',
579
- timeout: 2000,
580
- });
581
- assert.equal(proc.stdout, '', 'quiet must silence the no-manifest install notice on stdout');
582
- assert.equal(proc.stderr, '', 'quiet must be silent on stderr');
583
- assert.equal(proc.status, 0, 'quiet must exit 0');
584
- } finally {
585
- fs.rmSync(home, { recursive: true, force: true });
586
- }
587
- });
588
-
589
- // ── Phase E: hasSymptom + symptom-hint fallback ──────────────
590
-
591
- const { hasSymptom, SYMPTOM_PATTERNS } = require('./user-prompt-context');
592
-
593
- test('hasSymptom: 报告数据不准', () => {
594
- assert.equal(hasSymptom('今天的报告数据不准'), true);
595
- });
596
-
597
- test('hasSymptom: test 又挂了', () => {
598
- assert.equal(hasSymptom('test 又挂了'), true);
599
- });
600
-
601
- test('hasSymptom: Why does this not work?', () => {
602
- assert.equal(hasSymptom('Why does this not work?'), true);
603
- });
604
-
605
- test('hasSymptom: 有 bug', () => {
606
- assert.equal(hasSymptom('有 bug,帮我看看'), true);
607
- });
608
-
609
- test('hasSymptom: 为什么 (vague-question marker)', () => {
610
- assert.equal(hasSymptom('为什么会这样'), true);
611
- });
612
-
613
- test('hasSymptom: 哪里写错了', () => {
614
- assert.equal(hasSymptom('find 一下哪里写错了'), true);
615
- });
616
-
617
- test('hasSymptom: doesn\'t work / not working', () => {
618
- assert.equal(hasSymptom("this doesn't work as expected"), true);
619
- assert.equal(hasSymptom('the service is not working'), true);
620
- });
621
-
622
- test('hasSymptom: 挂了 / 失败 / 卡死', () => {
623
- assert.equal(hasSymptom('test 挂了'), true);
624
- assert.equal(hasSymptom('又失败了'), true);
625
- assert.equal(hasSymptom('整个服务卡死了'), true);
626
- });
627
-
628
- // Precision: must NOT flag normal task statements as symptoms.
629
- test('hasSymptom: 修改 parse_code → false', () => {
630
- assert.equal(hasSymptom('修改 parse_code 函数增加错误处理'), false);
631
- });
632
-
633
- test('hasSymptom: 看看 src/mcp/ → false', () => {
634
- assert.equal(hasSymptom('看看 src/mcp/ 模块的代码结构'), false);
635
- });
636
-
637
- test('hasSymptom: write tests → false', () => {
638
- assert.equal(hasSymptom('write tests for the embedding module'), false);
639
- });
640
-
641
- test('hasSymptom: empty / non-string → false', () => {
642
- assert.equal(hasSymptom(''), false);
643
- assert.equal(hasSymptom(null), false);
644
- assert.equal(hasSymptom(undefined), false);
645
- });
646
-
647
- test('SYMPTOM_PATTERNS: exported + non-empty array', () => {
648
- assert.ok(Array.isArray(SYMPTOM_PATTERNS));
649
- assert.ok(SYMPTOM_PATTERNS.length >= 8,
650
- `SYMPTOM_PATTERNS has ${SYMPTOM_PATTERNS.length} entries; want ≥8 for coverage`);
651
- });
652
-
653
- // ── determineQueryType: symptom-hint fallback ────────────────
654
-
655
- test('symptom-fallback: pure symptom message, no anchor → symptom-hint', () => {
656
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
657
- const symbols = { symbols: [], lowConfidence: false };
658
- const result = determineQueryType(intents, symbols, [], undefined, '今天的报告数据不准');
659
- assert.equal(result && result.type, 'symptom-hint');
660
- });
661
-
662
- test('symptom-fallback: intent + no symbol/path + symptom → symptom-hint', () => {
663
- // "find 一下哪里写错了" — search intent fires but no symbol or path is extractable.
664
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: true };
665
- const symbols = { symbols: [], lowConfidence: false };
666
- const result = determineQueryType(intents, symbols, [], undefined, 'find 一下哪里写错了');
667
- assert.equal(result && result.type, 'symptom-hint');
668
- });
669
-
670
- test('symptom-fallback: actionable path beats symptom-hint (precedence)', () => {
671
- // Impact path with strict symbol must take precedence even when symptom phrasing is present.
672
- const intents = { impact: true, modify: false, implement: false, understand: false, callgraph: false, search: false };
673
- const symbols = { symbols: ['parse_code'], lowConfidence: false };
674
- const result = determineQueryType(intents, symbols, [], undefined, '修改前看看 parse_code 的 bug 影响');
675
- assert.equal(result.type, 'impact');
676
- });
677
-
678
- test('symptom-fallback: no symptom + no anchor → null (unchanged)', () => {
679
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
680
- const symbols = { symbols: [], lowConfidence: false };
681
- const result = determineQueryType(intents, symbols, [], undefined, 'hello there');
682
- assert.equal(result, null);
683
- });
684
-
685
- test('symptom-fallback: cooldown blocks symptom-hint', () => {
686
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
687
- const symbols = { symbols: [], lowConfidence: false };
688
- const result = determineQueryType(intents, symbols, [], (t) => t === 'symptom', '今天的报告数据不准');
689
- assert.equal(result, null);
690
- });
691
-
692
- test('symptom-fallback: omitted message arg → backward-compat null', () => {
693
- // Existing callers (and the legacy bench harness) call determineQueryType
694
- // without the 5th arg. The fallback must NOT fire — preserve prior behavior.
695
- const intents = { impact: false, modify: false, implement: false, understand: false, callgraph: false, search: false };
696
- const symbols = { symbols: [], lowConfidence: false };
697
- const result = determineQueryType(intents, symbols, []);
698
- assert.equal(result, null);
699
- });
700
-
701
- // ── Integration: analyze() with symptom-only messages ──
702
-
703
- test('integration: 今天的报告数据不准 → symptom-hint', () => {
704
- const r = analyze('今天的报告数据不准');
705
- assert.equal(r.query && r.query.type, 'symptom-hint');
706
- });
707
-
708
- test('integration: test 又挂了 → symptom-hint', () => {
709
- const r = analyze('test 又挂了');
710
- assert.equal(r.query && r.query.type, 'symptom-hint');
711
- });
712
-
713
- test('integration: Why does this not work? → symptom-hint', () => {
714
- const r = analyze('Why does this not work?');
715
- assert.equal(r.query && r.query.type, 'symptom-hint');
716
- });
717
-
718
- // ── buildRunEnv: hook-internal delivery marker (anti phantom-conversion) ──
719
-
720
- test('buildRunEnv: tags CODE_GRAPH_INTERNAL=1 so deliveries are not logged as model `use`', () => {
721
- const env = buildRunEnv({ PATH: '/usr/bin', HOME: '/home/x' });
722
- assert.equal(env.CODE_GRAPH_INTERNAL, '1');
723
- // preserves the base env (binary still resolves on PATH, cwd inherited, etc.)
724
- assert.equal(env.PATH, '/usr/bin');
725
- assert.equal(env.HOME, '/home/x');
726
- });
727
-
728
- test('buildRunEnv: defaults to process.env when no base given', () => {
729
- const env = buildRunEnv();
730
- assert.equal(env.CODE_GRAPH_INTERNAL, '1');
731
- });
732
-
733
- test('run() wires buildRunEnv() into execFileSync (no phantom use-event leak)', () => {
734
- // run() lives inside runMain() (the file top-level-executes on require), so assert
735
- // the wiring at the source level: every code-graph-mcp invocation this hook makes
736
- // must carry the internal marker, else its PUSH injections read back as model
737
- // adoption (the 2026-06-23 mem audit: 100 phantom "model CLI calls"). Mirrors the
738
- // cg-answer.js / pre-edit-guide.js internal-env guard.
739
- const src = fs.readFileSync(path.join(__dirname, 'user-prompt-context.js'), 'utf8');
740
- const i = src.indexOf('function run(');
741
- assert.ok(i >= 0, 'run() helper present');
742
- assert.match(src.slice(i, i + 320), /env:\s*buildRunEnv\(\)/);
743
- });