@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,474 +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 crypto = require('crypto');
|
|
8
|
-
|
|
9
|
-
const {
|
|
10
|
-
commandExists,
|
|
11
|
-
fetchLatestRelease,
|
|
12
|
-
getExtractedPluginVersion,
|
|
13
|
-
parseLatestRelease,
|
|
14
|
-
readBinaryVersion,
|
|
15
|
-
promoteVerifiedBinary,
|
|
16
|
-
cachedBinaryPath,
|
|
17
|
-
cachedBinaryNeedsUpdate,
|
|
18
|
-
cachedBinaryStaleVsState,
|
|
19
|
-
downloadBinary,
|
|
20
|
-
selfHealStaleBinary,
|
|
21
|
-
isInstallMissingMode,
|
|
22
|
-
isSilentMode,
|
|
23
|
-
shouldCheck,
|
|
24
|
-
} = require('./auto-update');
|
|
25
|
-
|
|
26
|
-
function mkDir(t, prefix) {
|
|
27
|
-
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
28
|
-
t.after(() => fs.rmSync(dir, { recursive: true, force: true }));
|
|
29
|
-
return dir;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
test('getExtractedPluginVersion reads extracted plugin manifest version', (t) => {
|
|
33
|
-
const root = mkDir(t, 'code-graph-plugin-');
|
|
34
|
-
const manifest = path.join(root, '.claude-plugin', 'plugin.json');
|
|
35
|
-
fs.mkdirSync(path.dirname(manifest), { recursive: true });
|
|
36
|
-
fs.writeFileSync(manifest, JSON.stringify({ version: '1.2.3' }, null, 2));
|
|
37
|
-
assert.equal(getExtractedPluginVersion(root), '1.2.3');
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
function writeFakeBinary(filePath, version, mode = 0o755) {
|
|
41
|
-
const script = [
|
|
42
|
-
'#!/usr/bin/env bash',
|
|
43
|
-
'if [ "$1" = "--version" ]; then',
|
|
44
|
-
` echo "code-graph-mcp ${version}"`,
|
|
45
|
-
' exit 0',
|
|
46
|
-
'fi',
|
|
47
|
-
'exit 0',
|
|
48
|
-
`# ${'x'.repeat(1_100_000)}`,
|
|
49
|
-
'',
|
|
50
|
-
].join('\n');
|
|
51
|
-
fs.writeFileSync(filePath, script);
|
|
52
|
-
fs.chmodSync(filePath, mode);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
test('promoteVerifiedBinary accepts a runnable binary with the expected version', (t) => {
|
|
56
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
57
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
58
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
59
|
-
writeFakeBinary(tmp, '1.2.3');
|
|
60
|
-
|
|
61
|
-
assert.equal(readBinaryVersion(tmp), '1.2.3');
|
|
62
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
|
|
63
|
-
assert.equal(fs.existsSync(tmp), false);
|
|
64
|
-
assert.equal(fs.existsSync(dst), true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
test('promoteVerifiedBinary rejects binaries with mismatched version', (t) => {
|
|
68
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
69
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
70
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
71
|
-
writeFakeBinary(tmp, '1.2.2');
|
|
72
|
-
|
|
73
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), false);
|
|
74
|
-
assert.equal(fs.existsSync(tmp), false);
|
|
75
|
-
assert.equal(fs.existsSync(dst), false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
test('promoteVerifiedBinary promotes a non-executable (0644) download — curl -o regression', (t) => {
|
|
79
|
-
// `curl -o` writes the tmp file as 0644 (no exec bit). promoteVerifiedBinary
|
|
80
|
-
// must chmod before reading the version (readBinaryVersion executes the
|
|
81
|
-
// binary), otherwise the version read fails with EACCES → null → false and
|
|
82
|
-
// every download path silently wedges. Regression for the binary-stuck-at-old
|
|
83
|
-
// -version deadlock.
|
|
84
|
-
if (process.platform === 'win32') return; // no exec bit on win32
|
|
85
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
86
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
87
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
88
|
-
writeFakeBinary(tmp, '1.2.3', 0o644);
|
|
89
|
-
|
|
90
|
-
assert.equal(readBinaryVersion(tmp), null, 'precondition: 0644 binary is not executable');
|
|
91
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3'), true);
|
|
92
|
-
assert.equal(fs.existsSync(dst), true);
|
|
93
|
-
assert.equal(fs.statSync(dst).mode & 0o111, 0o111, 'promoted binary is executable');
|
|
94
|
-
assert.equal(readBinaryVersion(dst), '1.2.3');
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
test('promoteVerifiedBinary accepts a binary matching the expected sha256', (t) => {
|
|
98
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
99
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
100
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
101
|
-
writeFakeBinary(tmp, '1.2.3');
|
|
102
|
-
const sha = crypto.createHash('sha256').update(fs.readFileSync(tmp)).digest('hex');
|
|
103
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3', sha), true);
|
|
104
|
-
assert.equal(fs.existsSync(dst), true);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
test('promoteVerifiedBinary rejects a binary whose sha256 mismatches the sidecar', (t) => {
|
|
108
|
-
// Tampered/corrupted download: the checksum gate runs BEFORE chmod+exec, so a
|
|
109
|
-
// mismatched binary is refused and never made executable. Platform-independent
|
|
110
|
-
// (no exec needed to reject).
|
|
111
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
112
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
113
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
114
|
-
writeFakeBinary(tmp, '1.2.3');
|
|
115
|
-
const wrongSha = 'deadbeef'.repeat(8); // 64 hex chars, deliberately wrong
|
|
116
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3', wrongSha), false);
|
|
117
|
-
assert.equal(fs.existsSync(dst), false, 'tampered binary must not be promoted');
|
|
118
|
-
assert.equal(fs.existsSync(tmp), false, 'tmp cleaned up on rejection');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
test('promoteVerifiedBinary proceeds without a sidecar (TOFU back-compat)', (t) => {
|
|
122
|
-
// Older releases ship no <asset>.sha256; a null expected hash must not block
|
|
123
|
-
// install (the size + version-exec gates still apply).
|
|
124
|
-
const dir = mkDir(t, 'code-graph-bin-');
|
|
125
|
-
const tmp = path.join(dir, 'code-graph-mcp.tmp');
|
|
126
|
-
const dst = path.join(dir, 'code-graph-mcp');
|
|
127
|
-
writeFakeBinary(tmp, '1.2.3');
|
|
128
|
-
assert.equal(promoteVerifiedBinary(tmp, dst, '1.2.3', null), true);
|
|
129
|
-
assert.equal(fs.existsSync(dst), true);
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
test('cachedBinaryNeedsUpdate is version-aware, not existence-only', (t) => {
|
|
133
|
-
const dir = mkDir(t, 'code-graph-heal-');
|
|
134
|
-
const binaryPath = path.join(dir, 'code-graph-mcp');
|
|
135
|
-
const latest = { version: '0.45.0', binaryUrl: 'https://example.com/bin' };
|
|
136
|
-
|
|
137
|
-
// missing binary → needs update
|
|
138
|
-
assert.equal(cachedBinaryNeedsUpdate(latest, { binaryPath }), true);
|
|
139
|
-
|
|
140
|
-
// present but stale (the actual deadlock: shell at 0.45.0, binary at 0.16.6)
|
|
141
|
-
fs.writeFileSync(binaryPath, 'x');
|
|
142
|
-
assert.equal(
|
|
143
|
-
cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.16.6' }),
|
|
144
|
-
true,
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
// present and current → no update
|
|
148
|
-
assert.equal(
|
|
149
|
-
cachedBinaryNeedsUpdate(latest, { binaryPath, readVersion: () => '0.45.0' }),
|
|
150
|
-
false,
|
|
151
|
-
);
|
|
152
|
-
|
|
153
|
-
// no binaryUrl / null latest → no-op (nothing to download)
|
|
154
|
-
assert.equal(cachedBinaryNeedsUpdate({ version: '0.45.0', binaryUrl: null }, { binaryPath }), false);
|
|
155
|
-
assert.equal(cachedBinaryNeedsUpdate(null, { binaryPath }), false);
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
test('cachedBinaryStaleVsState bypasses throttle only for a present-but-stale binary', (t) => {
|
|
159
|
-
const dir = mkDir(t, 'code-graph-throttle-');
|
|
160
|
-
const binaryPath = path.join(dir, 'code-graph-mcp');
|
|
161
|
-
fs.writeFileSync(binaryPath, 'x'); // present
|
|
162
|
-
|
|
163
|
-
// no prior latestVersion → don't bypass (first run fetches anyway)
|
|
164
|
-
assert.equal(cachedBinaryStaleVsState({}, { binaryPath }), false);
|
|
165
|
-
assert.equal(cachedBinaryStaleVsState(null, { binaryPath }), false);
|
|
166
|
-
|
|
167
|
-
// present + stale vs last known latest → bypass throttle (the 6h-gap fix)
|
|
168
|
-
assert.equal(
|
|
169
|
-
cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.16.6' }),
|
|
170
|
-
true,
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
// present + current → stay throttled
|
|
174
|
-
assert.equal(
|
|
175
|
-
cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath, readVersion: () => '0.45.1' }),
|
|
176
|
-
false,
|
|
177
|
-
);
|
|
178
|
-
|
|
179
|
-
// missing binary → false here (the separate binaryMissing bypass handles it)
|
|
180
|
-
fs.rmSync(binaryPath);
|
|
181
|
-
assert.equal(cachedBinaryStaleVsState({ latestVersion: '0.45.1' }, { binaryPath }), false);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
test('shouldCheck re-verifies an up-to-date state on a short cadence (release-publish race)', () => {
|
|
185
|
-
const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
|
|
186
|
-
|
|
187
|
-
// never checked → always check
|
|
188
|
-
assert.equal(shouldCheck({}), true);
|
|
189
|
-
|
|
190
|
-
// Bug repro: the last check reported "up to date" (updateAvailable:false) and a
|
|
191
|
-
// release published moments later. 45min on, the plain 6h throttle kept the
|
|
192
|
-
// stale answer latched (every session reopen re-reported up-to-date); the short
|
|
193
|
-
// up-to-date cadence must allow a re-check so the new release is discovered.
|
|
194
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: false }), true);
|
|
195
|
-
|
|
196
|
-
// within the short window → still throttled (don't hammer the API every call)
|
|
197
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
|
|
198
|
-
|
|
199
|
-
// a pending-but-unfinished update keeps the 6h steady-state interval
|
|
200
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(45), updateAvailable: true }), false);
|
|
201
|
-
|
|
202
|
-
// rate-limit backoff (24h) wins even over the up-to-date short cadence
|
|
203
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(120), updateAvailable: false, rateLimited: true }), false);
|
|
204
|
-
});
|
|
205
|
-
|
|
206
|
-
test('shouldCheck lets a forced (session-start) check bypass the soft throttle', () => {
|
|
207
|
-
const minsAgo = (m) => new Date(Date.now() - m * 60 * 1000).toISOString();
|
|
208
|
-
|
|
209
|
-
// A new session / explicit reload is a strong "get me latest" signal: a forced
|
|
210
|
-
// check runs even inside the 30min up-to-date window (contrast the non-forced
|
|
211
|
-
// call on the same state, which stays throttled).
|
|
212
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }, { force: true }), true);
|
|
213
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(10), updateAvailable: false }), false);
|
|
214
|
-
|
|
215
|
-
// ...but a short anti-hammer floor still applies, so a crash/reopen loop can't
|
|
216
|
-
// pound the GitHub API on every restart.
|
|
217
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(0.5), updateAvailable: false }, { force: true }), false);
|
|
218
|
-
|
|
219
|
-
// Rate-limit backoff wins even over force — never push more requests into a 403.
|
|
220
|
-
assert.equal(shouldCheck({ lastCheck: minsAgo(60), updateAvailable: false, rateLimited: true }, { force: true }), false);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
test('selfHealStaleBinary wires the stale-binary check to a download (the v0.45.x glue)', async () => {
|
|
224
|
-
const latest = { version: '0.45.2', binaryUrl: 'https://example/bin' };
|
|
225
|
-
|
|
226
|
-
// Field failure mode: shell already at latest, binary pinned stale → MUST download.
|
|
227
|
-
let downloaded = false;
|
|
228
|
-
const healed = await selfHealStaleBinary(latest, {
|
|
229
|
-
needsUpdate: () => true,
|
|
230
|
-
download: async () => { downloaded = true; return true; },
|
|
231
|
-
});
|
|
232
|
-
assert.equal(downloaded, true, 'stale binary must trigger a download');
|
|
233
|
-
assert.equal(healed, true);
|
|
234
|
-
|
|
235
|
-
// Binary current → no download, no-op.
|
|
236
|
-
let touched = false;
|
|
237
|
-
const noop = await selfHealStaleBinary(latest, {
|
|
238
|
-
needsUpdate: () => false,
|
|
239
|
-
download: async () => { touched = true; return true; },
|
|
240
|
-
});
|
|
241
|
-
assert.equal(touched, false, 'current binary must not download');
|
|
242
|
-
assert.equal(noop, false);
|
|
243
|
-
|
|
244
|
-
// Download fails (no curl / network) → returns false so the next session retries.
|
|
245
|
-
const failed = await selfHealStaleBinary(latest, {
|
|
246
|
-
needsUpdate: () => true,
|
|
247
|
-
download: async () => false,
|
|
248
|
-
});
|
|
249
|
-
assert.equal(failed, false);
|
|
250
|
-
});
|
|
251
|
-
|
|
252
|
-
test('parseLatestRelease selects the matching platform asset', () => {
|
|
253
|
-
const latest = parseLatestRelease({
|
|
254
|
-
tag_name: 'v1.2.3',
|
|
255
|
-
tarball_url: 'https://example.com/tarball.tgz',
|
|
256
|
-
assets: [
|
|
257
|
-
{ name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/linux-x64' },
|
|
258
|
-
{ name: 'other', browser_download_url: 'https://example.com/other' },
|
|
259
|
-
],
|
|
260
|
-
}, 'code-graph-mcp-linux-x64');
|
|
261
|
-
|
|
262
|
-
assert.deepEqual(latest, {
|
|
263
|
-
version: '1.2.3',
|
|
264
|
-
tarballUrl: 'https://example.com/tarball.tgz',
|
|
265
|
-
binaryUrl: 'https://example.com/linux-x64',
|
|
266
|
-
});
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
// ── commandExists ──────────────────────────────────────────
|
|
270
|
-
|
|
271
|
-
test('commandExists returns true for a known command (node)', () => {
|
|
272
|
-
assert.equal(commandExists('node'), true);
|
|
273
|
-
});
|
|
274
|
-
|
|
275
|
-
test('commandExists returns false for a non-existent command', () => {
|
|
276
|
-
assert.equal(commandExists('__nonexistent_cmd_xyz_12345__'), false);
|
|
277
|
-
});
|
|
278
|
-
|
|
279
|
-
test('cachedBinaryPath returns expected platform binary path', () => {
|
|
280
|
-
const p = cachedBinaryPath();
|
|
281
|
-
const expectedName = process.platform === 'win32' ? 'code-graph-mcp.exe' : 'code-graph-mcp';
|
|
282
|
-
assert.equal(path.basename(p), expectedName);
|
|
283
|
-
assert.ok(p.includes('.cache') && p.includes('code-graph'),
|
|
284
|
-
`expected cache path to live under ~/.cache/code-graph: ${p}`);
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
test('downloadBinary returns false for missing binaryUrl (no-op safety)', async () => {
|
|
288
|
-
const result = await downloadBinary({ version: '1.0.0', binaryUrl: null });
|
|
289
|
-
assert.equal(result, false);
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
test('downloadBinary returns false when latest is null', async () => {
|
|
293
|
-
const result = await downloadBinary(null);
|
|
294
|
-
assert.equal(result, false);
|
|
295
|
-
});
|
|
296
|
-
|
|
297
|
-
// ── Flag parsing ───────────────────────────────────────────
|
|
298
|
-
|
|
299
|
-
test('isInstallMissingMode detects --install-missing in argv', () => {
|
|
300
|
-
assert.equal(isInstallMissingMode(['--install-missing']), true);
|
|
301
|
-
assert.equal(isInstallMissingMode(['check', '--install-missing']), true);
|
|
302
|
-
assert.equal(isInstallMissingMode(['check']), false);
|
|
303
|
-
assert.equal(isInstallMissingMode([]), false);
|
|
304
|
-
});
|
|
305
|
-
|
|
306
|
-
test('isSilentMode honors --silent flag and CODE_GRAPH_AUTO_UPDATE_SILENT env', () => {
|
|
307
|
-
assert.equal(isSilentMode(['--silent'], {}), true);
|
|
308
|
-
assert.equal(isSilentMode([], { CODE_GRAPH_AUTO_UPDATE_SILENT: '1' }), true);
|
|
309
|
-
assert.equal(isSilentMode([], {}), false);
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
test('fetchLatestRelease parses JSON without relying on global fetch', async () => {
|
|
313
|
-
const latest = await fetchLatestRelease(async () => ({
|
|
314
|
-
statusCode: 200,
|
|
315
|
-
body: JSON.stringify({
|
|
316
|
-
tag_name: 'v2.0.0',
|
|
317
|
-
tarball_url: 'https://example.com/release.tgz',
|
|
318
|
-
assets: [
|
|
319
|
-
{ name: 'code-graph-mcp-linux-x64', browser_download_url: 'https://example.com/bin' },
|
|
320
|
-
],
|
|
321
|
-
}),
|
|
322
|
-
}));
|
|
323
|
-
|
|
324
|
-
assert.equal(latest.version, '2.0.0');
|
|
325
|
-
assert.equal(latest.tarballUrl, 'https://example.com/release.tgz');
|
|
326
|
-
});
|
|
327
|
-
// ── refreshMarketplaceClone (v0.49.1 marketplace-staleness fix) ────────────
|
|
328
|
-
|
|
329
|
-
const { execFileSync: execGit } = require('child_process');
|
|
330
|
-
const { refreshMarketplaceClone, downloadAndInstall } = require('./auto-update');
|
|
331
|
-
|
|
332
|
-
function git(cwd, ...args) {
|
|
333
|
-
return execGit('git', ['-C', cwd, '-c', 'user.email=t@t', '-c', 'user.name=t', ...args],
|
|
334
|
-
{ stdio: 'pipe', encoding: 'utf8' });
|
|
335
|
-
}
|
|
336
|
-
|
|
337
|
-
test('refreshMarketplaceClone fast-forwards a stale clone', (t) => {
|
|
338
|
-
const root = mkDir(t, 'code-graph-mp-');
|
|
339
|
-
const remote = path.join(root, 'remote');
|
|
340
|
-
const clone = path.join(root, 'clone');
|
|
341
|
-
|
|
342
|
-
fs.mkdirSync(remote);
|
|
343
|
-
git(remote, 'init', '-q', '-b', 'main');
|
|
344
|
-
fs.writeFileSync(path.join(remote, 'marketplace.json'), '{"version":"0.48.0"}');
|
|
345
|
-
git(remote, 'add', '.');
|
|
346
|
-
git(remote, 'commit', '-q', '-m', 'v0.48.0');
|
|
347
|
-
execGit('git', ['clone', '-q', remote, clone], { stdio: 'pipe' });
|
|
348
|
-
|
|
349
|
-
// Remote advances (a release bumped marketplace.json) — clone is now stale.
|
|
350
|
-
fs.writeFileSync(path.join(remote, 'marketplace.json'), '{"version":"0.49.0"}');
|
|
351
|
-
git(remote, 'commit', '-q', '-am', 'v0.49.0');
|
|
352
|
-
|
|
353
|
-
assert.equal(refreshMarketplaceClone({ dir: clone }), true);
|
|
354
|
-
assert.match(fs.readFileSync(path.join(clone, 'marketplace.json'), 'utf8'), /0\.49\.0/);
|
|
355
|
-
});
|
|
356
|
-
|
|
357
|
-
test('refreshMarketplaceClone is a safe no-op on non-git dirs and pull failures', (t) => {
|
|
358
|
-
const root = mkDir(t, 'code-graph-mp-');
|
|
359
|
-
// Not a git repo → false, no throw.
|
|
360
|
-
assert.equal(refreshMarketplaceClone({ dir: root }), false);
|
|
361
|
-
// Missing dir → false, no throw.
|
|
362
|
-
assert.equal(refreshMarketplaceClone({ dir: path.join(root, 'nope') }), false);
|
|
363
|
-
// exec throws (diverged / dirty clone) → false, no throw.
|
|
364
|
-
const fakeGitDir = path.join(root, 'repo');
|
|
365
|
-
fs.mkdirSync(path.join(fakeGitDir, '.git'), { recursive: true });
|
|
366
|
-
assert.equal(refreshMarketplaceClone({
|
|
367
|
-
dir: fakeGitDir,
|
|
368
|
-
exec: () => { throw new Error('not a fast-forward'); },
|
|
369
|
-
}), false);
|
|
370
|
-
});
|
|
371
|
-
|
|
372
|
-
test('downloadAndInstall wires the marketplace refresh + binary download (orchestration glue)', async (t) => {
|
|
373
|
-
// In-process with all side-effectful deps injected would still write the
|
|
374
|
-
// manifest into the REAL ~/.cache (CACHE_DIR is bound at module load), so
|
|
375
|
-
// run in a subprocess with a sandboxed HOME — same pattern as install-e2e.
|
|
376
|
-
const sandboxHome = mkDir(t, 'code-graph-dai-');
|
|
377
|
-
const script = `
|
|
378
|
-
const fs = require('fs');
|
|
379
|
-
const path = require('path');
|
|
380
|
-
const { downloadAndInstall } = require(${JSON.stringify(path.join(__dirname, 'auto-update.js'))});
|
|
381
|
-
const latest = { version: '9.9.9', tarballUrl: 'https://example/tar', binaryUrl: null };
|
|
382
|
-
const calls = [];
|
|
383
|
-
const exec = (cmd, args) => {
|
|
384
|
-
calls.push(cmd);
|
|
385
|
-
if (cmd === 'tar') {
|
|
386
|
-
// Simulate extraction: produce claude-plugin/ with a matching version.
|
|
387
|
-
const tmpDir = args[args.indexOf('-C') + 1];
|
|
388
|
-
const mDir = path.join(tmpDir, 'claude-plugin', '.claude-plugin');
|
|
389
|
-
fs.mkdirSync(mDir, { recursive: true });
|
|
390
|
-
fs.writeFileSync(path.join(mDir, 'plugin.json'), JSON.stringify({ version: '9.9.9' }));
|
|
391
|
-
}
|
|
392
|
-
};
|
|
393
|
-
(async () => {
|
|
394
|
-
let refreshed = 0;
|
|
395
|
-
let binDownloads = 0;
|
|
396
|
-
const result = await downloadAndInstall(latest, {
|
|
397
|
-
exec,
|
|
398
|
-
cmdExists: () => true, // don't depend on host curl/tar
|
|
399
|
-
refreshMarketplace: () => { refreshed++; return true; },
|
|
400
|
-
downloadBin: async () => { binDownloads++; return true; },
|
|
401
|
-
});
|
|
402
|
-
console.log(JSON.stringify({ result, refreshed, binDownloads, calls }));
|
|
403
|
-
})();
|
|
404
|
-
`;
|
|
405
|
-
const out = execGit(process.execPath, ['-e', script], {
|
|
406
|
-
env: { ...process.env, HOME: sandboxHome },
|
|
407
|
-
encoding: 'utf8',
|
|
408
|
-
});
|
|
409
|
-
const { result, refreshed, binDownloads } = JSON.parse(out.trim().split('\n').pop());
|
|
410
|
-
assert.equal(result.pluginUpdated, true, 'plugin files must install from the extracted tarball');
|
|
411
|
-
assert.equal(refreshed, 1, 'marketplace refresh must run exactly once after a plugin update');
|
|
412
|
-
assert.equal(result.marketplaceRefreshed, true);
|
|
413
|
-
assert.equal(binDownloads, 1, 'binary download must run');
|
|
414
|
-
assert.equal(result.binaryUpdated, true);
|
|
415
|
-
// Plugin landed in the sandboxed cache, not the real one.
|
|
416
|
-
const dst = path.join(sandboxHome, '.claude', 'plugins', 'cache',
|
|
417
|
-
'code-graph-mcp', 'code-graph-mcp', '9.9.9', '.claude-plugin', 'plugin.json');
|
|
418
|
-
assert.equal(fs.existsSync(dst), true, 'plugin copied into sandbox plugins cache');
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
test('downloadAndInstall does NOT repoint install state when the plugin copy is skipped (version drift)', async (t) => {
|
|
422
|
-
// Guard for a silent-breakage bug: when the extracted tarball's plugin.json version
|
|
423
|
-
// doesn't match latest.version, the copy is skipped and pluginDst is never created.
|
|
424
|
-
// installed_plugins.json must NOT be advanced to that nonexistent dir, or Claude Code
|
|
425
|
-
// ends up pointed at a missing install while state reads "up to date".
|
|
426
|
-
const sandboxHome = mkDir(t, 'code-graph-dai-skip-');
|
|
427
|
-
const installedDir = path.join(sandboxHome, '.claude', 'plugins');
|
|
428
|
-
fs.mkdirSync(installedDir, { recursive: true });
|
|
429
|
-
const installedPath = path.join(installedDir, 'installed_plugins.json');
|
|
430
|
-
fs.writeFileSync(installedPath, JSON.stringify({
|
|
431
|
-
plugins: { 'code-graph-mcp@code-graph-mcp': [
|
|
432
|
-
{ installPath: '/old/install/path', version: '0.0.1', lastUpdated: 'seed' },
|
|
433
|
-
] },
|
|
434
|
-
}));
|
|
435
|
-
|
|
436
|
-
const script = `
|
|
437
|
-
const fs = require('fs');
|
|
438
|
-
const path = require('path');
|
|
439
|
-
const { downloadAndInstall } = require(${JSON.stringify(path.join(__dirname, 'auto-update.js'))});
|
|
440
|
-
const latest = { version: '9.9.9', tarballUrl: 'https://example/tar', binaryUrl: null };
|
|
441
|
-
const exec = (cmd, args) => {
|
|
442
|
-
if (cmd === 'tar') {
|
|
443
|
-
// Extract a claude-plugin/ whose version DRIFTS from latest → copy is skipped.
|
|
444
|
-
const tmpDir = args[args.indexOf('-C') + 1];
|
|
445
|
-
const mDir = path.join(tmpDir, 'claude-plugin', '.claude-plugin');
|
|
446
|
-
fs.mkdirSync(mDir, { recursive: true });
|
|
447
|
-
fs.writeFileSync(path.join(mDir, 'plugin.json'), JSON.stringify({ version: '0.0.0' }));
|
|
448
|
-
}
|
|
449
|
-
};
|
|
450
|
-
(async () => {
|
|
451
|
-
const result = await downloadAndInstall(latest, {
|
|
452
|
-
exec,
|
|
453
|
-
cmdExists: () => true, // don't depend on host curl/tar — exercise the guard deterministically
|
|
454
|
-
refreshMarketplace: () => true,
|
|
455
|
-
downloadBin: async () => true,
|
|
456
|
-
});
|
|
457
|
-
console.log(JSON.stringify({ result }));
|
|
458
|
-
})();
|
|
459
|
-
`;
|
|
460
|
-
const out = execGit(process.execPath, ['-e', script], {
|
|
461
|
-
env: { ...process.env, HOME: sandboxHome },
|
|
462
|
-
encoding: 'utf8',
|
|
463
|
-
});
|
|
464
|
-
const { result } = JSON.parse(out.trim().split('\n').pop());
|
|
465
|
-
assert.equal(result.pluginUpdated, false, 'version drift must skip the plugin copy');
|
|
466
|
-
|
|
467
|
-
// The pre-seeded record must be UNTOUCHED — not repointed to the 9.9.9 dir.
|
|
468
|
-
const rec = JSON.parse(fs.readFileSync(installedPath, 'utf8'))
|
|
469
|
-
.plugins['code-graph-mcp@code-graph-mcp'][0];
|
|
470
|
-
assert.equal(rec.installPath, '/old/install/path',
|
|
471
|
-
'installPath must not be repointed when the copy was skipped');
|
|
472
|
-
assert.equal(rec.version, '0.0.1',
|
|
473
|
-
'version must not be advanced when the copy was skipped');
|
|
474
|
-
});
|