pi-lean-dimension 0.5.0 → 0.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/README.md +5 -3
- package/node_modules/pi-lean-host/AGENTS.md +13 -6
- package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
- package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
- package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
- package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
- package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
- package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
- package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
- package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
- package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
- package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
- package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
- package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
- package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
- package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
- package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
- package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
- package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
- package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
- package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
- package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
- package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
- package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
- package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
- package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
- package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
- package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
- package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
- package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
- package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
- package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
- package/node_modules/pi-lean-host/core/auth.ts +2 -1
- package/node_modules/pi-lean-host/core/helpers.ts +29 -35
- package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
- package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
- package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
- package/node_modules/pi-lean-host/core/transport.ts +1 -1
- package/node_modules/pi-lean-host/package.json +1 -1
- package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
- package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
- package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
- package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
- package/node_modules/pi-lean-host/tools/utils.ts +69 -0
- package/node_modules/pi-lean-portal/AGENTS.md +4 -3
- package/node_modules/pi-lean-portal/README.md +9 -9
- package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
- package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
- package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
- package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
- package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
- package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
- package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
- package/node_modules/pi-lean-portal/browser-install.ts +454 -0
- package/node_modules/pi-lean-portal/browser-status.ts +21 -0
- package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
- package/node_modules/pi-lean-portal/core/router.ts +7 -32
- package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
- package/node_modules/pi-lean-portal/index.ts +1 -18
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
- package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
- package/node_modules/pi-lean-search/AGENTS.md +5 -3
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
- package/node_modules/pi-lean-search/index.ts +0 -3
- package/node_modules/pi-lean-search/package.json +1 -1
- package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
- package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
- package/node_modules/yaml/dist/nodes/Alias.js +25 -23
- package/node_modules/yaml/package.json +1 -1
- package/package.json +4 -4
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
* regression guard for that exact boundary.
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
|
-
import { describe, it, expect } from "vitest";
|
|
19
|
+
import { describe, it, expect, beforeAll, afterAll } from "vitest";
|
|
20
20
|
import {
|
|
21
21
|
waitForRetry,
|
|
22
22
|
redactSecretParams,
|
|
@@ -150,6 +150,32 @@ describe("fetchUrl content-encoding handling", () => {
|
|
|
150
150
|
});
|
|
151
151
|
});
|
|
152
152
|
|
|
153
|
+
// ═══════════════════════════════════════════════════════════════
|
|
154
|
+
// Body-size ceiling — a response body past MAX_BODY_BYTES aborts the
|
|
155
|
+
// collect loop with BodyTooLargeError, which fetchUrl's retry logic
|
|
156
|
+
// classifies as non-transient (exactly one request, no retry).
|
|
157
|
+
// ═══════════════════════════════════════════════════════════════
|
|
158
|
+
|
|
159
|
+
describe("fetchUrl body-size ceiling", () => {
|
|
160
|
+
it("throws BodyTooLargeError on an oversized body without retrying", async () => {
|
|
161
|
+
let requests = 0;
|
|
162
|
+
const server = createServer((_req, res) => {
|
|
163
|
+
requests++;
|
|
164
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
165
|
+
res.end(Buffer.alloc(10 * 1024 * 1024 + 1));
|
|
166
|
+
});
|
|
167
|
+
const port = await listenAsync(server);
|
|
168
|
+
try {
|
|
169
|
+
await expect(
|
|
170
|
+
fetchUrl(`http://127.0.0.1:${port}/huge`, { fresh: true }),
|
|
171
|
+
).rejects.toMatchObject({ name: "BodyTooLargeError" });
|
|
172
|
+
expect(requests).toBe(1);
|
|
173
|
+
} finally {
|
|
174
|
+
server.close();
|
|
175
|
+
}
|
|
176
|
+
});
|
|
177
|
+
});
|
|
178
|
+
|
|
153
179
|
// ═══════════════════════════════════════════════════════════════════
|
|
154
180
|
// Grant-based caching — a body is served from cache only when the
|
|
155
181
|
// server granted freshness: a time grant (Cache-Control: max-age) or a
|
|
@@ -207,6 +233,15 @@ async function startCacheServer(
|
|
|
207
233
|
};
|
|
208
234
|
}
|
|
209
235
|
|
|
236
|
+
/** Cache-suite teardown: restore the real clock and close the server in
|
|
237
|
+
* one step, so a test can't reset one and forget the other. */
|
|
238
|
+
function stopCacheServer(
|
|
239
|
+
srv: Awaited<ReturnType<typeof startCacheServer>>,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
_setClockForTest(() => Date.now());
|
|
242
|
+
return srv.close();
|
|
243
|
+
}
|
|
244
|
+
|
|
210
245
|
/** Fixed test epoch so all clock math is deterministic. */
|
|
211
246
|
const T0 = 1_700_000_000_000;
|
|
212
247
|
|
|
@@ -236,8 +271,7 @@ describe("grant-based caching", () => {
|
|
|
236
271
|
expect(r3.cached).toBe(false);
|
|
237
272
|
expect(srv.requestCount()).toBe(2);
|
|
238
273
|
} finally {
|
|
239
|
-
|
|
240
|
-
await srv.close();
|
|
274
|
+
await stopCacheServer(srv);
|
|
241
275
|
}
|
|
242
276
|
});
|
|
243
277
|
|
|
@@ -271,8 +305,7 @@ describe("grant-based caching", () => {
|
|
|
271
305
|
expect(JSON.parse(r3.body)).toEqual({ n: 3, changed: true });
|
|
272
306
|
expect(srv.ifNoneMatches[2]).toBe('"v1"');
|
|
273
307
|
} finally {
|
|
274
|
-
|
|
275
|
-
await srv.close();
|
|
308
|
+
await stopCacheServer(srv);
|
|
276
309
|
}
|
|
277
310
|
});
|
|
278
311
|
|
|
@@ -292,8 +325,7 @@ describe("grant-based caching", () => {
|
|
|
292
325
|
expect(r3.cached).toBe(false);
|
|
293
326
|
expect(srv.requestCount()).toBe(3);
|
|
294
327
|
} finally {
|
|
295
|
-
|
|
296
|
-
await srv.close();
|
|
328
|
+
await stopCacheServer(srv);
|
|
297
329
|
}
|
|
298
330
|
});
|
|
299
331
|
|
|
@@ -314,8 +346,7 @@ describe("grant-based caching", () => {
|
|
|
314
346
|
expect(srv.ifNoneMatches[1]).toBeUndefined();
|
|
315
347
|
expect(srv.requestCount()).toBe(2);
|
|
316
348
|
} finally {
|
|
317
|
-
|
|
318
|
-
await srv.close();
|
|
349
|
+
await stopCacheServer(srv);
|
|
319
350
|
}
|
|
320
351
|
});
|
|
321
352
|
|
|
@@ -344,8 +375,7 @@ describe("grant-based caching", () => {
|
|
|
344
375
|
expect(r3.cached).toBe(true);
|
|
345
376
|
expect(srv.requestCount()).toBe(3);
|
|
346
377
|
} finally {
|
|
347
|
-
|
|
348
|
-
await srv.close();
|
|
378
|
+
await stopCacheServer(srv);
|
|
349
379
|
}
|
|
350
380
|
});
|
|
351
381
|
|
|
@@ -370,8 +400,7 @@ describe("grant-based caching", () => {
|
|
|
370
400
|
expect(srv.requestCount()).toBe(2);
|
|
371
401
|
expect(srv.ifNoneMatches[1]).toBeUndefined();
|
|
372
402
|
} finally {
|
|
373
|
-
|
|
374
|
-
await srv.close();
|
|
403
|
+
await stopCacheServer(srv);
|
|
375
404
|
}
|
|
376
405
|
},
|
|
377
406
|
);
|
|
@@ -395,8 +424,7 @@ describe("grant-based caching", () => {
|
|
|
395
424
|
expect(srv.requestCount()).toBe(2);
|
|
396
425
|
expect(srv.ifNoneMatches[1]).toBe('"ncx"');
|
|
397
426
|
} finally {
|
|
398
|
-
|
|
399
|
-
await srv.close();
|
|
427
|
+
await stopCacheServer(srv);
|
|
400
428
|
}
|
|
401
429
|
});
|
|
402
430
|
|
|
@@ -424,8 +452,7 @@ describe("grant-based caching", () => {
|
|
|
424
452
|
expect(r3.cached).toBe(true);
|
|
425
453
|
expect(srv.requestCount()).toBe(2);
|
|
426
454
|
} finally {
|
|
427
|
-
|
|
428
|
-
await srv.close();
|
|
455
|
+
await stopCacheServer(srv);
|
|
429
456
|
}
|
|
430
457
|
});
|
|
431
458
|
|
|
@@ -455,8 +482,7 @@ describe("grant-based caching", () => {
|
|
|
455
482
|
expect(r3.cached).toBe(true);
|
|
456
483
|
expect(srv.requestCount()).toBe(2);
|
|
457
484
|
} finally {
|
|
458
|
-
|
|
459
|
-
await srv.close();
|
|
485
|
+
await stopCacheServer(srv);
|
|
460
486
|
}
|
|
461
487
|
});
|
|
462
488
|
|
|
@@ -502,8 +528,7 @@ describe("grant-based caching", () => {
|
|
|
502
528
|
expect(srv.requestCount()).toBe(5); // conditional GET, not a header hit
|
|
503
529
|
expect(srv.ifNoneMatches[4]).toBe('"nc304"');
|
|
504
530
|
} finally {
|
|
505
|
-
|
|
506
|
-
await srv.close();
|
|
531
|
+
await stopCacheServer(srv);
|
|
507
532
|
}
|
|
508
533
|
});
|
|
509
534
|
|
|
@@ -530,8 +555,7 @@ describe("grant-based caching", () => {
|
|
|
530
555
|
expect(srv.requestCount()).toBe(3);
|
|
531
556
|
expect(srv.ifNoneMatches[2]).toBeUndefined();
|
|
532
557
|
} finally {
|
|
533
|
-
|
|
534
|
-
await srv.close();
|
|
558
|
+
await stopCacheServer(srv);
|
|
535
559
|
}
|
|
536
560
|
});
|
|
537
561
|
|
|
@@ -583,8 +607,7 @@ describe("grant-based caching", () => {
|
|
|
583
607
|
expect(targetInms[1]).toBe('"race"'); // second: conditional GET
|
|
584
608
|
expect(srv.requestCount()).toBe(103);
|
|
585
609
|
} finally {
|
|
586
|
-
|
|
587
|
-
await srv.close();
|
|
610
|
+
await stopCacheServer(srv);
|
|
588
611
|
}
|
|
589
612
|
});
|
|
590
613
|
|
|
@@ -612,8 +635,7 @@ describe("grant-based caching", () => {
|
|
|
612
635
|
expect(r3.cached).toBe(true);
|
|
613
636
|
expect(srv.requestCount()).toBe(2);
|
|
614
637
|
} finally {
|
|
615
|
-
|
|
616
|
-
await srv.close();
|
|
638
|
+
await stopCacheServer(srv);
|
|
617
639
|
}
|
|
618
640
|
});
|
|
619
641
|
|
|
@@ -636,8 +658,7 @@ describe("grant-based caching", () => {
|
|
|
636
658
|
expect(r3.cached).toBe(false);
|
|
637
659
|
expect(srv.requestCount()).toBe(3);
|
|
638
660
|
} finally {
|
|
639
|
-
|
|
640
|
-
await srv.close();
|
|
661
|
+
await stopCacheServer(srv);
|
|
641
662
|
}
|
|
642
663
|
});
|
|
643
664
|
|
|
@@ -667,8 +688,134 @@ describe("grant-based caching", () => {
|
|
|
667
688
|
expect(r2.body).not.toContain("public");
|
|
668
689
|
expect(srv.requestCount()).toBe(2);
|
|
669
690
|
} finally {
|
|
670
|
-
|
|
671
|
-
|
|
691
|
+
await stopCacheServer(srv);
|
|
692
|
+
}
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
// ═══════════════════════════════════════════════════════════════
|
|
697
|
+
// fetchUrl — fallbackCharset + guardRedirects (moved from helpers.test.ts)
|
|
698
|
+
// ═══════════════════════════════════════════════════════════════
|
|
699
|
+
|
|
700
|
+
interface CharsetTestCtx {
|
|
701
|
+
serverUrl: string;
|
|
702
|
+
stop: () => Promise<void>;
|
|
703
|
+
requestCounts: Map<string, number>;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Serves the latin-1 / charset-declaration / redirect-metadata routes. */
|
|
707
|
+
async function startCharsetServer(): Promise<CharsetTestCtx> {
|
|
708
|
+
const requestCounts = new Map<string, number>();
|
|
709
|
+
const server = createServer((req, res) => {
|
|
710
|
+
const pathname = new URL(req.url ?? "/", "http://localhost").pathname;
|
|
711
|
+
requestCounts.set(pathname, (requestCounts.get(pathname) ?? 0) + 1);
|
|
712
|
+
|
|
713
|
+
if (pathname === "/api/latin1-no-charset") {
|
|
714
|
+
// ISO-8859-1 bytes for áéíóú, served with NO charset parameter —
|
|
715
|
+
// the transport must fall back to the caller's fallbackCharset.
|
|
716
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
717
|
+
res.end(Buffer.from([0xe1, 0xe9, 0xed, 0xf3, 0xfa]));
|
|
718
|
+
return;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (pathname === "/api/utf8-with-charset") {
|
|
722
|
+
// Real UTF-8 bytes for áéíóú, served WITH charset=utf-8 — the
|
|
723
|
+
// header charset must win even if a fallbackCharset is supplied.
|
|
724
|
+
res.writeHead(200, {
|
|
725
|
+
"Content-Type": "application/json; charset=utf-8",
|
|
726
|
+
});
|
|
727
|
+
res.end(Buffer.from("áéíóú", "utf-8"));
|
|
728
|
+
return;
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
// 302 redirect to the cloud metadata endpoint (guardRedirects). The
|
|
732
|
+
// initial URL is on 127.0.0.1 — fetchUrl does NOT ssrf-check the
|
|
733
|
+
// initial URL (paginate owns that), only redirect targets.
|
|
734
|
+
if (pathname === "/redirect-to-metadata") {
|
|
735
|
+
res.writeHead(302, {
|
|
736
|
+
Location: "http://169.254.169.254/latest/meta-data/",
|
|
737
|
+
});
|
|
738
|
+
res.end();
|
|
739
|
+
return;
|
|
672
740
|
}
|
|
741
|
+
|
|
742
|
+
res.writeHead(404);
|
|
743
|
+
res.end();
|
|
744
|
+
});
|
|
745
|
+
const port = await listenAsync(server);
|
|
746
|
+
return {
|
|
747
|
+
serverUrl: `http://127.0.0.1:${port}`,
|
|
748
|
+
requestCounts,
|
|
749
|
+
stop: () =>
|
|
750
|
+
new Promise<void>((resolve, reject) =>
|
|
751
|
+
server.close((err) => (err ? reject(err) : resolve())),
|
|
752
|
+
),
|
|
753
|
+
};
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
describe("fetchUrl — fallbackCharset", () => {
|
|
757
|
+
let ctx: CharsetTestCtx;
|
|
758
|
+
|
|
759
|
+
beforeAll(async () => {
|
|
760
|
+
ctx = await startCharsetServer();
|
|
761
|
+
});
|
|
762
|
+
afterAll(async () => {
|
|
763
|
+
await ctx.stop();
|
|
764
|
+
});
|
|
765
|
+
|
|
766
|
+
it("falls back to fallbackCharset when the response omits a charset", async () => {
|
|
767
|
+
// Server serves ISO-8859-1 bytes with no charset parameter.
|
|
768
|
+
const { body } = await fetchUrl(`${ctx.serverUrl}/api/latin1-no-charset`, {
|
|
769
|
+
fallbackCharset: "iso-8859-1",
|
|
770
|
+
fresh: true,
|
|
771
|
+
});
|
|
772
|
+
expect(body).toBe("áéíóú");
|
|
773
|
+
});
|
|
774
|
+
|
|
775
|
+
it("uses utf-8 by default when no fallbackCharset is supplied", async () => {
|
|
776
|
+
const { body } = await fetchUrl(`${ctx.serverUrl}/api/latin1-no-charset`, {
|
|
777
|
+
fresh: true,
|
|
778
|
+
});
|
|
779
|
+
expect(body).not.toBe("áéíóú");
|
|
780
|
+
expect(body).toBe("�����");
|
|
781
|
+
});
|
|
782
|
+
|
|
783
|
+
it("header charset wins over fallbackCharset", async () => {
|
|
784
|
+
// Server declares charset=utf-8; supplying a latin-1 fallback must
|
|
785
|
+
// NOT override it — the header charset always wins.
|
|
786
|
+
const { body } = await fetchUrl(`${ctx.serverUrl}/api/utf8-with-charset`, {
|
|
787
|
+
fallbackCharset: "iso-8859-1",
|
|
788
|
+
fresh: true,
|
|
789
|
+
});
|
|
790
|
+
expect(body).toBe("áéíóú");
|
|
791
|
+
});
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
describe("fetchUrl — guardRedirects (M3)", () => {
|
|
795
|
+
let ctx: CharsetTestCtx;
|
|
796
|
+
|
|
797
|
+
beforeAll(async () => {
|
|
798
|
+
ctx = await startCharsetServer();
|
|
799
|
+
});
|
|
800
|
+
|
|
801
|
+
afterAll(async () => {
|
|
802
|
+
await ctx.stop();
|
|
803
|
+
});
|
|
804
|
+
|
|
805
|
+
it("blocks a 302 redirect to the cloud metadata endpoint", async () => {
|
|
806
|
+
// fetchUrl does NOT ssrf-check the initial URL (paginate owns that),
|
|
807
|
+
// so hitting the 127.0.0.1 test server is fine. The redirect target
|
|
808
|
+
// (169.254.169.254) must be blocked before it is fetched.
|
|
809
|
+
const before = ctx.requestCounts.get("/redirect-to-metadata") ?? 0;
|
|
810
|
+
await expect(
|
|
811
|
+
fetchUrl(`${ctx.serverUrl}/redirect-to-metadata`, {
|
|
812
|
+
guardRedirects: true,
|
|
813
|
+
}),
|
|
814
|
+
).rejects.toThrow(/Redirect to blocked host/i);
|
|
815
|
+
|
|
816
|
+
// The redirect endpoint was hit exactly once (the SSRF block is not
|
|
817
|
+
// transient, so fetchUrl must not retry).
|
|
818
|
+
const after = ctx.requestCounts.get("/redirect-to-metadata") ?? 0;
|
|
819
|
+
expect(after - before).toBe(1);
|
|
673
820
|
});
|
|
674
821
|
});
|
|
@@ -43,6 +43,7 @@ import { setOAuthDir, writeToken } from "../core/oauth-store.js";
|
|
|
43
43
|
import { handleVerifySubcommand } from "../core/verify-command.js";
|
|
44
44
|
import { TODAY } from "../core/parse-api-guide.js";
|
|
45
45
|
import { resetDisabledHelpers } from "../core/local-helpers.js";
|
|
46
|
+
import { mockCtx, notifyText } from "./test-utils.js";
|
|
46
47
|
|
|
47
48
|
// ═══════════════════════════════════════════════════════════════════
|
|
48
49
|
// Fixtures
|
|
@@ -234,14 +235,6 @@ function readGuideIn(dirName: string): string {
|
|
|
234
235
|
return readFileSync(join(tmpGuidesDir, dirName, "guide.md"), "utf-8");
|
|
235
236
|
}
|
|
236
237
|
|
|
237
|
-
function mockCtx(overrides: Record<string, unknown> = {}): any {
|
|
238
|
-
return { ui: { notify: vi.fn() }, hasUI: true, ...overrides };
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
function notifyText(ctx: any): string {
|
|
242
|
-
return ctx.ui.notify.mock.calls.map((c: unknown[]) => c[0]).join("\n");
|
|
243
|
-
}
|
|
244
|
-
|
|
245
238
|
function requestedUrls(): string[] {
|
|
246
239
|
return vi.mocked(fetchUrl).mock.calls.map((c) => String(c[0]));
|
|
247
240
|
}
|
|
@@ -28,6 +28,7 @@ import { fetchUrl } from "../core/transport.js";
|
|
|
28
28
|
import { setUserGuidesDir, invalidateCache } from "../core/guide-store.js";
|
|
29
29
|
import { handleVerifySubcommand } from "../core/verify-command.js";
|
|
30
30
|
import { TODAY } from "../core/parse-api-guide.js";
|
|
31
|
+
import { mockCtx } from "./test-utils.js";
|
|
31
32
|
|
|
32
33
|
let tmpGuidesDir: string;
|
|
33
34
|
|
|
@@ -81,10 +82,6 @@ function readGuide(): string {
|
|
|
81
82
|
return readFileSync(join(tmpGuidesDir, "verify", "guide.md"), "utf-8");
|
|
82
83
|
}
|
|
83
84
|
|
|
84
|
-
function mockCtx(): any {
|
|
85
|
-
return { ui: { notify: vi.fn() }, hasUI: true };
|
|
86
|
-
}
|
|
87
|
-
|
|
88
85
|
describe("/api verify stamp routine", () => {
|
|
89
86
|
it("--force stamps verified: today without any HTTP", async () => {
|
|
90
87
|
setupGuide(recipe("verified: 2026-07-17"));
|
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
11
|
-
import {
|
|
12
|
-
mkdtempSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
readFileSync,
|
|
17
|
-
} from "node:fs";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
18
12
|
import { tmpdir } from "node:os";
|
|
19
13
|
import { join } from "node:path";
|
|
20
14
|
import type { ApiGuide } from "../../core/api-guide-types.js";
|
|
@@ -29,25 +23,20 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
29
23
|
|
|
30
24
|
import { restGet } from "../../core/helpers.js";
|
|
31
25
|
import { callHelper } from "../../core/local-helpers.js";
|
|
32
|
-
import {
|
|
33
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
26
|
+
import { stageGuides } from "../../__tests__/test-utils.js";
|
|
34
27
|
|
|
35
28
|
const XML_BODY = `<diarios><diario id="1"><titulo>Test diary</titulo></diario></diarios>`;
|
|
36
29
|
|
|
37
30
|
let tmpBase: string;
|
|
38
31
|
|
|
39
32
|
async function setupRecipe(): Promise<{ guide: ApiGuide }> {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
}
|
|
47
|
-
setUserGuidesDir(guidesDir);
|
|
48
|
-
invalidateCache();
|
|
49
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
50
|
-
return { guide: loaded.guides["boe"]! };
|
|
33
|
+
const { guides } = stageGuides(
|
|
34
|
+
tmpBase,
|
|
35
|
+
new URL("../", import.meta.url),
|
|
36
|
+
["boe"],
|
|
37
|
+
["guide.md", "helper.ts"],
|
|
38
|
+
);
|
|
39
|
+
return { guide: guides["boe"]! };
|
|
51
40
|
}
|
|
52
41
|
|
|
53
42
|
beforeAll(() => {
|
|
@@ -13,13 +13,7 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
16
|
-
import {
|
|
17
|
-
mkdtempSync,
|
|
18
|
-
mkdirSync,
|
|
19
|
-
writeFileSync,
|
|
20
|
-
rmSync,
|
|
21
|
-
readFileSync,
|
|
22
|
-
} from "node:fs";
|
|
16
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
23
17
|
import { tmpdir } from "node:os";
|
|
24
18
|
import { join } from "node:path";
|
|
25
19
|
import { HelperError } from "../../core/helpers.js";
|
|
@@ -34,8 +28,7 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
34
28
|
}));
|
|
35
29
|
|
|
36
30
|
import { paginate } from "../../core/helpers.js";
|
|
37
|
-
import {
|
|
38
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
31
|
+
import { stageGuides } from "../../__tests__/test-utils.js";
|
|
39
32
|
|
|
40
33
|
const XML_HEADERS = { "content-type": "text/xml;charset=UTF-8" };
|
|
41
34
|
const JSON_HEADERS = { "content-type": "application/json" };
|
|
@@ -72,15 +65,10 @@ async function mock200(
|
|
|
72
65
|
}
|
|
73
66
|
|
|
74
67
|
async function loadDnbGuide(): Promise<ApiGuide> {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
|
|
80
|
-
setUserGuidesDir(guidesDir);
|
|
81
|
-
invalidateCache();
|
|
82
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
83
|
-
return loaded.guides["dnb"]!;
|
|
68
|
+
const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
|
|
69
|
+
"dnb",
|
|
70
|
+
]);
|
|
71
|
+
return guides["dnb"]!;
|
|
84
72
|
}
|
|
85
73
|
|
|
86
74
|
// Inline-guide builder for the World Bank mirror.
|
|
@@ -8,13 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
11
|
-
import {
|
|
12
|
-
mkdtempSync,
|
|
13
|
-
mkdirSync,
|
|
14
|
-
writeFileSync,
|
|
15
|
-
rmSync,
|
|
16
|
-
readFileSync,
|
|
17
|
-
} from "node:fs";
|
|
11
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
18
12
|
import { tmpdir } from "node:os";
|
|
19
13
|
import { join } from "node:path";
|
|
20
14
|
import type { ApiGuide } from "../../core/api-guide-types.js";
|
|
@@ -28,8 +22,7 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
28
22
|
}));
|
|
29
23
|
|
|
30
24
|
import { paginate } from "../../core/helpers.js";
|
|
31
|
-
import {
|
|
32
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
25
|
+
import { stageGuides } from "../../__tests__/test-utils.js";
|
|
33
26
|
|
|
34
27
|
const PAGE1_XML = `<OAI-PMH><ListRecords><record><metadata><title>One</title></metadata></record><record><metadata><title>Two</title></metadata></record><resumptionToken completeListSize="3">token-abc</resumptionToken></ListRecords></OAI-PMH>`;
|
|
35
28
|
|
|
@@ -39,15 +32,10 @@ const PAGE2_XML = `<OAI-PMH><ListRecords><record><metadata><title>Three</title><
|
|
|
39
32
|
let tmpBase: string;
|
|
40
33
|
|
|
41
34
|
async function setupRecipe(): Promise<{ guide: ApiGuide }> {
|
|
42
|
-
const
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
|
|
47
|
-
setUserGuidesDir(guidesDir);
|
|
48
|
-
invalidateCache();
|
|
49
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
50
|
-
return { guide: loaded.guides["dnb"]! };
|
|
35
|
+
const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
|
|
36
|
+
"dnb",
|
|
37
|
+
]);
|
|
38
|
+
return { guide: guides["dnb"]! };
|
|
51
39
|
}
|
|
52
40
|
|
|
53
41
|
beforeAll(() => {
|
|
@@ -20,13 +20,7 @@ import {
|
|
|
20
20
|
afterAll,
|
|
21
21
|
beforeEach,
|
|
22
22
|
} from "vitest";
|
|
23
|
-
import {
|
|
24
|
-
mkdtempSync,
|
|
25
|
-
mkdirSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
rmSync,
|
|
28
|
-
readFileSync,
|
|
29
|
-
} from "node:fs";
|
|
23
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
24
|
import { tmpdir } from "node:os";
|
|
31
25
|
import { join } from "node:path";
|
|
32
26
|
import type { ApiGuide } from "../../core/api-guide-types.js";
|
|
@@ -40,8 +34,7 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
40
34
|
}));
|
|
41
35
|
|
|
42
36
|
import { paginate } from "../../core/helpers.js";
|
|
43
|
-
import {
|
|
44
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
37
|
+
import { stageGuides } from "../../__tests__/test-utils.js";
|
|
45
38
|
import { fetchUrl } from "../../core/transport.js";
|
|
46
39
|
|
|
47
40
|
// Drain any under-consumed Once-queue so a leaky test can't shift its
|
|
@@ -124,15 +117,10 @@ const PAGE2 = JSON.stringify({
|
|
|
124
117
|
let tmpBase: string;
|
|
125
118
|
|
|
126
119
|
async function setupRecipe(): Promise<{ guide: ApiGuide }> {
|
|
127
|
-
const
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
|
|
132
|
-
setUserGuidesDir(guidesDir);
|
|
133
|
-
invalidateCache();
|
|
134
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
135
|
-
return { guide: loaded.guides["frost-sensorthings"]! };
|
|
120
|
+
const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
|
|
121
|
+
"frost-sensorthings",
|
|
122
|
+
]);
|
|
123
|
+
return { guide: guides["frost-sensorthings"]! };
|
|
136
124
|
}
|
|
137
125
|
|
|
138
126
|
beforeAll(() => {
|
|
@@ -15,16 +15,10 @@
|
|
|
15
15
|
*/
|
|
16
16
|
|
|
17
17
|
import { describe, it, expect, vi, beforeAll, afterAll } from "vitest";
|
|
18
|
-
import {
|
|
19
|
-
mkdtempSync,
|
|
20
|
-
mkdirSync,
|
|
21
|
-
writeFileSync,
|
|
22
|
-
rmSync,
|
|
23
|
-
readFileSync,
|
|
24
|
-
} from "node:fs";
|
|
18
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
25
19
|
import { tmpdir } from "node:os";
|
|
26
20
|
import { join } from "node:path";
|
|
27
|
-
import type { ApiGuide
|
|
21
|
+
import type { ApiGuide } from "../../core/api-guide-types.js";
|
|
28
22
|
|
|
29
23
|
// Mock the transport layer BEFORE any imports that use it.
|
|
30
24
|
vi.mock("../../core/transport.js", async () => ({
|
|
@@ -35,27 +29,15 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
35
29
|
}));
|
|
36
30
|
|
|
37
31
|
import { restGet, paginate } from "../../core/helpers.js";
|
|
38
|
-
import {
|
|
39
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
32
|
+
import { stageGuides, findOp } from "../../__tests__/test-utils.js";
|
|
40
33
|
|
|
41
34
|
let tmpBase: string;
|
|
42
35
|
|
|
43
36
|
async function setupRecipe(): Promise<{ guide: ApiGuide }> {
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
|
|
49
|
-
setUserGuidesDir(guidesDir);
|
|
50
|
-
invalidateCache();
|
|
51
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
52
|
-
return { guide: loaded.guides["github"]! };
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function findOp(guide: ApiGuide, name: string): Operation {
|
|
56
|
-
const op = guide.operations.find((o) => o.name === name);
|
|
57
|
-
if (!op) throw new Error(`op ${name} not found`);
|
|
58
|
-
return op;
|
|
37
|
+
const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
|
|
38
|
+
"github",
|
|
39
|
+
]);
|
|
40
|
+
return { guide: guides["github"]! };
|
|
59
41
|
}
|
|
60
42
|
|
|
61
43
|
beforeAll(() => {
|
|
@@ -20,13 +20,7 @@ import {
|
|
|
20
20
|
afterAll,
|
|
21
21
|
beforeEach,
|
|
22
22
|
} from "vitest";
|
|
23
|
-
import {
|
|
24
|
-
mkdtempSync,
|
|
25
|
-
mkdirSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
rmSync,
|
|
28
|
-
readFileSync,
|
|
29
|
-
} from "node:fs";
|
|
23
|
+
import { mkdtempSync, rmSync } from "node:fs";
|
|
30
24
|
import { tmpdir } from "node:os";
|
|
31
25
|
import { join } from "node:path";
|
|
32
26
|
import type { ApiGuide } from "../../core/api-guide-types.js";
|
|
@@ -41,8 +35,7 @@ vi.mock("../../core/transport.js", async () => ({
|
|
|
41
35
|
|
|
42
36
|
import { fetchUrl } from "../../core/transport.js";
|
|
43
37
|
import { paginate } from "../../core/helpers.js";
|
|
44
|
-
import {
|
|
45
|
-
import { setUserGuidesDir, invalidateCache } from "../../core/guide-store.js";
|
|
38
|
+
import { stageGuides } from "../../__tests__/test-utils.js";
|
|
46
39
|
|
|
47
40
|
// Drain any under-consumed Once-queue so a leaky test can't shift its
|
|
48
41
|
// leftover pages into the next test (mockReset also clears calls).
|
|
@@ -100,15 +93,10 @@ const PAGE3 = JSON.stringify({
|
|
|
100
93
|
let tmpBase: string;
|
|
101
94
|
|
|
102
95
|
async function setupRecipe(): Promise<{ guide: ApiGuide }> {
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
writeFileSync(join(domainDir, "guide.md"), source, "utf-8");
|
|
108
|
-
setUserGuidesDir(guidesDir);
|
|
109
|
-
invalidateCache();
|
|
110
|
-
const loaded = loadApiGuidesFromDir(guidesDir);
|
|
111
|
-
return { guide: loaded.guides["inaturalist"]! };
|
|
96
|
+
const { guides } = stageGuides(tmpBase, new URL("../", import.meta.url), [
|
|
97
|
+
"inaturalist",
|
|
98
|
+
]);
|
|
99
|
+
return { guide: guides["inaturalist"]! };
|
|
112
100
|
}
|
|
113
101
|
|
|
114
102
|
beforeAll(() => {
|