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.
Files changed (104) hide show
  1. package/README.md +5 -3
  2. package/node_modules/pi-lean-host/AGENTS.md +13 -6
  3. package/node_modules/pi-lean-host/__tests__/api-learn-fetch-recipe.test.ts +282 -10
  4. package/node_modules/pi-lean-host/__tests__/api-learn-multi-file.test.ts +8 -16
  5. package/node_modules/pi-lean-host/__tests__/api-probe.test.ts +276 -279
  6. package/node_modules/pi-lean-host/__tests__/api-scaffold.test.ts +12 -33
  7. package/node_modules/pi-lean-host/__tests__/api-toggle.test.ts +14 -12
  8. package/node_modules/pi-lean-host/__tests__/bootstrap-command.test.ts +1 -7
  9. package/node_modules/pi-lean-host/__tests__/delete-command.test.ts +1 -12
  10. package/node_modules/pi-lean-host/__tests__/guide-catalog.test.ts +484 -0
  11. package/node_modules/pi-lean-host/__tests__/helpers.test.ts +0 -132
  12. package/node_modules/pi-lean-host/__tests__/oauth-command.test.ts +95 -0
  13. package/node_modules/pi-lean-host/__tests__/oauth-flow.test.ts +163 -0
  14. package/node_modules/pi-lean-host/__tests__/oauth-mint.test.ts +79 -6
  15. package/node_modules/pi-lean-host/__tests__/parse-api-guide.test.ts +4 -409
  16. package/node_modules/pi-lean-host/__tests__/response-spill.test.ts +0 -1
  17. package/node_modules/pi-lean-host/__tests__/secrets-command.test.ts +19 -35
  18. package/node_modules/pi-lean-host/__tests__/smoke.test.ts +1 -82
  19. package/node_modules/pi-lean-host/__tests__/ssrf-guard.test.ts +98 -0
  20. package/node_modules/pi-lean-host/__tests__/test-utils.ts +73 -0
  21. package/node_modules/pi-lean-host/__tests__/tools.test.ts +2 -518
  22. package/node_modules/pi-lean-host/__tests__/transport.test.ts +178 -31
  23. package/node_modules/pi-lean-host/__tests__/verify-command.test.ts +1 -8
  24. package/node_modules/pi-lean-host/__tests__/verify-stamp.test.ts +1 -4
  25. package/node_modules/pi-lean-host/api-guides/boe/local-helper.test.ts +9 -20
  26. package/node_modules/pi-lean-host/api-guides/dnb/error-envelope.test.ts +6 -18
  27. package/node_modules/pi-lean-host/api-guides/dnb/resumption-token.test.ts +6 -18
  28. package/node_modules/pi-lean-host/api-guides/frost-sensorthings/dotted-key.test.ts +6 -18
  29. package/node_modules/pi-lean-host/api-guides/github/static-key.test.ts +7 -25
  30. package/node_modules/pi-lean-host/api-guides/inaturalist/derived-id.test.ts +6 -18
  31. package/node_modules/pi-lean-host/api-guides/internet-archive/multi-recipe.test.ts +6 -26
  32. package/node_modules/pi-lean-host/api-guides/stripe/has-more.test.ts +6 -18
  33. package/node_modules/pi-lean-host/api-guides/telegram-bot/path-auth.test.ts +7 -25
  34. package/node_modules/pi-lean-host/api-guides/twitch/oauth2.test.ts +12 -47
  35. package/node_modules/pi-lean-host/api-guides/twitch-user/oauth-user.test.ts +11 -52
  36. package/node_modules/pi-lean-host/api-guides/usgs/transform.test.ts +10 -27
  37. package/node_modules/pi-lean-host/api-guides/wikidata-search/numeric-cursor.test.ts +6 -18
  38. package/node_modules/pi-lean-host/api-guides/wikimedia-action/token-bag.test.ts +9 -20
  39. package/node_modules/pi-lean-host/core/auth.ts +2 -1
  40. package/node_modules/pi-lean-host/core/helpers.ts +29 -35
  41. package/node_modules/pi-lean-host/core/oauth-command.ts +10 -8
  42. package/node_modules/pi-lean-host/core/oauth-flow.ts +20 -4
  43. package/node_modules/pi-lean-host/core/parse-api-guide.ts +3 -5
  44. package/node_modules/pi-lean-host/core/transport.ts +1 -1
  45. package/node_modules/pi-lean-host/package.json +1 -1
  46. package/node_modules/pi-lean-host/tools/api-guide.ts +18 -57
  47. package/node_modules/pi-lean-host/tools/api-learn.ts +20 -49
  48. package/node_modules/pi-lean-host/tools/api-probe.ts +17 -26
  49. package/node_modules/pi-lean-host/tools/api-scaffold.ts +20 -42
  50. package/node_modules/pi-lean-host/tools/utils.ts +69 -0
  51. package/node_modules/pi-lean-portal/AGENTS.md +4 -3
  52. package/node_modules/pi-lean-portal/README.md +9 -9
  53. package/node_modules/pi-lean-portal/__tests__/accessibility-tree.test.ts +0 -15
  54. package/node_modules/pi-lean-portal/__tests__/browser-install.test.ts +737 -0
  55. package/node_modules/pi-lean-portal/__tests__/browser-navigate.test.ts +33 -2
  56. package/node_modules/pi-lean-portal/__tests__/browser-status.test.ts +48 -25
  57. package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +3 -9
  58. package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +3 -29
  59. package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +51 -0
  60. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313-pytest-9.1.1.pyc +0 -0
  61. package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc +0 -0
  62. package/node_modules/pi-lean-portal/__tests__/helpers/mock-pi.ts +34 -0
  63. package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +14 -24
  64. package/node_modules/pi-lean-portal/__tests__/plugin-registry.test.ts +0 -90
  65. package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +32 -125
  66. package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +12 -170
  67. package/node_modules/pi-lean-portal/__tests__/router-session.test.ts +39 -151
  68. package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +1 -44
  69. package/node_modules/pi-lean-portal/backends/chromium/index.ts +1 -1
  70. package/node_modules/pi-lean-portal/backends/firefox/index.ts +1 -1
  71. package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
  72. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-312.pyc +0 -0
  73. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/playwright_base.cpython-313.pyc +0 -0
  74. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-312.pyc +0 -0
  75. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/transport.cpython-313.pyc +0 -0
  76. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/playwright_base.py +43 -57
  77. package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/transport.py +6 -21
  78. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313-pytest-9.1.1.pyc +0 -0
  79. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313-pytest-9.1.1.pyc +0 -0
  80. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_playwright_base_quirks.cpython-313.pyc +0 -0
  81. package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_transport.cpython-313-pytest-9.1.1.pyc +0 -0
  82. package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +157 -73
  83. package/node_modules/pi-lean-portal/backends/python-base/tests/test_transport.py +15 -41
  84. package/node_modules/pi-lean-portal/browser-install.ts +454 -0
  85. package/node_modules/pi-lean-portal/browser-status.ts +21 -0
  86. package/node_modules/pi-lean-portal/browser-toggle.ts +13 -11
  87. package/node_modules/pi-lean-portal/core/fetch-backend.ts +2 -2
  88. package/node_modules/pi-lean-portal/core/router.ts +7 -32
  89. package/node_modules/pi-lean-portal/core/shared/session-manager.ts +1 -3
  90. package/node_modules/pi-lean-portal/index.ts +1 -18
  91. package/node_modules/pi-lean-portal/package.json +2 -1
  92. package/node_modules/pi-lean-portal/tools/browser-navigate.ts +6 -2
  93. package/node_modules/pi-lean-portal/tools/utils.ts +5 -6
  94. package/node_modules/pi-lean-search/AGENTS.md +5 -3
  95. package/node_modules/pi-lean-search/__tests__/web-search.test.ts +180 -127
  96. package/node_modules/pi-lean-search/index.ts +0 -3
  97. package/node_modules/pi-lean-search/package.json +1 -1
  98. package/node_modules/pi-lean-search/web-search-tool.ts +22 -13
  99. package/node_modules/yaml/browser/dist/compose/resolve-flow-scalar.js +19 -18
  100. package/node_modules/yaml/browser/dist/nodes/Alias.js +25 -23
  101. package/node_modules/yaml/dist/compose/resolve-flow-scalar.js +19 -18
  102. package/node_modules/yaml/dist/nodes/Alias.js +25 -23
  103. package/node_modules/yaml/package.json +1 -1
  104. package/package.json +4 -4
@@ -2,11 +2,10 @@
2
2
  * Smoke tests.
3
3
  *
4
4
  * Verifies the extension loads without throwing and vendored primitives
5
- * work as expected.
5
+ * work as expected. (ssrfGuard unit tests live in ssrf-guard.test.ts.)
6
6
  */
7
7
 
8
8
  import { describe, it, expect } from "vitest";
9
- import { ssrfGuard } from "../core/ssrf-guard.js";
10
9
  import { buildDomainMap, type Guide } from "../core/guide-loader.js";
11
10
 
12
11
  // ─── index.ts loads without throwing ─────────────────────────────
@@ -20,86 +19,6 @@ describe("extension smoke", () => {
20
19
  }, 120_000); // the full extension import is slow under host load
21
20
  });
22
21
 
23
- // ─── ssrfGuard ───────────────────────────────────────────────────
24
-
25
- describe("ssrfGuard", () => {
26
- function expectRejected(raw: string): void {
27
- const result = ssrfGuard(raw);
28
- expect(result.ok).toBe(false);
29
- const r = result as { ok: false; reason: string };
30
- expect(r.reason).toBeTruthy();
31
- }
32
-
33
- it("rejects AWS metadata endpoint", () => {
34
- const result = ssrfGuard("http://169.254.169.254/latest/meta-data/");
35
- expect(result.ok).toBe(false);
36
- expect((result as { ok: false; reason: string }).reason).toContain(
37
- "169.254.169.254",
38
- );
39
- });
40
-
41
- it("rejects private IP 10.x.x.x", () => {
42
- const result = ssrfGuard("http://10.0.0.1/admin");
43
- expect(result.ok).toBe(false);
44
- expect((result as { ok: false; reason: string }).reason).toContain(
45
- "internal network",
46
- );
47
- });
48
-
49
- it("rejects 127.0.0.1 (loopback)", () => {
50
- expectRejected("http://127.0.0.1/");
51
- });
52
-
53
- it("rejects any 127.0.0.0/8 loopback (not just .1)", () => {
54
- expectRejected("http://127.0.0.2/");
55
- expectRejected("http://127.255.255.254/");
56
- });
57
-
58
- it("rejects link-local 169.254.0.0/16 (not just metadata IP)", () => {
59
- expectRejected("http://169.254.169.253/");
60
- });
61
-
62
- it("rejects IPv6 loopback ::1 exactly", () => {
63
- expectRejected("http://[::1]/");
64
- });
65
-
66
- it("allows public IPv6 that merely contains ::1 mid-string", () => {
67
- expect(ssrfGuard("http://[2001:db8::1:2:3]/").ok).toBe(true);
68
- });
69
-
70
- it("allows public IPv6 that merely contains ::ffff: mid-string", () => {
71
- expect(ssrfGuard("http://[2001:db8::ffff:1]/").ok).toBe(true);
72
- });
73
-
74
- it("rejects IPv6 link-local fe80::/10 and unique-local fc00::/7", () => {
75
- expectRejected("http://[fe80::1]/");
76
- expectRejected("http://[febf::1]/");
77
- expectRejected("http://[fc00::1]/");
78
- expectRejected("http://[fd12:3456::1]/");
79
- });
80
-
81
- it("rejects 192.168.x.x (private network)", () => {
82
- expectRejected("http://192.168.1.1/");
83
- });
84
-
85
- it("rejects metadata.google.internal", () => {
86
- expectRejected("http://metadata.google.internal/");
87
- });
88
-
89
- it("allows a legitimate public API URL", () => {
90
- const result = ssrfGuard("https://apidatos.boe.es/v1/diario/20260717");
91
- expect(result.ok).toBe(true);
92
- });
93
-
94
- it("rejects malformed URL", () => {
95
- const result = ssrfGuard("not a url at all");
96
- expect(result.ok).toBe(false);
97
- expect((result as { ok: false; reason: string }).reason).toBe(
98
- "Malformed URL",
99
- );
100
- });
101
- });
102
-
103
22
  // ─── buildDomainMap ──────────────────────────────────────────────
104
23
 
105
24
  describe("buildDomainMap", () => {
@@ -0,0 +1,98 @@
1
+ /**
2
+ * ssrfGuard (core/ssrf-guard.ts) unit tests — consolidated here from
3
+ * helpers.test.ts and smoke.test.ts. Pure function: no server, no fixtures.
4
+ */
5
+
6
+ import { describe, it, expect } from "vitest";
7
+ import { ssrfGuard } from "../core/ssrf-guard.js";
8
+
9
+ function expectRejected(raw: string): void {
10
+ const result = ssrfGuard(raw);
11
+ expect(result.ok).toBe(false);
12
+ const r = result as { ok: false; reason: string };
13
+ expect(r.reason).toBeTruthy();
14
+ }
15
+
16
+ describe("ssrfGuard — IPv4-mapped IPv6 (M1)", () => {
17
+ it("blocks IPv4-mapped loopback in hex form", () => {
18
+ // Node renders http://[::ffff:127.0.0.1]/ as "[::ffff:7f00:1]" —
19
+ // the old decimal "::ffff:127" check never matched this.
20
+ expect(ssrfGuard("http://[::ffff:127.0.0.1]/").ok).toBe(false);
21
+ });
22
+
23
+ it("blocks IPv4-mapped private + metadata ranges", () => {
24
+ expect(ssrfGuard("http://[::ffff:10.0.0.1]/").ok).toBe(false);
25
+ expect(ssrfGuard("http://[::ffff:192.168.1.1]/").ok).toBe(false);
26
+ expect(ssrfGuard("http://[::ffff:169.254.169.254]/").ok).toBe(false);
27
+ });
28
+
29
+ it("allows public hostnames", () => {
30
+ expect(ssrfGuard("https://api.example.com/v1/foo").ok).toBe(true);
31
+ expect(ssrfGuard("https://apidatos.boe.es/v1/diario/20260717").ok).toBe(true);
32
+ });
33
+ });
34
+
35
+ describe("ssrfGuard — baseline blocks", () => {
36
+ it("rejects AWS metadata endpoint with the host named in the reason", () => {
37
+ const result = ssrfGuard("http://169.254.169.254/latest/meta-data/");
38
+ expect(result.ok).toBe(false);
39
+ expect((result as { ok: false; reason: string }).reason).toContain(
40
+ "169.254.169.254",
41
+ );
42
+ });
43
+
44
+ it("rejects private IP 10.x.x.x with an internal-network reason", () => {
45
+ const result = ssrfGuard("http://10.0.0.1/admin");
46
+ expect(result.ok).toBe(false);
47
+ expect((result as { ok: false; reason: string }).reason).toContain(
48
+ "internal network",
49
+ );
50
+ });
51
+
52
+ it("still blocks plain IPv4 private/metadata/loopback", () => {
53
+ expect(ssrfGuard("http://127.0.0.1/").ok).toBe(false);
54
+ expect(ssrfGuard("http://10.0.0.1/").ok).toBe(false);
55
+ expect(ssrfGuard("http://169.254.169.254/").ok).toBe(false);
56
+ expect(ssrfGuard("http://192.168.1.1/").ok).toBe(false);
57
+ });
58
+
59
+ it("rejects any 127.0.0.0/8 loopback (not just .1)", () => {
60
+ expectRejected("http://127.0.0.2/");
61
+ expectRejected("http://127.255.255.254/");
62
+ });
63
+
64
+ it("rejects link-local 169.254.0.0/16 (not just the metadata IP)", () => {
65
+ expectRejected("http://169.254.169.253/");
66
+ });
67
+
68
+ it("rejects IPv6 loopback ::1 exactly", () => {
69
+ expectRejected("http://[::1]/");
70
+ });
71
+
72
+ it("allows public IPv6 that merely contains ::1 mid-string", () => {
73
+ expect(ssrfGuard("http://[2001:db8::1:2:3]/").ok).toBe(true);
74
+ });
75
+
76
+ it("allows public IPv6 that merely contains ::ffff: mid-string", () => {
77
+ expect(ssrfGuard("http://[2001:db8::ffff:1]/").ok).toBe(true);
78
+ });
79
+
80
+ it("rejects IPv6 link-local fe80::/10 and unique-local fc00::/7", () => {
81
+ expectRejected("http://[fe80::1]/");
82
+ expectRejected("http://[febf::1]/");
83
+ expectRejected("http://[fc00::1]/");
84
+ expectRejected("http://[fd12:3456::1]/");
85
+ });
86
+
87
+ it("rejects metadata.google.internal", () => {
88
+ expectRejected("http://metadata.google.internal/");
89
+ });
90
+
91
+ it("rejects malformed URL", () => {
92
+ const result = ssrfGuard("not a url at all");
93
+ expect(result.ok).toBe(false);
94
+ expect((result as { ok: false; reason: string }).reason).toBe(
95
+ "Malformed URL",
96
+ );
97
+ });
98
+ });
@@ -4,6 +4,41 @@
4
4
  */
5
5
 
6
6
  import { vi } from "vitest";
7
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
8
+ import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
9
+ import { join } from "node:path";
10
+ import { loadApiGuidesFromDir } from "../core/guide-catalog.js";
11
+ import { setUserGuidesDir, invalidateCache } from "../core/guide-store.js";
12
+ import type { ApiGuide, Operation } from "../core/api-guide-types.js";
13
+
14
+ /**
15
+ * Minimal UI mock context. `notify` is always a captured vi.fn; `input`/
16
+ * `confirm` are present as async vi.fn defaults — every command handler
17
+ * awaits them, so the superset base works for all command tests.
18
+ */
19
+ export function mockCtx(overrides: Record<string, unknown> = {}): any {
20
+ return {
21
+ hasUI: true,
22
+ ui: {
23
+ input: vi.fn(async () => undefined),
24
+ confirm: vi.fn(async () => true),
25
+ notify: vi.fn(),
26
+ },
27
+ ...overrides,
28
+ };
29
+ }
30
+
31
+ /** Collect a mock ctx's notify() calls as one newline-joined string. */
32
+ export function notifyText(ctx: any): string {
33
+ return ctx.ui.notify.mock.calls.map((c: unknown[]) => c[0]).join("\n");
34
+ }
35
+
36
+ /** Extract the handler registered for a command via registerCommand. */
37
+ export function captureApiHandler(
38
+ pi: ExtensionAPI,
39
+ ): (args: string, ctx: any) => Promise<void> {
40
+ return (pi.registerCommand as any).mock.calls[0][1].handler;
41
+ }
7
42
 
8
43
  /** Stub global fetch as a token-endpoint mock; returns the underlying vi.fn. */
9
44
  export function stubTokenEndpoint(
@@ -29,3 +64,41 @@ export const mockTheme = {
29
64
  fg: (_style: string, text: string) => text,
30
65
  bold: (s: string) => s,
31
66
  } as any;
67
+
68
+ /**
69
+ * Copy guide files from the api-guides tree into a fresh tmp guides dir under
70
+ * `tmpBase` (one subdir per name), point the guide store at it, and load.
71
+ * `guidesRoot` is the URL of the dir whose subdirs are named by `names`
72
+ * (co-located tests pass `new URL("../", import.meta.url)`). `files` defaults
73
+ * to ["guide.md"]; add "helper.ts" etc. for recipes with sibling files.
74
+ * Returns `{ guides }` — loaded guides keyed by directory name.
75
+ */
76
+ export function stageGuides(
77
+ tmpBase: string,
78
+ guidesRoot: URL,
79
+ names: readonly string[],
80
+ files: readonly string[] = ["guide.md"],
81
+ ): { guides: Record<string, ApiGuide> } {
82
+ const guidesDir = mkdtempSync(join(tmpBase, "guides-"));
83
+ for (const name of names) {
84
+ mkdirSync(join(guidesDir, name), { recursive: true });
85
+ for (const file of files) {
86
+ writeFileSync(
87
+ join(guidesDir, name, file),
88
+ readFileSync(new URL(`${name}/${file}`, guidesRoot), "utf-8"),
89
+ );
90
+ }
91
+ }
92
+ setUserGuidesDir(guidesDir);
93
+ invalidateCache();
94
+ return {
95
+ guides: loadApiGuidesFromDir(guidesDir).guides as Record<string, ApiGuide>,
96
+ };
97
+ }
98
+
99
+ /** Get an op by name from a loaded guide; throws naming the missing op. */
100
+ export function findOp(guide: ApiGuide, name: string): Operation {
101
+ const op = guide.operations.find((o) => o.name === name);
102
+ if (!op) throw new Error(`op ${name} not found`);
103
+ return op;
104
+ }