skillrepo 3.1.1 → 3.1.2
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/README.md +4 -2
- package/package.json +1 -1
- package/src/commands/init-session-sync.mjs +307 -0
- package/src/commands/init.mjs +74 -111
- package/src/commands/session-sync-actions.mjs +92 -0
- package/src/lib/binary-locator.mjs +99 -0
- package/src/lib/cli-config.mjs +7 -72
- package/src/lib/cli-version.mjs +56 -0
- package/src/lib/global-install.mjs +387 -0
- package/src/lib/mcp-merge.mjs +16 -5
- package/src/lib/mergers/session-hook.mjs +80 -68
- package/src/lib/transient-runners.mjs +204 -0
- package/src/test/commands/init.test.mjs +662 -1
- package/src/test/commands/session-sync-actions.test.mjs +74 -0
- package/src/test/helpers/mock-spawn.mjs +121 -0
- package/src/test/lib/cli-config.test.mjs +66 -9
- package/src/test/lib/cli-version.test.mjs +47 -0
- package/src/test/lib/global-install.test.mjs +424 -0
- package/src/test/lib/mcp-merge.test.mjs +3 -3
- package/src/test/lib/transient-runners.test.mjs +270 -0
- package/src/test/mergers/session-hook.test.mjs +284 -14
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for src/lib/global-install.mjs (#894 / v3.1.2).
|
|
3
|
+
*
|
|
4
|
+
* These cover the auto-install-global helper that init.mjs step 6
|
|
5
|
+
* uses under npx. The spawn function is injected via the `spawn`
|
|
6
|
+
* option so no test ever shells out to npm.
|
|
7
|
+
*
|
|
8
|
+
* Categories:
|
|
9
|
+
* 1. Spawn invocation shape (POSIX vs Windows command name,
|
|
10
|
+
* args, stdio).
|
|
11
|
+
* 2. Version pinning (always `skillrepo@<version>`).
|
|
12
|
+
* 3. Result enum coverage:
|
|
13
|
+
* success | eacces | enoent-npm | npm-nonzero | timeout |
|
|
14
|
+
* path-not-updated.
|
|
15
|
+
* 4. Output mode: inherit vs silent (stdio shape differs).
|
|
16
|
+
* 5. resolveGlobalBinary: filters _npx cache paths.
|
|
17
|
+
* 6. Programmer-error throws (missing version arg).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { describe, it, beforeEach, afterEach } from "node:test";
|
|
21
|
+
import assert from "node:assert/strict";
|
|
22
|
+
import {
|
|
23
|
+
installSkillrepoGlobally,
|
|
24
|
+
resolveGlobalBinary,
|
|
25
|
+
} from "../../lib/global-install.mjs";
|
|
26
|
+
import { makeMockSpawn } from "../helpers/mock-spawn.mjs";
|
|
27
|
+
|
|
28
|
+
// ── installSkillrepoGlobally — spawn invocation shape ────────────
|
|
29
|
+
|
|
30
|
+
describe("installSkillrepoGlobally — spawn shape", () => {
|
|
31
|
+
it("invokes `npm install -g skillrepo@<version>` on POSIX", async () => {
|
|
32
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
33
|
+
// Pass platform: "linux" to force POSIX shape regardless of host.
|
|
34
|
+
await installSkillrepoGlobally({
|
|
35
|
+
version: "3.1.2",
|
|
36
|
+
spawn,
|
|
37
|
+
platform: "linux",
|
|
38
|
+
});
|
|
39
|
+
assert.equal(spawn.calls.length, 1);
|
|
40
|
+
assert.equal(spawn.calls[0].cmd, "npm");
|
|
41
|
+
assert.deepEqual(spawn.calls[0].args, [
|
|
42
|
+
"install",
|
|
43
|
+
"-g",
|
|
44
|
+
"skillrepo@3.1.2",
|
|
45
|
+
]);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("invokes `npm.cmd install -g skillrepo@<version>` on Windows", async () => {
|
|
49
|
+
// The Windows code path must use `npm.cmd` because npm on
|
|
50
|
+
// Windows is a batch script, not a native binary. spawn() with
|
|
51
|
+
// `shell: false` requires the literal name on disk. This test
|
|
52
|
+
// is the regression guard for that platform branch.
|
|
53
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
54
|
+
await installSkillrepoGlobally({
|
|
55
|
+
version: "3.1.2",
|
|
56
|
+
spawn,
|
|
57
|
+
platform: "win32",
|
|
58
|
+
});
|
|
59
|
+
assert.equal(spawn.calls[0].cmd, "npm.cmd");
|
|
60
|
+
assert.deepEqual(spawn.calls[0].args, [
|
|
61
|
+
"install",
|
|
62
|
+
"-g",
|
|
63
|
+
"skillrepo@3.1.2",
|
|
64
|
+
]);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("uses stdio: 'inherit' by default so npm output streams to terminal", async () => {
|
|
68
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
69
|
+
await installSkillrepoGlobally({
|
|
70
|
+
version: "3.1.2",
|
|
71
|
+
spawn,
|
|
72
|
+
platform: "linux",
|
|
73
|
+
});
|
|
74
|
+
assert.equal(spawn.calls[0].opts.stdio, "inherit");
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it("uses stdio: ['ignore','pipe','pipe'] when outputMode is 'silent'", async () => {
|
|
78
|
+
// Silent mode is for --json: npm output must NOT touch the
|
|
79
|
+
// process stdout (which is reserved for the JSON blob), but
|
|
80
|
+
// stderr is captured for failure-message extraction.
|
|
81
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
82
|
+
await installSkillrepoGlobally({
|
|
83
|
+
version: "3.1.2",
|
|
84
|
+
spawn,
|
|
85
|
+
platform: "linux",
|
|
86
|
+
outputMode: "silent",
|
|
87
|
+
});
|
|
88
|
+
assert.deepEqual(spawn.calls[0].opts.stdio, ["ignore", "pipe", "pipe"]);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it("pins the version exactly — no extra suffixes or flags", async () => {
|
|
92
|
+
// Lock in that the spawn args are exactly what we expect, with
|
|
93
|
+
// no future drift to e.g. `--save-exact` or `--no-fund` slipping
|
|
94
|
+
// in. The npm command is intentionally minimal.
|
|
95
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
96
|
+
await installSkillrepoGlobally({
|
|
97
|
+
version: "3.1.2",
|
|
98
|
+
spawn,
|
|
99
|
+
platform: "linux",
|
|
100
|
+
});
|
|
101
|
+
assert.deepEqual(spawn.calls[0].args, [
|
|
102
|
+
"install",
|
|
103
|
+
"-g",
|
|
104
|
+
"skillrepo@3.1.2",
|
|
105
|
+
]);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it("uses the version arg verbatim (no normalization)", async () => {
|
|
109
|
+
// Defense: a future caller passing a version like "3.1.2-rc.1"
|
|
110
|
+
// or "next" must get exactly that string in the npm spec, not
|
|
111
|
+
// a normalized form. This is the contract: caller controls the
|
|
112
|
+
// version string; the helper just embeds it.
|
|
113
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
114
|
+
await installSkillrepoGlobally({
|
|
115
|
+
version: "3.2.0-rc.1",
|
|
116
|
+
spawn,
|
|
117
|
+
platform: "linux",
|
|
118
|
+
});
|
|
119
|
+
assert.equal(spawn.calls[0].args[2], "skillrepo@3.2.0-rc.1");
|
|
120
|
+
});
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
// ── installSkillrepoGlobally — programmer error throws ──────────
|
|
124
|
+
|
|
125
|
+
describe("installSkillrepoGlobally — programmer errors", () => {
|
|
126
|
+
it("throws when version is missing", async () => {
|
|
127
|
+
await assert.rejects(
|
|
128
|
+
() => installSkillrepoGlobally({ spawn: makeMockSpawn() }),
|
|
129
|
+
/version.*non-empty string/,
|
|
130
|
+
);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it("throws when version is an empty string", async () => {
|
|
134
|
+
await assert.rejects(
|
|
135
|
+
() => installSkillrepoGlobally({ version: "", spawn: makeMockSpawn() }),
|
|
136
|
+
/version.*non-empty string/,
|
|
137
|
+
);
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("throws when version is not a string", async () => {
|
|
141
|
+
await assert.rejects(
|
|
142
|
+
() =>
|
|
143
|
+
installSkillrepoGlobally({
|
|
144
|
+
version: 312,
|
|
145
|
+
spawn: makeMockSpawn(),
|
|
146
|
+
}),
|
|
147
|
+
/version.*non-empty string/,
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// ── installSkillrepoGlobally — result enum ──────────────────────
|
|
153
|
+
//
|
|
154
|
+
// resolveGlobalBinary is called by installSkillrepoGlobally on the
|
|
155
|
+
// success path to verify the binary actually landed on PATH. In
|
|
156
|
+
// these tests we DON'T have a real `skillrepo` to find — so the
|
|
157
|
+
// "happy" tests below assert errorCode === "path-not-updated"
|
|
158
|
+
// rather than success === true. The integration tests in
|
|
159
|
+
// init.test.mjs exercise the full happy path with a shim on PATH.
|
|
160
|
+
|
|
161
|
+
describe("installSkillrepoGlobally — failure categorization", () => {
|
|
162
|
+
it("returns errorCode='eacces' when stderr contains EACCES", async () => {
|
|
163
|
+
const spawn = makeMockSpawn({
|
|
164
|
+
exitCode: 243,
|
|
165
|
+
stderrText:
|
|
166
|
+
"npm ERR! code EACCES\nnpm ERR! Error: EACCES: permission denied",
|
|
167
|
+
});
|
|
168
|
+
const result = await installSkillrepoGlobally({
|
|
169
|
+
version: "3.1.2",
|
|
170
|
+
spawn,
|
|
171
|
+
platform: "linux",
|
|
172
|
+
outputMode: "silent",
|
|
173
|
+
});
|
|
174
|
+
assert.equal(result.success, false);
|
|
175
|
+
assert.equal(result.errorCode, "eacces");
|
|
176
|
+
assert.match(result.error, /permissions/i);
|
|
177
|
+
assert.match(result.error, /sudo/i);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns errorCode='enoent-npm' when spawn fires error with code ENOENT", async () => {
|
|
181
|
+
const enoent = Object.assign(new Error("spawn npm ENOENT"), {
|
|
182
|
+
code: "ENOENT",
|
|
183
|
+
});
|
|
184
|
+
const spawn = makeMockSpawn({ error: enoent });
|
|
185
|
+
const result = await installSkillrepoGlobally({
|
|
186
|
+
version: "3.1.2",
|
|
187
|
+
spawn,
|
|
188
|
+
platform: "linux",
|
|
189
|
+
});
|
|
190
|
+
assert.equal(result.success, false);
|
|
191
|
+
assert.equal(result.errorCode, "enoent-npm");
|
|
192
|
+
assert.match(result.error, /npm.*not found/i);
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it("returns errorCode='npm-nonzero' for generic non-zero exits", async () => {
|
|
196
|
+
const spawn = makeMockSpawn({
|
|
197
|
+
exitCode: 1,
|
|
198
|
+
stderrText: "npm ERR! 404 Not Found - GET https://registry...",
|
|
199
|
+
});
|
|
200
|
+
const result = await installSkillrepoGlobally({
|
|
201
|
+
version: "3.1.2",
|
|
202
|
+
spawn,
|
|
203
|
+
platform: "linux",
|
|
204
|
+
outputMode: "silent",
|
|
205
|
+
});
|
|
206
|
+
assert.equal(result.success, false);
|
|
207
|
+
assert.equal(result.errorCode, "npm-nonzero");
|
|
208
|
+
assert.match(result.error, /exited with code 1/);
|
|
209
|
+
// First ~200 chars of stderr should be included for diagnosis.
|
|
210
|
+
assert.match(result.error, /404 Not Found/);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("includes stderr snippet when in silent mode", async () => {
|
|
214
|
+
const spawn = makeMockSpawn({
|
|
215
|
+
exitCode: 1,
|
|
216
|
+
stderrText: "specific-failure-marker-12345",
|
|
217
|
+
});
|
|
218
|
+
const result = await installSkillrepoGlobally({
|
|
219
|
+
version: "3.1.2",
|
|
220
|
+
spawn,
|
|
221
|
+
platform: "linux",
|
|
222
|
+
outputMode: "silent",
|
|
223
|
+
});
|
|
224
|
+
assert.match(result.error, /specific-failure-marker-12345/);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it("returns errorCode='timeout' when child does not complete in time", async () => {
|
|
228
|
+
// Use a hanging spawn + a very short timeout. The result must
|
|
229
|
+
// come back within ~100ms to keep the suite fast.
|
|
230
|
+
const spawn = makeMockSpawn({ hang: true });
|
|
231
|
+
const result = await installSkillrepoGlobally({
|
|
232
|
+
version: "3.1.2",
|
|
233
|
+
spawn,
|
|
234
|
+
platform: "linux",
|
|
235
|
+
timeoutMs: 50,
|
|
236
|
+
});
|
|
237
|
+
assert.equal(result.success, false);
|
|
238
|
+
assert.equal(result.errorCode, "timeout");
|
|
239
|
+
assert.match(result.error, /did not complete/i);
|
|
240
|
+
// The mock's kill should have been invoked.
|
|
241
|
+
assert.equal(spawn.killed, true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("returns errorCode='path-not-updated' when npm exits 0 but no binary on PATH", async () => {
|
|
245
|
+
// No shim on PATH → resolveGlobalBinary returns null → the
|
|
246
|
+
// helper categorizes as path-not-updated. This is the nvm-
|
|
247
|
+
// misconfiguration case: install succeeded, but the user's
|
|
248
|
+
// npm prefix bin dir isn't on PATH so the binary is invisible
|
|
249
|
+
// to the hook runner.
|
|
250
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
251
|
+
// Save and clear PATH so resolveGlobalBinary genuinely finds
|
|
252
|
+
// nothing — even if the developer happens to have a global
|
|
253
|
+
// skillrepo installed.
|
|
254
|
+
const originalPath = process.env.PATH;
|
|
255
|
+
process.env.PATH = "/nonexistent/dir";
|
|
256
|
+
try {
|
|
257
|
+
const result = await installSkillrepoGlobally({
|
|
258
|
+
version: "3.1.2",
|
|
259
|
+
spawn,
|
|
260
|
+
platform: "linux",
|
|
261
|
+
});
|
|
262
|
+
assert.equal(result.success, false);
|
|
263
|
+
assert.equal(result.errorCode, "path-not-updated");
|
|
264
|
+
assert.match(result.error, /not found on PATH/);
|
|
265
|
+
} finally {
|
|
266
|
+
if (originalPath === undefined) {
|
|
267
|
+
delete process.env.PATH;
|
|
268
|
+
} else {
|
|
269
|
+
process.env.PATH = originalPath;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it("never throws on user-recoverable failures", async () => {
|
|
275
|
+
// Defensive: every failure path must return a result, not
|
|
276
|
+
// throw. The init flow depends on this — a thrown exception
|
|
277
|
+
// here would abort init, which is the wrong behavior because
|
|
278
|
+
// the rest of init succeeded.
|
|
279
|
+
const cases = [
|
|
280
|
+
{ exitCode: 1 },
|
|
281
|
+
{
|
|
282
|
+
error: Object.assign(new Error("ENOENT"), { code: "ENOENT" }),
|
|
283
|
+
},
|
|
284
|
+
{ exitCode: 243, stderrText: "EACCES" },
|
|
285
|
+
{ hang: true },
|
|
286
|
+
];
|
|
287
|
+
for (const opts of cases) {
|
|
288
|
+
const spawn = makeMockSpawn(opts);
|
|
289
|
+
const result = await installSkillrepoGlobally({
|
|
290
|
+
version: "3.1.2",
|
|
291
|
+
spawn,
|
|
292
|
+
platform: "linux",
|
|
293
|
+
timeoutMs: 50,
|
|
294
|
+
outputMode: "silent",
|
|
295
|
+
});
|
|
296
|
+
assert.equal(typeof result, "object");
|
|
297
|
+
assert.equal(result.success, false);
|
|
298
|
+
assert.ok(result.errorCode, `errorCode set for ${JSON.stringify(opts)}`);
|
|
299
|
+
assert.ok(result.error, `error set for ${JSON.stringify(opts)}`);
|
|
300
|
+
}
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
// ── installSkillrepoGlobally — happy path with binary on PATH ──
|
|
305
|
+
|
|
306
|
+
describe("installSkillrepoGlobally — happy path", () => {
|
|
307
|
+
// Use installShim from skillrepo-shim helper to put a fake
|
|
308
|
+
// skillrepo on PATH. After the mock spawn succeeds,
|
|
309
|
+
// resolveGlobalBinary should find the shim and return success.
|
|
310
|
+
// (PATH is restored by uninstallShim — no separate originalPath
|
|
311
|
+
// capture needed.)
|
|
312
|
+
|
|
313
|
+
let shim;
|
|
314
|
+
|
|
315
|
+
beforeEach(async () => {
|
|
316
|
+
const { installShim } = await import("../helpers/skillrepo-shim.mjs");
|
|
317
|
+
const { mkdtempSync } = await import("node:fs");
|
|
318
|
+
const { tmpdir } = await import("node:os");
|
|
319
|
+
const { join } = await import("node:path");
|
|
320
|
+
const sandbox = mkdtempSync(join(tmpdir(), "sr-globalinstall-"));
|
|
321
|
+
shim = installShim(sandbox);
|
|
322
|
+
// Save the sandbox for cleanup
|
|
323
|
+
shim.sandbox = sandbox;
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
afterEach(async () => {
|
|
327
|
+
const { uninstallShim } = await import("../helpers/skillrepo-shim.mjs");
|
|
328
|
+
const { rmSync } = await import("node:fs");
|
|
329
|
+
uninstallShim(shim);
|
|
330
|
+
if (shim?.sandbox) rmSync(shim.sandbox, { recursive: true, force: true });
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it("returns success with binaryPath when npm exits 0 and binary is on PATH", async () => {
|
|
334
|
+
const spawn = makeMockSpawn({ exitCode: 0 });
|
|
335
|
+
const result = await installSkillrepoGlobally({
|
|
336
|
+
version: "3.1.2",
|
|
337
|
+
spawn,
|
|
338
|
+
});
|
|
339
|
+
assert.equal(result.success, true, `expected success, got ${JSON.stringify(result)}`);
|
|
340
|
+
assert.ok(
|
|
341
|
+
result.binaryPath,
|
|
342
|
+
"binaryPath must be set on success",
|
|
343
|
+
);
|
|
344
|
+
// The resolved path should be under our sandbox. We check the
|
|
345
|
+
// sandbox's UNIQUE BASENAME (e.g. `sr-globalinstall-XYZ`)
|
|
346
|
+
// rather than `.startsWith(shim.binDir)` because Windows can
|
|
347
|
+
// hand back two different forms of the SAME directory: 8.3
|
|
348
|
+
// short-name (`C:\Users\RUNNER~1\...`) from `os.tmpdir()` vs
|
|
349
|
+
// long-name (`C:\Users\runneradmin\...`) from `where.exe`.
|
|
350
|
+
// The basename is invariant — `mkdtempSync`'s random suffix
|
|
351
|
+
// doesn't have a short-name alias.
|
|
352
|
+
const { basename } = await import("node:path");
|
|
353
|
+
const sandboxName = basename(shim.sandbox);
|
|
354
|
+
assert.ok(
|
|
355
|
+
result.binaryPath.includes(sandboxName) &&
|
|
356
|
+
result.binaryPath.includes("skillrepo"),
|
|
357
|
+
`expected binaryPath to contain sandbox ${sandboxName} and "skillrepo", got ${result.binaryPath}`,
|
|
358
|
+
);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// ── resolveGlobalBinary — npx-cache filtering ───────────────────
|
|
363
|
+
|
|
364
|
+
describe("resolveGlobalBinary — transient-cache filtering", () => {
|
|
365
|
+
// The cache-filter logic is integration-tested via the production
|
|
366
|
+
// path (`installSkillrepoGlobally` happy-path test below confirms
|
|
367
|
+
// a stable shim is found through the filter). This describe block
|
|
368
|
+
// covers the negative case: when only transient cache binaries
|
|
369
|
+
// are on PATH, return null.
|
|
370
|
+
|
|
371
|
+
let originalPath;
|
|
372
|
+
beforeEach(() => {
|
|
373
|
+
originalPath = process.env.PATH;
|
|
374
|
+
process.env.PATH = "/nonexistent/dir";
|
|
375
|
+
});
|
|
376
|
+
afterEach(() => {
|
|
377
|
+
if (originalPath === undefined) delete process.env.PATH;
|
|
378
|
+
else process.env.PATH = originalPath;
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it("returns null when no skillrepo is on PATH", () => {
|
|
382
|
+
assert.equal(resolveGlobalBinary(), null);
|
|
383
|
+
});
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
// ── resolveGlobalBinary — happy path with shim ─────────────────
|
|
387
|
+
|
|
388
|
+
describe("resolveGlobalBinary — happy path", () => {
|
|
389
|
+
// PATH is restored by uninstallShim in afterEach — no separate
|
|
390
|
+
// originalPath capture needed.
|
|
391
|
+
let shim;
|
|
392
|
+
let sandbox;
|
|
393
|
+
|
|
394
|
+
beforeEach(async () => {
|
|
395
|
+
const { installShim } = await import("../helpers/skillrepo-shim.mjs");
|
|
396
|
+
const { mkdtempSync } = await import("node:fs");
|
|
397
|
+
const { tmpdir } = await import("node:os");
|
|
398
|
+
const { join } = await import("node:path");
|
|
399
|
+
sandbox = mkdtempSync(join(tmpdir(), "sr-resolveglobal-"));
|
|
400
|
+
shim = installShim(sandbox);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
afterEach(async () => {
|
|
404
|
+
const { uninstallShim } = await import("../helpers/skillrepo-shim.mjs");
|
|
405
|
+
const { rmSync } = await import("node:fs");
|
|
406
|
+
uninstallShim(shim);
|
|
407
|
+
if (sandbox) rmSync(sandbox, { recursive: true, force: true });
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
it("finds a stable global binary on PATH and returns its absolute path", async () => {
|
|
411
|
+
const result = resolveGlobalBinary();
|
|
412
|
+
assert.ok(result, "expected non-null binary path");
|
|
413
|
+
// Same Windows 8.3-vs-long-name reasoning as the
|
|
414
|
+
// `installSkillrepoGlobally — happy path` test above. Compare
|
|
415
|
+
// by the unique random sandbox basename rather than the full
|
|
416
|
+
// path prefix.
|
|
417
|
+
const { basename } = await import("node:path");
|
|
418
|
+
const sandboxName = basename(sandbox);
|
|
419
|
+
assert.ok(
|
|
420
|
+
result.includes(sandboxName) && result.includes("skillrepo"),
|
|
421
|
+
`expected resolved path to contain sandbox ${sandboxName} and "skillrepo", got ${result}`,
|
|
422
|
+
);
|
|
423
|
+
});
|
|
424
|
+
});
|
|
@@ -200,7 +200,7 @@ describe("mergeMcpForVendors — user declined", () => {
|
|
|
200
200
|
mcpUrl: "https://x.com/mcp",
|
|
201
201
|
yes: false, // NOT --yes — will call confirmFn
|
|
202
202
|
io: { stdout, stderr },
|
|
203
|
-
confirmFn,
|
|
203
|
+
deps: { confirmFn },
|
|
204
204
|
});
|
|
205
205
|
|
|
206
206
|
assert.equal(results[0].outcome, "skipped");
|
|
@@ -216,7 +216,7 @@ describe("mergeMcpForVendors — user declined", () => {
|
|
|
216
216
|
mcpUrl: "https://x.com/mcp",
|
|
217
217
|
yes: false,
|
|
218
218
|
io: { stdout, stderr },
|
|
219
|
-
confirmFn,
|
|
219
|
+
deps: { confirmFn },
|
|
220
220
|
});
|
|
221
221
|
assert.equal(results[0].outcome, "merged");
|
|
222
222
|
assert.equal(results[1].outcome, "skipped");
|
|
@@ -282,7 +282,7 @@ describe("mergeMcpForVendors — failure handling", () => {
|
|
|
282
282
|
mcpUrl: "https://x.com/mcp",
|
|
283
283
|
yes: false,
|
|
284
284
|
io: { stdout, stderr },
|
|
285
|
-
confirmFn,
|
|
285
|
+
deps: { confirmFn },
|
|
286
286
|
});
|
|
287
287
|
|
|
288
288
|
assert.equal(results.length, 3);
|