@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.
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 -474
  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,162 +0,0 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
- /**
4
- * Tests for claude-plugin/scripts/mcp-launcher.js — the .mcp.json entry point
5
- * that resolves the binary (with auto-install fallbacks) and stdio-forwards
6
- * MCP JSON-RPC. install-e2e.test.js §4.3 covers find-binary in dev mode but
7
- * doesn't exercise the launcher's full chain (find → spawn → forward).
8
- *
9
- * The negative paths (no binary anywhere → npm install + GitHub fallback +
10
- * exit 1) are intentionally NOT covered here — the network-bound fallbacks
11
- * have ~150s timeouts and aren't deterministic in CI sandboxes. End-to-end
12
- * dev-mode coverage is the highest-leverage gap.
13
- *
14
- * Run: node --test claude-plugin/scripts/mcp-launcher.test.js
15
- */
16
- const test = require('node:test');
17
- const assert = require('node:assert/strict');
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { spawn } = require('child_process');
21
-
22
- const PLUGIN_ROOT = path.resolve(__dirname, '..');
23
- const REPO_ROOT = path.resolve(PLUGIN_ROOT, '..');
24
- const LAUNCHER = path.join(__dirname, 'mcp-launcher.js');
25
- const BINARY_NAME = process.platform === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
26
- const REL_BINARY = path.join(REPO_ROOT, 'target', 'release', BINARY_NAME);
27
-
28
- function hasBuiltBinary() {
29
- return fs.existsSync(REL_BINARY);
30
- }
31
-
32
- /**
33
- * Run the launcher, send one MCP message on stdin, collect stdout/stderr,
34
- * resolve once we either see a JSON-RPC response on stdout or hit timeout.
35
- */
36
- function runLauncherInitialize(timeoutMs = 15000, extraEnv = {}, cwd = REPO_ROOT) {
37
- return new Promise((resolve, reject) => {
38
- const child = spawn(process.execPath, [LAUNCHER], {
39
- stdio: ['pipe', 'pipe', 'pipe'],
40
- env: { ...process.env, ...extraEnv },
41
- cwd,
42
- });
43
-
44
- let stdout = '';
45
- let stderr = '';
46
- const timer = setTimeout(() => {
47
- child.kill('SIGTERM');
48
- reject(new Error(`launcher timed out after ${timeoutMs}ms; stdout=${stdout.slice(0, 400)} stderr=${stderr.slice(0, 400)}`));
49
- }, timeoutMs);
50
-
51
- child.stdout.on('data', (d) => {
52
- stdout += d.toString();
53
- if (stdout.includes('"result"') || stdout.includes('"error"')) {
54
- clearTimeout(timer);
55
- child.kill('SIGTERM');
56
- // Wait for the child to actually exit so the test doesn't leave an
57
- // orphan mid-write (matters on macOS / Windows where SIGTERM
58
- // delivery is less synchronous than on Linux).
59
- child.once('exit', () => resolve({ stdout, stderr }));
60
- }
61
- });
62
- child.stderr.on('data', (d) => { stderr += d.toString(); });
63
- child.on('error', (err) => { clearTimeout(timer); reject(err); });
64
-
65
- const initMsg = JSON.stringify({
66
- jsonrpc: '2.0', id: 1, method: 'initialize',
67
- params: {
68
- protocolVersion: '2024-11-05',
69
- capabilities: {},
70
- clientInfo: { name: 'launcher-test', version: '1.0.0' },
71
- },
72
- });
73
- child.stdin.write(initMsg + '\n');
74
- });
75
- }
76
-
77
- test('mcp-launcher resolves dev binary and forwards MCP JSON-RPC stdin/stdout', async (t) => {
78
- if (!hasBuiltBinary()) {
79
- t.skip(`release binary missing at ${REL_BINARY} — run \`cargo build --release\` first`);
80
- return;
81
- }
82
-
83
- // REPO_ROOT has its own .mcp.json registering code-graph-dev (v0.31.2
84
- // landed that to capture dev session metrics), which trips the launcher's
85
- // dedup gate. Force the original launch path so this test still covers
86
- // it. The dedup behavior gets its own test below.
87
- const { stdout, stderr } = await runLauncherInitialize(15000, { CODE_GRAPH_FORCE_PLUGIN_MCP: '1' });
88
-
89
- // Find the JSON-RPC line in the bytes the launcher forwarded from the binary.
90
- // Stderr may contain "[code-graph] ..." breadcrumbs from the launcher; those
91
- // are diagnostic and shouldn't break the contract that stdout carries protocol.
92
- const respLine = stdout.trim().split('\n').find((l) => l.includes('"result"'));
93
- assert.ok(respLine,
94
- `expected a JSON-RPC result line on launcher stdout, got: ${stdout.slice(0, 400)} | stderr: ${stderr.slice(0, 400)}`);
95
- const resp = JSON.parse(respLine);
96
- assert.equal(resp.jsonrpc, '2.0');
97
- assert.equal(resp.id, 1);
98
- assert.ok(resp.result.serverInfo, 'response must carry serverInfo from the binary');
99
- assert.equal(resp.result.serverInfo.name, 'code-graph-mcp');
100
- });
101
-
102
- test('mcp-launcher enters dedup stub when project .mcp.json registers a code-graph server', async () => {
103
- // REPO_ROOT/.mcp.json registers code-graph-dev → dedup gate fires →
104
- // launcher serves a 0-tools stub with a distinctive serverInfo.name.
105
- // No need for the release binary; the stub is implemented in the
106
- // launcher script itself.
107
- const { stdout, stderr } = await runLauncherInitialize();
108
- const respLine = stdout.trim().split('\n').find((l) => l.includes('"result"'));
109
- assert.ok(respLine,
110
- `expected stub JSON-RPC result on stdout, got: ${stdout.slice(0, 400)} | stderr: ${stderr.slice(0, 400)}`);
111
- const resp = JSON.parse(respLine);
112
- assert.match(resp.result.serverInfo.name, /stub|dedup/i,
113
- `serverInfo.name should indicate stub mode, got ${JSON.stringify(resp.result.serverInfo)}`);
114
- assert.match(stderr, /plugin MCP serving 0 tools/,
115
- `stderr should explain the dedup, got: ${stderr.slice(0, 400)}`);
116
- });
117
-
118
- test('mcp-launcher serves 0-tool stub in a non-project cwd (no binary spawn, no index created)', async (t) => {
119
- const os = require('os');
120
- // A bare temp dir with no .git/manifest → isNonProjectCwd → the launcher
121
- // serves the 0-tool stub WITHOUT spawning the binary, so no .code-graph is
122
- // created and no `instructions` block is injected. This is the fix for the
123
- // ~2035 headless /tmp mem-lite calls that half-activated code-graph.
124
- const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'cg-launcher-nonproj-'));
125
- t.after(() => fs.rmSync(cwd, { recursive: true, force: true }));
126
-
127
- const { stdout, stderr } = await runLauncherInitialize(15000, {}, cwd);
128
- const respLine = stdout.trim().split('\n').find((l) => l.includes('"result"'));
129
- assert.ok(respLine,
130
- `expected stub JSON-RPC result on stdout, got: ${stdout.slice(0, 400)} | stderr: ${stderr.slice(0, 400)}`);
131
- const resp = JSON.parse(respLine);
132
- assert.match(resp.result.serverInfo.name, /stub/i,
133
- `serverInfo.name should indicate stub mode, got ${JSON.stringify(resp.result.serverInfo)}`);
134
- assert.equal(resp.result.instructions, undefined,
135
- 'stub initialize must NOT carry an instructions block (the ~780B NOISY tax)');
136
- assert.match(stderr, /non-project cwd/,
137
- `stderr should explain the non-project gate, got: ${stderr.slice(0, 400)}`);
138
- assert.ok(!fs.existsSync(path.join(cwd, '.code-graph')),
139
- 'must NOT create .code-graph in a non-project cwd');
140
- });
141
-
142
- test('mcp-launcher sets _FIND_BINARY_ROOT from __dirname (does not trust CLAUDE_PLUGIN_ROOT)', () => {
143
- // Static check: the source must derive _FIND_BINARY_ROOT from __dirname so a
144
- // sibling plugin's CLAUDE_PLUGIN_ROOT can't redirect us to the wrong binary.
145
- // Memory: feedback_plugin_env_isolation.md.
146
- const src = fs.readFileSync(LAUNCHER, 'utf8');
147
- assert.match(src, /_FIND_BINARY_ROOT\s*=\s*path\.resolve\(__dirname/,
148
- 'launcher must derive _FIND_BINARY_ROOT from __dirname, not CLAUDE_PLUGIN_ROOT');
149
- // And must NOT read CLAUDE_PLUGIN_ROOT from env.
150
- assert.doesNotMatch(src, /process\.env\.CLAUDE_PLUGIN_ROOT/,
151
- 'launcher must not trust CLAUDE_PLUGIN_ROOT — it can leak from sibling plugins');
152
- });
153
-
154
- test('mcp-launcher rejects executable-permission failure with platform-specific hint', () => {
155
- // Static check: the macOS quarantine guard must surface xattr/chmod fix
156
- // commands rather than silently failing on the spawn.
157
- const src = fs.readFileSync(LAUNCHER, 'utf8');
158
- assert.match(src, /accessSync\s*\(\s*binary\s*,\s*fs\.constants\.X_OK\s*\)/,
159
- 'launcher must pre-check binary X_OK before spawn');
160
- assert.match(src, /xattr -d com\.apple\.quarantine/,
161
- 'macOS guard must surface the xattr removal command in stderr');
162
- });
@@ -1,207 +0,0 @@
1
- 'use strict';
2
- const test = require('node:test');
3
- const assert = require('node:assert/strict');
4
- const { PassThrough } = require('stream');
5
- const { EventEmitter } = require('events');
6
- const { serveEmptyMcpStub, SENTINEL_ID } = require('./mcp-stub');
7
-
8
- const tick = () => new Promise((r) => setImmediate(r));
9
-
10
- // A fake child_process: collect what the launcher writes to child.stdin, and
11
- // let the test push child.stdout lines + emit exit/error.
12
- function makeFakeChild() {
13
- const stdin = new PassThrough();
14
- const stdout = new PassThrough();
15
- const ee = new EventEmitter();
16
- const linesToChild = [];
17
- let sbuf = '';
18
- stdin.setEncoding('utf8');
19
- stdin.on('data', (c) => {
20
- sbuf += c;
21
- let nl;
22
- while ((nl = sbuf.indexOf('\n')) >= 0) { linesToChild.push(sbuf.slice(0, nl)); sbuf = sbuf.slice(nl + 1); }
23
- });
24
- return {
25
- stdin, stdout,
26
- on: (e, cb) => ee.on(e, cb),
27
- emit: (e, ...a) => ee.emit(e, ...a),
28
- linesToChild,
29
- };
30
- }
31
-
32
- function makeRig(upgrade) {
33
- const input = new PassThrough();
34
- const out = [];
35
- const output = { write: (s) => { out.push(s); return true; } };
36
- let exitCode = null;
37
- const handle = serveEmptyMcpStub({
38
- input, output,
39
- setInterval: () => 0, clearInterval: () => {}, // no real timer; drive attemptUpgrade() manually
40
- exit: (c) => { exitCode = c; },
41
- upgrade,
42
- });
43
- const send = (obj) => input.write(JSON.stringify(obj) + '\n');
44
- return { input, out, handle, send, getExit: () => exitCode };
45
- }
46
-
47
- const INIT = { jsonrpc: '2.0', id: 1, method: 'initialize', params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'cc', version: '1' } } };
48
-
49
- test('permanent stub: 0 tools, listChanged:false, unknown method → -32601', async () => {
50
- const { out, send } = makeRig(null);
51
- send(INIT);
52
- send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
53
- send({ jsonrpc: '2.0', id: 3, method: 'nope/nope', params: {} });
54
- await tick();
55
- const initResp = JSON.parse(out[0]);
56
- assert.equal(initResp.result.capabilities.tools.listChanged, false);
57
- assert.match(initResp.result.serverInfo.name, /dedup/);
58
- assert.deepEqual(JSON.parse(out[1]).result.tools, []);
59
- assert.equal(JSON.parse(out[2]).error.code, -32601);
60
- });
61
-
62
- test('upgradeable stub advertises tools.listChanged:true', async () => {
63
- const { out, send } = makeRig({ shouldUpgrade: () => false, spawnReal: () => null });
64
- send(INIT);
65
- await tick();
66
- assert.equal(JSON.parse(out[0]).result.capabilities.tools.listChanged, true);
67
- });
68
-
69
- test('upgradeable stub does NOT spawn while shouldUpgrade() is false', async () => {
70
- let spawnCalls = 0;
71
- const { handle } = makeRig({ shouldUpgrade: () => false, spawnReal: () => { spawnCalls++; return null; } });
72
- handle.attemptUpgrade();
73
- await tick();
74
- assert.equal(spawnCalls, 0);
75
- assert.equal(handle._state().hasChild, false);
76
- });
77
-
78
- test('upgrade handoff: proxies to real binary + emits tools/list_changed + forwards real tools', async () => {
79
- const child = makeFakeChild();
80
- let allow = false;
81
- const { out, send, handle } = makeRig({ shouldUpgrade: () => allow, spawnReal: () => child });
82
-
83
- // 1. client initialize + tools/list while still a non-project stub
84
- send(INIT);
85
- send({ jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} });
86
- await tick();
87
- assert.equal(JSON.parse(out[0]).result.capabilities.tools.listChanged, true);
88
- assert.deepEqual(JSON.parse(out[1]).result.tools, []); // stub → empty
89
- const outLenBeforeUpgrade = out.length;
90
-
91
- // 2. cwd becomes a project → upgrade fires
92
- allow = true;
93
- handle.attemptUpgrade();
94
- await tick();
95
- assert.equal(handle._state().hasChild, true);
96
- // launcher replayed initialize to the child under the sentinel id
97
- const replay = JSON.parse(child.linesToChild[0]);
98
- assert.equal(replay.method, 'initialize');
99
- assert.equal(replay.id, SENTINEL_ID);
100
-
101
- // 3. child answers the sentinel initialize
102
- child.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: SENTINEL_ID, result: { capabilities: { tools: { listChanged: false } }, instructions: 'real server' } }) + '\n');
103
- await tick();
104
- // launcher must: send initialized to child, and tools/list_changed to client
105
- assert.ok(child.linesToChild.some((l) => JSON.parse(l).method === 'notifications/initialized'));
106
- const newClientMsgs = out.slice(outLenBeforeUpgrade).map((s) => JSON.parse(s));
107
- assert.ok(newClientMsgs.some((m) => m.method === 'notifications/tools/list_changed'),
108
- 'client must be told tools changed');
109
- // the sentinel init reply must NOT leak to the client
110
- assert.ok(!newClientMsgs.some((m) => m.id === SENTINEL_ID), 'sentinel reply must be swallowed');
111
-
112
- // 4. client re-requests tools/list → forwarded to child (not answered empty)
113
- const childLinesBefore = child.linesToChild.length;
114
- send({ jsonrpc: '2.0', id: 3, method: 'tools/list', params: {} });
115
- await tick();
116
- const forwarded = child.linesToChild.slice(childLinesBefore).map((l) => JSON.parse(l));
117
- assert.ok(forwarded.some((m) => m.id === 3 && m.method === 'tools/list'), 'tools/list forwarded to real binary');
118
-
119
- // 5. child returns real tools → forwarded verbatim to client
120
- child.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: 3, result: { tools: [{ name: 'get_call_graph' }, { name: 'semantic_code_search' }] } }) + '\n');
121
- await tick();
122
- const toolResp = out.map((s) => JSON.parse(s)).find((m) => m.id === 3);
123
- assert.ok(toolResp, 'client received a tools/list response');
124
- assert.deepEqual(toolResp.result.tools.map((t) => t.name), ['get_call_graph', 'semantic_code_search']);
125
- });
126
-
127
- test('upgrade with unresolvable binary stays a stub and keeps answering', async () => {
128
- let allow = true;
129
- const { out, send, handle } = makeRig({ shouldUpgrade: () => allow, spawnReal: () => null });
130
- send(INIT);
131
- await tick();
132
- handle.attemptUpgrade(); // spawnReal returns null → no child
133
- await tick();
134
- assert.equal(handle._state().hasChild, false);
135
- send({ jsonrpc: '2.0', id: 9, method: 'tools/list', params: {} });
136
- await tick();
137
- const resp = out.map((s) => JSON.parse(s)).find((m) => m.id === 9);
138
- assert.deepEqual(resp.result.tools, []); // still served by the stub
139
- });
140
-
141
- test('upgrade: a request in the handoff window is queued then flushed to the child in order', async () => {
142
- const child = makeFakeChild();
143
- const { send, handle } = makeRig({ shouldUpgrade: () => true, spawnReal: () => child });
144
- send(INIT);
145
- await tick();
146
- handle.attemptUpgrade(); // child spawned; sentinel initialize sent; NOT ready yet
147
- await tick();
148
- // client sends a call while the child is mid-handshake → must be queued, not forwarded
149
- send({ jsonrpc: '2.0', id: 7, method: 'tools/call', params: { name: 'x' } });
150
- await tick();
151
- assert.equal(child.linesToChild.length, 1);
152
- assert.equal(JSON.parse(child.linesToChild[0]).id, SENTINEL_ID);
153
- // child completes its handshake → queued request flushes AFTER initialized
154
- child.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: SENTINEL_ID, result: { capabilities: {} } }) + '\n');
155
- await tick();
156
- const methods = child.linesToChild.map((l) => { const m = JSON.parse(l); return m.method || `id:${m.id}`; });
157
- assert.deepEqual(methods, ['initialize', 'notifications/initialized', 'tools/call']);
158
- });
159
-
160
- test('upgrade: child that exits before ready falls back to stub and answers queued requests', async () => {
161
- const child = makeFakeChild();
162
- const { out, send, handle } = makeRig({ shouldUpgrade: () => true, spawnReal: () => child });
163
- send(INIT);
164
- await tick();
165
- handle.attemptUpgrade(); // child spawned, not ready
166
- await tick();
167
- send({ jsonrpc: '2.0', id: 8, method: 'tools/list', params: {} }); // queued during handoff
168
- await tick();
169
- child.emit('exit', 1, null); // child dies BEFORE the sentinel reply
170
- await tick();
171
- assert.equal(handle._state().hasChild, false); // reverted to stub
172
- const resp = out.map((s) => JSON.parse(s)).find((m) => m.id === 8);
173
- assert.ok(resp, 'queued request got a stub answer (client not left hanging)');
174
- assert.deepEqual(resp.result.tools, []);
175
- });
176
-
177
- test('upgrade: error on a READY child exits instead of reverting to a flapping stub', async () => {
178
- const child = makeFakeChild();
179
- const { send, handle, getExit } = makeRig({ shouldUpgrade: () => true, spawnReal: () => child });
180
- send(INIT);
181
- await tick();
182
- handle.attemptUpgrade();
183
- await tick();
184
- child.stdout.write(JSON.stringify({ jsonrpc: '2.0', id: SENTINEL_ID, result: { capabilities: {} } }) + '\n');
185
- await tick();
186
- assert.equal(handle._state().childReady, true);
187
- child.emit('error', new Error('boom')); // a READY child errors
188
- await tick();
189
- assert.equal(handle._state().hasChild, true, 'must NOT revert to stub (that would flap)');
190
- assert.equal(getExit(), 1, 'ready-child error exits the launcher like the binary died');
191
- });
192
-
193
- test('poller is wired to attemptUpgrade at the default interval', () => {
194
- const input = new PassThrough();
195
- const out = [];
196
- let captured = null, spawnCalls = 0;
197
- serveEmptyMcpStub({
198
- input, output: { write: (s) => out.push(s) },
199
- setInterval: (fn, ms) => { captured = { fn, ms }; return 1; },
200
- clearInterval: () => {}, exit: () => {},
201
- upgrade: { shouldUpgrade: () => true, spawnReal: () => { spawnCalls++; return null; } },
202
- });
203
- assert.ok(captured, 'poller started via setInterval');
204
- assert.equal(captured.ms, 4000); // DEFAULT_POLL_MS
205
- captured.fn(); // simulate one poll tick
206
- assert.equal(spawnCalls, 1); // tick attempted the upgrade
207
- });