skillrepo 4.5.1 → 4.6.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/package.json +1 -1
- package/src/commands/add.mjs +2 -2
- package/src/commands/get.mjs +1 -1
- package/src/commands/list.mjs +36 -10
- package/src/lib/http.mjs +72 -4
- package/src/lib/sync.mjs +199 -4
- package/src/test/commands/list.test.mjs +14 -0
- package/src/test/e2e/mock-server.mjs +185 -2
- package/src/test/integration/update-list-contract.integration.test.mjs +575 -28
- package/src/test/lib/http.test.mjs +103 -0
- package/src/test/lib/mcp-merge.test.mjs +2 -2
- package/src/test/lib/sync.test.mjs +343 -0
- package/src/test/mergers/claude-mcp.test.mjs +4 -4
- package/src/test/mergers/uninstall-claude-mcp.test.mjs +1 -1
- package/src/test/mergers/uninstall-vscode-mcp.test.mjs +1 -1
|
@@ -80,6 +80,31 @@ export function createMockServer(initialPayload, options = {}) {
|
|
|
80
80
|
/** @type {{ status: number, body: any } | null} */
|
|
81
81
|
let forcedError = null;
|
|
82
82
|
|
|
83
|
+
// Library-request inspection. Tests use these to distinguish a true
|
|
84
|
+
// 304 wire exchange (`If-None-Match` sent, server replied 304) from
|
|
85
|
+
// a request that produced "up to date" output via some other path.
|
|
86
|
+
// PR #1575's placement-presence fix made this distinction
|
|
87
|
+
// load-bearing — see the assertion at the call sites in
|
|
88
|
+
// update-list-contract.integration.test.mjs.
|
|
89
|
+
/** @type {string | null} */
|
|
90
|
+
let lastLibraryIfNoneMatch = null;
|
|
91
|
+
/** @type {string | null} */
|
|
92
|
+
let lastLibrarySince = null;
|
|
93
|
+
let libraryRequestCount = 0;
|
|
94
|
+
// #1832 — `?manifest=1` capture for the GET /api/v1/library route so
|
|
95
|
+
// tests can assert `skillrepo list` made the non-recording manifest read.
|
|
96
|
+
/** @type {string | null} */
|
|
97
|
+
let lastLibraryManifest = null;
|
|
98
|
+
|
|
99
|
+
// #1832 — POST /api/v1/library/sync-receipts inspection + response slot.
|
|
100
|
+
// `lastReceipt` holds the most recent JSON-parsed receipt body;
|
|
101
|
+
// `receiptResponse` overrides the default 200 so tests can simulate
|
|
102
|
+
// failures (the CLI treats a failed receipt as a non-fatal warning).
|
|
103
|
+
let lastReceipt = null;
|
|
104
|
+
let receiptRequestCount = 0;
|
|
105
|
+
/** @type {{ status: number, body: any } | null} */
|
|
106
|
+
let receiptResponse = null;
|
|
107
|
+
|
|
83
108
|
// PR3a mutable slots for POST/DELETE library routes
|
|
84
109
|
//
|
|
85
110
|
// Each entry is `{ status: number, body: any }` describing the
|
|
@@ -262,6 +287,41 @@ export function createMockServer(initialPayload, options = {}) {
|
|
|
262
287
|
return;
|
|
263
288
|
}
|
|
264
289
|
|
|
290
|
+
// ── #1832: POST /api/v1/library/sync-receipts (confirmed-write) ─
|
|
291
|
+
//
|
|
292
|
+
// The CLI posts this after `runSync` writes skills to disk. Default
|
|
293
|
+
// 200 echoes a `recorded` count; tests can override via
|
|
294
|
+
// setReceiptResponse to exercise the non-fatal-warning path.
|
|
295
|
+
if (
|
|
296
|
+
url.pathname === "/api/v1/library/sync-receipts" &&
|
|
297
|
+
req.method === "POST"
|
|
298
|
+
) {
|
|
299
|
+
if (!checkAuth(req, res)) return;
|
|
300
|
+
let body;
|
|
301
|
+
try {
|
|
302
|
+
body = rawBody.length > 0 ? JSON.parse(rawBody) : {};
|
|
303
|
+
} catch {
|
|
304
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
305
|
+
res.end(
|
|
306
|
+
JSON.stringify({ error: "Body must be JSON", code: "bad_request" }),
|
|
307
|
+
);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
lastReceipt = body;
|
|
311
|
+
receiptRequestCount++;
|
|
312
|
+
if (receiptResponse) {
|
|
313
|
+
res.writeHead(receiptResponse.status, {
|
|
314
|
+
"Content-Type": "application/json",
|
|
315
|
+
});
|
|
316
|
+
res.end(JSON.stringify(receiptResponse.body ?? {}));
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
const recorded = Array.isArray(body?.skills) ? body.skills.length : 0;
|
|
320
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
321
|
+
res.end(JSON.stringify({ recorded }));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
265
325
|
// ── #1452: POST /api/v1/library (multipart file-push) ──────────
|
|
266
326
|
//
|
|
267
327
|
// Accepts multipart/form-data and returns a synthesized
|
|
@@ -486,18 +546,58 @@ export function createMockServer(initialPayload, options = {}) {
|
|
|
486
546
|
return;
|
|
487
547
|
}
|
|
488
548
|
|
|
489
|
-
// ETag short-circuit
|
|
549
|
+
// ETag short-circuit. Capture the inbound If-None-Match and
|
|
550
|
+
// `since` into inspectable slots BEFORE deciding 304-vs-200 so
|
|
551
|
+
// tests can assert on what the client actually sent — not just
|
|
552
|
+
// whether the response was 304. PR #1575's placement-presence
|
|
553
|
+
// fix makes the wire-level distinction load-bearing.
|
|
490
554
|
const ifNoneMatch = req.headers["if-none-match"];
|
|
555
|
+
const sinceParam = url.searchParams.get("since");
|
|
556
|
+
lastLibraryIfNoneMatch = ifNoneMatch ?? null;
|
|
557
|
+
lastLibrarySince = sinceParam ?? null;
|
|
558
|
+
lastLibraryManifest = url.searchParams.get("manifest");
|
|
559
|
+
libraryRequestCount++;
|
|
491
560
|
if (libraryEtag && ifNoneMatch === libraryEtag) {
|
|
492
561
|
res.writeHead(304, { ETag: libraryEtag });
|
|
493
562
|
res.end();
|
|
494
563
|
return;
|
|
495
564
|
}
|
|
496
565
|
|
|
566
|
+
// Mirror the production server's `since` filter — only return
|
|
567
|
+
// skills updated AFTER the `since` timestamp. Pre-PR #1575 the
|
|
568
|
+
// mock returned every skill regardless of `since`, which hid a
|
|
569
|
+
// real production bug: dropping the ETag without ALSO dropping
|
|
570
|
+
// `since` made the server respond with an empty delta, leaving
|
|
571
|
+
// newly-detected vendor placements empty forever. Mirroring the
|
|
572
|
+
// filter here means any future regression of that class fails
|
|
573
|
+
// at test time. See src/lib/queries/library.ts in the server
|
|
574
|
+
// for the production behavior (`gt(skills.updatedAt, since)`).
|
|
575
|
+
let respondedSkills = libraryResponse.skills ?? [];
|
|
576
|
+
if (sinceParam && Array.isArray(respondedSkills)) {
|
|
577
|
+
const sinceDate = new Date(sinceParam);
|
|
578
|
+
if (!Number.isNaN(sinceDate.getTime())) {
|
|
579
|
+
respondedSkills = respondedSkills.filter((s) => {
|
|
580
|
+
// updatedAt is a string ISO timestamp in the fixture skills.
|
|
581
|
+
// Missing/invalid → treat as "older than since" → exclude
|
|
582
|
+
// (matches the production semantic of "not modified in
|
|
583
|
+
// the window").
|
|
584
|
+
if (typeof s.updatedAt !== "string") return false;
|
|
585
|
+
const updated = new Date(s.updatedAt);
|
|
586
|
+
if (Number.isNaN(updated.getTime())) return false;
|
|
587
|
+
return updated.getTime() > sinceDate.getTime();
|
|
588
|
+
});
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
const filteredResponse = {
|
|
593
|
+
...libraryResponse,
|
|
594
|
+
skills: respondedSkills,
|
|
595
|
+
};
|
|
596
|
+
|
|
497
597
|
const headers = { "Content-Type": "application/json" };
|
|
498
598
|
if (libraryEtag) headers.ETag = libraryEtag;
|
|
499
599
|
res.writeHead(200, headers);
|
|
500
|
-
res.end(JSON.stringify(
|
|
600
|
+
res.end(JSON.stringify(filteredResponse));
|
|
501
601
|
return;
|
|
502
602
|
}
|
|
503
603
|
|
|
@@ -586,6 +686,89 @@ export function createMockServer(initialPayload, options = {}) {
|
|
|
586
686
|
libraryEtag = etag;
|
|
587
687
|
},
|
|
588
688
|
|
|
689
|
+
/**
|
|
690
|
+
* Inspect the inbound `If-None-Match` header from the LAST request
|
|
691
|
+
* to GET /api/v1/library. Returns the literal header string, or
|
|
692
|
+
* `null` if the request didn't send one (full-fetch path). Used
|
|
693
|
+
* by tests asserting that the placement-presence check correctly
|
|
694
|
+
* dropped or sent the ETag header. See PR #1575's coverage gap
|
|
695
|
+
* 2 — without this accessor, the "304 fires" tests could pass on
|
|
696
|
+
* an implementation that broke the wire-level optimization while
|
|
697
|
+
* preserving the user-facing "up to date" output.
|
|
698
|
+
*/
|
|
699
|
+
getLastLibraryIfNoneMatch() {
|
|
700
|
+
return lastLibraryIfNoneMatch;
|
|
701
|
+
},
|
|
702
|
+
|
|
703
|
+
/** Count of GET /api/v1/library requests since server start or reset. */
|
|
704
|
+
getLibraryRequestCount() {
|
|
705
|
+
return libraryRequestCount;
|
|
706
|
+
},
|
|
707
|
+
|
|
708
|
+
/**
|
|
709
|
+
* Inspect the inbound `?since=` query param from the LAST request
|
|
710
|
+
* to GET /api/v1/library. Returns the literal string or `null` if
|
|
711
|
+
* not present. The placement-presence fix must drop BOTH the
|
|
712
|
+
* If-None-Match header AND the `since` query param when placements
|
|
713
|
+
* are incomplete — otherwise the server returns an empty delta
|
|
714
|
+
* and missing placements stay empty. Tests use this to assert the
|
|
715
|
+
* fix at the wire level.
|
|
716
|
+
*/
|
|
717
|
+
getLastLibrarySince() {
|
|
718
|
+
return lastLibrarySince;
|
|
719
|
+
},
|
|
720
|
+
|
|
721
|
+
/** Reset the inspection slots — useful between phases of a multi-step test. */
|
|
722
|
+
resetLibraryInspection() {
|
|
723
|
+
lastLibraryIfNoneMatch = null;
|
|
724
|
+
lastLibrarySince = null;
|
|
725
|
+
lastLibraryManifest = null;
|
|
726
|
+
libraryRequestCount = 0;
|
|
727
|
+
},
|
|
728
|
+
|
|
729
|
+
/**
|
|
730
|
+
* Inspect the inbound `?manifest=` query param from the LAST request
|
|
731
|
+
* to GET /api/v1/library. `"1"` means the client made the
|
|
732
|
+
* non-recording manifest read (#1832, `skillrepo list`); `null`
|
|
733
|
+
* means a normal recording sync.
|
|
734
|
+
*/
|
|
735
|
+
getLastLibraryManifest() {
|
|
736
|
+
return lastLibraryManifest;
|
|
737
|
+
},
|
|
738
|
+
|
|
739
|
+
// ── #1832: sync-receipt slots ─────────────────────────────────
|
|
740
|
+
|
|
741
|
+
/**
|
|
742
|
+
* Return the most recent POST /api/v1/library/sync-receipts body
|
|
743
|
+
* (JSON-parsed), or null if none received. Tests assert on
|
|
744
|
+
* `.skills` (the confirmed-write list) and `.syncedAt`.
|
|
745
|
+
*/
|
|
746
|
+
getLastReceipt() {
|
|
747
|
+
return lastReceipt;
|
|
748
|
+
},
|
|
749
|
+
|
|
750
|
+
/** Count of sync-receipt POSTs since server start or reset. */
|
|
751
|
+
getReceiptRequestCount() {
|
|
752
|
+
return receiptRequestCount;
|
|
753
|
+
},
|
|
754
|
+
|
|
755
|
+
/**
|
|
756
|
+
* Override the sync-receipt response to simulate a failure (e.g.
|
|
757
|
+
* `{ status: 500, body: { error: "boom" } }`). The CLI treats a
|
|
758
|
+
* failed receipt as a non-fatal warning, so tests use this to
|
|
759
|
+
* assert the sync still succeeds. Pass null to restore the 200.
|
|
760
|
+
*/
|
|
761
|
+
setReceiptResponse(override) {
|
|
762
|
+
receiptResponse = override;
|
|
763
|
+
},
|
|
764
|
+
|
|
765
|
+
/** Reset sync-receipt inspection slots. */
|
|
766
|
+
resetReceipts() {
|
|
767
|
+
lastReceipt = null;
|
|
768
|
+
receiptRequestCount = 0;
|
|
769
|
+
receiptResponse = null;
|
|
770
|
+
},
|
|
771
|
+
|
|
589
772
|
/** Register a single-skill response keyed by `owner/name`. */
|
|
590
773
|
setSkillResponse(owner, name, skill) {
|
|
591
774
|
skillResponses.set(`${owner}/${name}`, skill);
|