skillrepo 4.5.0 → 4.5.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.
@@ -80,6 +80,18 @@ 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
+
83
95
  // PR3a mutable slots for POST/DELETE library routes
84
96
  //
85
97
  // Each entry is `{ status: number, body: any }` describing the
@@ -486,18 +498,57 @@ export function createMockServer(initialPayload, options = {}) {
486
498
  return;
487
499
  }
488
500
 
489
- // ETag short-circuit
501
+ // ETag short-circuit. Capture the inbound If-None-Match and
502
+ // `since` into inspectable slots BEFORE deciding 304-vs-200 so
503
+ // tests can assert on what the client actually sent — not just
504
+ // whether the response was 304. PR #1575's placement-presence
505
+ // fix makes the wire-level distinction load-bearing.
490
506
  const ifNoneMatch = req.headers["if-none-match"];
507
+ const sinceParam = url.searchParams.get("since");
508
+ lastLibraryIfNoneMatch = ifNoneMatch ?? null;
509
+ lastLibrarySince = sinceParam ?? null;
510
+ libraryRequestCount++;
491
511
  if (libraryEtag && ifNoneMatch === libraryEtag) {
492
512
  res.writeHead(304, { ETag: libraryEtag });
493
513
  res.end();
494
514
  return;
495
515
  }
496
516
 
517
+ // Mirror the production server's `since` filter — only return
518
+ // skills updated AFTER the `since` timestamp. Pre-PR #1575 the
519
+ // mock returned every skill regardless of `since`, which hid a
520
+ // real production bug: dropping the ETag without ALSO dropping
521
+ // `since` made the server respond with an empty delta, leaving
522
+ // newly-detected vendor placements empty forever. Mirroring the
523
+ // filter here means any future regression of that class fails
524
+ // at test time. See src/lib/queries/library.ts in the server
525
+ // for the production behavior (`gt(skills.updatedAt, since)`).
526
+ let respondedSkills = libraryResponse.skills ?? [];
527
+ if (sinceParam && Array.isArray(respondedSkills)) {
528
+ const sinceDate = new Date(sinceParam);
529
+ if (!Number.isNaN(sinceDate.getTime())) {
530
+ respondedSkills = respondedSkills.filter((s) => {
531
+ // updatedAt is a string ISO timestamp in the fixture skills.
532
+ // Missing/invalid → treat as "older than since" → exclude
533
+ // (matches the production semantic of "not modified in
534
+ // the window").
535
+ if (typeof s.updatedAt !== "string") return false;
536
+ const updated = new Date(s.updatedAt);
537
+ if (Number.isNaN(updated.getTime())) return false;
538
+ return updated.getTime() > sinceDate.getTime();
539
+ });
540
+ }
541
+ }
542
+
543
+ const filteredResponse = {
544
+ ...libraryResponse,
545
+ skills: respondedSkills,
546
+ };
547
+
497
548
  const headers = { "Content-Type": "application/json" };
498
549
  if (libraryEtag) headers.ETag = libraryEtag;
499
550
  res.writeHead(200, headers);
500
- res.end(JSON.stringify(libraryResponse));
551
+ res.end(JSON.stringify(filteredResponse));
501
552
  return;
502
553
  }
503
554
 
@@ -586,6 +637,45 @@ export function createMockServer(initialPayload, options = {}) {
586
637
  libraryEtag = etag;
587
638
  },
588
639
 
640
+ /**
641
+ * Inspect the inbound `If-None-Match` header from the LAST request
642
+ * to GET /api/v1/library. Returns the literal header string, or
643
+ * `null` if the request didn't send one (full-fetch path). Used
644
+ * by tests asserting that the placement-presence check correctly
645
+ * dropped or sent the ETag header. See PR #1575's coverage gap
646
+ * 2 — without this accessor, the "304 fires" tests could pass on
647
+ * an implementation that broke the wire-level optimization while
648
+ * preserving the user-facing "up to date" output.
649
+ */
650
+ getLastLibraryIfNoneMatch() {
651
+ return lastLibraryIfNoneMatch;
652
+ },
653
+
654
+ /** Count of GET /api/v1/library requests since server start or reset. */
655
+ getLibraryRequestCount() {
656
+ return libraryRequestCount;
657
+ },
658
+
659
+ /**
660
+ * Inspect the inbound `?since=` query param from the LAST request
661
+ * to GET /api/v1/library. Returns the literal string or `null` if
662
+ * not present. The placement-presence fix must drop BOTH the
663
+ * If-None-Match header AND the `since` query param when placements
664
+ * are incomplete — otherwise the server returns an empty delta
665
+ * and missing placements stay empty. Tests use this to assert the
666
+ * fix at the wire level.
667
+ */
668
+ getLastLibrarySince() {
669
+ return lastLibrarySince;
670
+ },
671
+
672
+ /** Reset the inspection slots — useful between phases of a multi-step test. */
673
+ resetLibraryInspection() {
674
+ lastLibraryIfNoneMatch = null;
675
+ lastLibrarySince = null;
676
+ libraryRequestCount = 0;
677
+ },
678
+
589
679
  /** Register a single-skill response keyed by `owner/name`. */
590
680
  setSkillResponse(owner, name, skill) {
591
681
  skillResponses.set(`${owner}/${name}`, skill);