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
@@ -6,6 +6,9 @@
6
6
  */
7
7
 
8
8
  import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
9
+ import { readFileSync } from "node:fs";
10
+ import { join, dirname } from "node:path";
11
+ import { fileURLToPath } from "node:url";
9
12
  import { browserNavigateTool } from "../tools/browser-navigate.js";
10
13
 
11
14
  // ─── Mock router ─────────────────────────────────────────────────
@@ -16,7 +19,7 @@ const mockNavigateResult = {
16
19
  title: "",
17
20
  snapshot: "",
18
21
  elementCount: 0,
19
- error: "Browser not installed. Run: npx playwright install chromium firefox",
22
+ error: "Browser not installed. Run /web install to install it.",
20
23
  backendUsed: "chromium",
21
24
  };
22
25
 
@@ -112,7 +115,7 @@ describe("browser-navigate notify path", () => {
112
115
  // notify should have been called with the install command
113
116
  expect(notifySpy).toHaveBeenCalledTimes(1);
114
117
  expect(notifySpy).toHaveBeenCalledWith(
115
- "Browser not installed. Run: npx playwright install chromium firefox",
118
+ "Browser not installed. Run /web install to install it.",
116
119
  "warning",
117
120
  );
118
121
  });
@@ -142,3 +145,31 @@ describe("browser-navigate notify path", () => {
142
145
  expect(notifySpy).not.toHaveBeenCalled();
143
146
  });
144
147
  });
148
+
149
+ // ─── Structural: hint-string coupling ───────────────────────────
150
+
151
+ // The navigate notify is gated on result.error containing "not installed"
152
+ // (see the isInstallError check in browser-navigate.ts), and that error is
153
+ // the backend `installHint` string. A hint that loses the substring silently
154
+ // disables the notify — and the mocked-error tests above cannot catch it,
155
+ // because they control result.error themselves. These source-level checks
156
+ // pin the coupling.
157
+
158
+ describe("installHint string coupling", () => {
159
+ const portalRoot = join(dirname(fileURLToPath(import.meta.url)), "..");
160
+ const hintSources = [
161
+ join(portalRoot, "backends", "chromium", "index.ts"),
162
+ join(portalRoot, "backends", "firefox", "index.ts"),
163
+ join(portalRoot, "tools", "browser-navigate.ts"),
164
+ ];
165
+
166
+ for (const src of hintSources) {
167
+ it(`${src.split(/[\\/]/).slice(-2).join("/")} hint contains "not installed" and no npx advice`, () => {
168
+ const text = readFileSync(src, "utf8");
169
+ expect(text).toContain(
170
+ '"Browser not installed. Run /web install to install it."',
171
+ );
172
+ expect(text).not.toContain("npx playwright install");
173
+ });
174
+ }
175
+ });
@@ -15,6 +15,7 @@ import type { BrowserSession } from "../core/shared/session-manager.js";
15
15
  import { handleStatusSubcommand } from "../browser-status.js";
16
16
  import { sessionManager } from "../core/shared/session-manager.js";
17
17
  import { listProfiles } from "../browser-profile.js";
18
+ import { mockCtx } from "./helpers/mock-pi.js";
18
19
 
19
20
  // ─── Mock dependencies ──────────────────────────────────────────
20
21
 
@@ -44,6 +45,12 @@ vi.mock("../core/shared/storage-state.js", () => ({
44
45
  isSessionProfile: vi.fn((name: string) => name.startsWith("_session-")),
45
46
  }));
46
47
 
48
+ vi.mock("../browser-install.js", () => ({
49
+ ENGINES: ["chromium", "firefox"],
50
+ resolveBundledPlaywright: vi.fn(() => ({ pw: {} })),
51
+ detectInstalledBrowsers: vi.fn(() => ({ chromium: true, firefox: false })),
52
+ }));
53
+
47
54
  // ─── Type shortcuts ─────────────────────────────────────────────
48
55
 
49
56
  type MockSession = {
@@ -62,19 +69,6 @@ type MockSession = {
62
69
 
63
70
  type Profile = ReturnType<typeof listProfiles>[number];
64
71
 
65
- // ─── Helpers ────────────────────────────────────────────────────
66
-
67
- function mockCtx(): ExtensionContext {
68
- return {
69
- ui: {
70
- notify: vi.fn(),
71
- setStatus: vi.fn(),
72
- },
73
- // Minimal shape required by ExtensionContext
74
- sessionManager: {},
75
- } as unknown as ExtensionContext;
76
- }
77
-
78
72
  // ─── Tests ──────────────────────────────────────────────────────
79
73
 
80
74
  describe("handleStatusSubcommand", () => {
@@ -82,7 +76,9 @@ describe("handleStatusSubcommand", () => {
82
76
  let notifySpy: ReturnType<typeof vi.fn>;
83
77
 
84
78
  beforeEach(() => {
85
- vi.clearAllMocks();
79
+ // restoreAllMocks (not clearAllMocks): per-test mockReturnValue/mockImplementation
80
+ // overrides (e.g. the throwing resolveBundledPlaywright) must not leak into later tests.
81
+ vi.restoreAllMocks();
86
82
  ctx = mockCtx();
87
83
  notifySpy = ctx.ui.notify as ReturnType<typeof vi.fn>;
88
84
  // Default: empty session-manager state, idle status
@@ -109,6 +105,44 @@ describe("handleStatusSubcommand", () => {
109
105
  expect(msg).toContain("📖 Learn mode: ❌ off");
110
106
  });
111
107
 
108
+ // ── Browser-binary detection line ───────────────────────
109
+
110
+ it("shows per-engine browser detection with install hint when missing", async () => {
111
+ const { detectInstalledBrowsers } = await import("../browser-install.js");
112
+ vi.mocked(detectInstalledBrowsers).mockReturnValue({
113
+ chromium: true,
114
+ firefox: false,
115
+ });
116
+ handleStatusSubcommand(ctx, true, false);
117
+
118
+ const msg = notifySpy.mock.lastCall?.[0] as string;
119
+ expect(msg).toContain("Browsers: chromium ✓ firefox ✗ (run /web install)");
120
+ });
121
+
122
+ it("omits the install hint when all Node-backend browsers are present", async () => {
123
+ const { detectInstalledBrowsers } = await import("../browser-install.js");
124
+ vi.mocked(detectInstalledBrowsers).mockReturnValue({
125
+ chromium: true,
126
+ firefox: true,
127
+ });
128
+ handleStatusSubcommand(ctx, true, false);
129
+
130
+ const msg = notifySpy.mock.lastCall?.[0] as string;
131
+ expect(msg).toContain("Browsers: chromium ✓ firefox ✓");
132
+ expect(msg).not.toContain("run /web install");
133
+ });
134
+
135
+ it("omits the Browsers line when playwright is unresolvable", async () => {
136
+ const { resolveBundledPlaywright } = await import("../browser-install.js");
137
+ vi.mocked(resolveBundledPlaywright).mockImplementation(() => {
138
+ throw new Error("Cannot find module 'playwright'");
139
+ });
140
+ expect(() => handleStatusSubcommand(ctx, true, false)).not.toThrow();
141
+
142
+ const msg = notifySpy.mock.lastCall?.[0] as string;
143
+ expect(msg).not.toContain("Browsers:");
144
+ });
145
+
112
146
  // ── Plugin listing ─────────────────────────────────────────
113
147
 
114
148
  it("lists plugins with enabled/disabled state", () => {
@@ -457,15 +491,4 @@ describe("handleStatusSubcommand", () => {
457
491
  // Session profiles collapsed
458
492
  expect(msg).toContain("Session profiles: 1 (manage with /web profile)");
459
493
  });
460
-
461
- // ── Edge: no profiles but named/session filter weirdness ───
462
-
463
- it('shows "Profiles: none" when listProfiles returns an empty array', () => {
464
- vi.mocked(listProfiles).mockReturnValue([]);
465
-
466
- handleStatusSubcommand(ctx, false, false);
467
-
468
- const msg = notifySpy.mock.lastCall?.[0] as string;
469
- expect(msg).toContain("Profiles: none");
470
- });
471
494
  });
@@ -27,6 +27,7 @@ vi.mock("node:fs", () => ({
27
27
 
28
28
  import browserToggle from "../browser-toggle.js";
29
29
  import { listProfiles, formatProfileList } from "../browser-profile.js";
30
+ import { captureWebHandler } from "./helpers/mock-pi.js";
30
31
 
31
32
  // ─── Helpers ─────────────────────────────────────────────────────
32
33
 
@@ -55,20 +56,13 @@ function mockPi(tools?: string[]): ExtensionAPI {
55
56
  }
56
57
 
57
58
  /**
58
- * Capture the command handler from registerCommand.
59
+ * Register the /web command and capture its handler.
59
60
  */
60
61
  function captureHandler(
61
62
  pi: ExtensionAPI,
62
63
  ): (args: string, ctx: any) => Promise<void> {
63
- let capturedHandler: ((args: string, ctx: any) => Promise<void>) | undefined;
64
- (pi.registerCommand as Mock).mockImplementation(
65
- (_name: string, opts: any) => {
66
- capturedHandler = opts.handler;
67
- },
68
- );
69
64
  browserToggle(pi);
70
- if (!capturedHandler) throw new Error("Handler was not registered");
71
- return capturedHandler;
65
+ return captureWebHandler(pi);
72
66
  }
73
67
 
74
68
  beforeEach(() => {
@@ -11,11 +11,9 @@
11
11
 
12
12
  import { describe, it, expect, vi, beforeEach } from "vitest";
13
13
  import { EventEmitter } from "node:events";
14
- import type {
15
- ExtensionAPI,
16
- ExtensionContext,
17
- } from "@earendil-works/pi-coding-agent";
14
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
18
15
  import { TOOLSET_EVENTS } from "pi-tool-masking";
16
+ import { mockCtx, captureWebHandler } from "./helpers/mock-pi.js";
19
17
  import browserToggle, {
20
18
  getToggleState,
21
19
  getLearnState,
@@ -111,30 +109,6 @@ function mockPi(initialTools?: string[]): MockPi {
111
109
  return { pi, events: eventEmitter, handlers, entryCalls };
112
110
  }
113
111
 
114
- function mockCtx(overrides: Partial<ExtensionContext> = {}): any {
115
- return {
116
- sessionManager: { getBranch: () => [] },
117
- ui: {
118
- setStatus: vi.fn(),
119
- theme: { fg: (_c: string, t: string) => t },
120
- notify: vi.fn(),
121
- },
122
- mode: "tui",
123
- cwd: "/mock",
124
- hasUI: true,
125
- isIdle: () => true,
126
- modelRegistry: {} as any,
127
- ...overrides,
128
- };
129
- }
130
-
131
- /** Capture the /web command handler from registerCommand. */
132
- function captureWebHandler(
133
- pi: ExtensionAPI,
134
- ): (args: string, ctx: any) => Promise<void> {
135
- return (pi.registerCommand as any).mock.calls[0][1].handler;
136
- }
137
-
138
112
  // ==================================================================
139
113
  // Default export
140
114
  // ==================================================================
@@ -371,7 +345,7 @@ describe("/web focus-mode guard", () => {
371
345
  browserToggle(pi);
372
346
  focusAllowlistForTest();
373
347
 
374
- for (const sub of ["status", "profile", "cookies", ""]) {
348
+ for (const sub of ["status", "profile", "cookies", "install", ""]) {
375
349
  const ctx = mockCtx();
376
350
  await captureWebHandler(pi)(sub, ctx);
377
351
 
@@ -33,6 +33,11 @@ interface MockFetchOpts {
33
33
  * Only meaningful when delayMs is not set.
34
34
  */
35
35
  abortOnly?: boolean;
36
+ /**
37
+ * If true, respond with headers immediately but keep the body open until
38
+ * aborted (for body-phase timeout tests). Takes precedence over delayMs.
39
+ */
40
+ bodyStall?: boolean;
36
41
  }
37
42
 
38
43
  /**
@@ -70,6 +75,31 @@ function mockFetch(opts: MockFetchOpts): void {
70
75
  return;
71
76
  }
72
77
 
78
+ if (opts.bodyStall) {
79
+ // Headers arrive at once; the body read only settles on abort.
80
+ resolve(
81
+ new Response(
82
+ new ReadableStream({
83
+ start(streamController) {
84
+ signal?.addEventListener(
85
+ "abort",
86
+ () =>
87
+ streamController.error(
88
+ new DOMException(
89
+ "The operation was aborted",
90
+ "AbortError",
91
+ ),
92
+ ),
93
+ { once: true },
94
+ );
95
+ },
96
+ }),
97
+ { status: 200, headers: { "Content-Type": "text/html" } },
98
+ ),
99
+ );
100
+ return;
101
+ }
102
+
73
103
  const respond = (): void => {
74
104
  resolve(
75
105
  new Response(html, {
@@ -371,4 +401,25 @@ describe("webFetch — timeout", () => {
371
401
  expect(result.success).toBe(false);
372
402
  expect(result.error).toBe("timeout");
373
403
  });
404
+
405
+ it("times out when the body stalls after the headers arrive", async () => {
406
+ vi.useFakeTimers();
407
+ try {
408
+ mockFetch({ body: "", bodyStall: true });
409
+
410
+ const pending = webFetch({
411
+ url: "http://example.com/stalled-body",
412
+ timeout: 1,
413
+ });
414
+
415
+ // Fire the abort timer deterministically instead of waiting on it.
416
+ await vi.advanceTimersByTimeAsync(1000);
417
+ const result = await pending;
418
+
419
+ expect(result.success).toBe(false);
420
+ expect(result.error).toBe("timeout");
421
+ } finally {
422
+ vi.useRealTimers();
423
+ }
424
+ });
374
425
  });
@@ -0,0 +1,34 @@
1
+ import { vi } from "vitest";
2
+
3
+ /**
4
+ * Shared ExtensionContext stub for command-handler tests. Superset of the
5
+ * shapes the browser-toggle / browser-install / browser-status tests need;
6
+ * callers add or replace fields via `overrides`.
7
+ */
8
+ export function mockCtx(overrides: Record<string, unknown> = {}): any {
9
+ return {
10
+ sessionManager: { getBranch: () => [] },
11
+ ui: {
12
+ notify: vi.fn(),
13
+ setStatus: vi.fn(),
14
+ setWidget: vi.fn(),
15
+ theme: { fg: (_c: string, t: string) => t },
16
+ custom: vi.fn(async () => undefined),
17
+ },
18
+ mode: "tui",
19
+ cwd: "/mock",
20
+ hasUI: true,
21
+ isIdle: () => true,
22
+ modelRegistry: {},
23
+ ...overrides,
24
+ };
25
+ }
26
+
27
+ /** Capture the /web command handler registered via registerCommand. */
28
+ export function captureWebHandler(
29
+ pi: unknown,
30
+ ): (args: string, ctx: any) => Promise<void> {
31
+ const handler = (pi as any).registerCommand.mock.calls[0]?.[1]?.handler;
32
+ if (!handler) throw new Error("Handler was not registered");
33
+ return handler;
34
+ }
@@ -2,11 +2,14 @@
2
2
  """Mock bridge for testing PythonPluginAdapter.
3
3
 
4
4
  Reads JSON-RPC 2.0 requests from stdin and writes hardcoded responses
5
- to stdout. Supports: ping, shutdown, browser.navigate, browser.snapshot,
6
- browser.click, browser.cleanup. Unknown methods return a METHOD_NOT_FOUND
7
- error. The string "INVALID_JSON" as a request line triggers a malformed
8
- JSON response (to test protocol violation handling).
5
+ to stdout: the ping / browser.init / shutdown handshake, the
6
+ browser.* navigation, interaction, console, evaluate, screenshot,
7
+ cookie and storage-state calls, and browser.cleanup. browser.error
8
+ and browser.missingSession deliberately return JSON-RPC errors (to
9
+ test error handling). Unknown methods return a METHOD_NOT_FOUND
10
+ error, and a malformed request line returns a parse error.
9
11
  """
12
+
10
13
  import json
11
14
  import sys
12
15
 
@@ -17,12 +20,6 @@ def main() -> None:
17
20
  if not line:
18
21
  continue
19
22
 
20
- # Special case: protocol violation test
21
- if line == "INVALID_JSON":
22
- sys.stdout.write("not valid json\n")
23
- sys.stdout.flush()
24
- continue
25
-
26
23
  try:
27
24
  request = json.loads(line)
28
25
  except json.JSONDecodeError:
@@ -61,7 +58,7 @@ def main() -> None:
61
58
  "name": "Example Domain",
62
59
  "props": [],
63
60
  "depth": 0,
64
- "raw": "- link \"Example Domain\"",
61
+ "raw": '- link "Example Domain"',
65
62
  "occurrenceIndex": 0,
66
63
  },
67
64
  "e2": {
@@ -69,7 +66,7 @@ def main() -> None:
69
66
  "name": "Submit",
70
67
  "props": [],
71
68
  "depth": 0,
72
- "raw": "- button \"Submit\"",
69
+ "raw": '- button "Submit"',
73
70
  "occurrenceIndex": 0,
74
71
  },
75
72
  },
@@ -89,7 +86,7 @@ def main() -> None:
89
86
  "name": "Example",
90
87
  "props": [],
91
88
  "depth": 0,
92
- "raw": "- link \"Example\"",
89
+ "raw": '- link "Example"',
93
90
  "occurrenceIndex": 0,
94
91
  },
95
92
  },
@@ -200,17 +197,13 @@ def main() -> None:
200
197
  "origins": [
201
198
  {
202
199
  "origin": "https://example.com",
203
- "localStorage": [
204
- {"name": "pref", "value": "dark_mode"}
205
- ],
200
+ "localStorage": [{"name": "pref", "value": "dark_mode"}],
206
201
  }
207
202
  ],
208
203
  },
209
204
  )
210
205
 
211
206
  elif method == "browser.getCookies":
212
- params = request.get("params", {})
213
- urls = params.get("urls", [])
214
207
  write_response(
215
208
  req_id,
216
209
  result={
@@ -240,7 +233,9 @@ def main() -> None:
240
233
  error={
241
234
  "code": -32000,
242
235
  "message": "Something went wrong",
243
- "data": {"traceback": "Traceback (most recent call last):\n File \"mock.py\", line 1, in <module>\nRuntimeError: test error"},
236
+ "data": {
237
+ "traceback": 'Traceback (most recent call last):\n File "mock.py", line 1, in <module>\nRuntimeError: test error'
238
+ },
244
239
  },
245
240
  )
246
241
 
@@ -254,11 +249,6 @@ def main() -> None:
254
249
  },
255
250
  )
256
251
 
257
- elif method == "browser.timeout":
258
- # Simulate a timeout by not responding
259
- # (the caller's transport timeout will fire)
260
- pass
261
-
262
252
  else:
263
253
  write_response(
264
254
  req_id,
@@ -358,93 +358,3 @@ describe("PluginRegistry.clear", () => {
358
358
  expect(registry.available()).toEqual([]);
359
359
  });
360
360
  });
361
-
362
- // ─── MockPlugin call tracking ────────────────────────────────────
363
-
364
- describe("MockPlugin call tracking", () => {
365
- it("records navigate calls with correct args", async () => {
366
- const plugin = new MockPlugin();
367
- await plugin.navigate("https://example.com", "task-1", 30000);
368
- await plugin.navigate("https://other.com", "task-2", 5000);
369
- const calls = plugin.calls.get("navigate")!;
370
- expect(calls).toHaveLength(2);
371
- expect(calls[0]).toEqual(["https://example.com", "task-1", 30000, undefined]);
372
- expect(calls[1]).toEqual(["https://other.com", "task-2", 5000, undefined]);
373
- });
374
-
375
- it("records click calls", async () => {
376
- const plugin = new MockPlugin();
377
- await plugin.click("task-1", "@e5");
378
- expect(plugin.calls.get("click")).toEqual([["task-1", "@e5"]]);
379
- });
380
-
381
- it("records type calls", async () => {
382
- const plugin = new MockPlugin();
383
- await plugin.type("task-1", "@e3", "hello");
384
- expect(plugin.calls.get("type")).toEqual([["task-1", "@e3", "hello"]]);
385
- });
386
-
387
- it("records cleanup calls", async () => {
388
- const plugin = new MockPlugin();
389
- await plugin.cleanup("task-1");
390
- expect(plugin.calls.get("cleanup")).toEqual([["task-1"]]);
391
- });
392
-
393
- it("throws when shouldThrow is set", async () => {
394
- const plugin = new MockPlugin();
395
- plugin.shouldThrow.add("navigate");
396
- await expect(
397
- plugin.navigate("https://example.com", "task-1", 30000),
398
- ).rejects.toThrow("navigate failed");
399
- });
400
- });
401
-
402
- // ─── MockPlugin configurable returns ─────────────────────────────
403
-
404
- describe("MockPlugin configurable returns", () => {
405
- it("returns custom navigate result", async () => {
406
- const plugin = new MockPlugin();
407
- plugin.navResult = {
408
- success: false,
409
- error: "page not found",
410
- url: "https://example.com/404",
411
- title: "",
412
- snapshot: "",
413
- elementCount: 0,
414
- };
415
- const result = await plugin.navigate(
416
- "https://example.com/404",
417
- "task-1",
418
- 30000,
419
- );
420
- expect(result.success).toBe(false);
421
- expect(result.error).toBe("page not found");
422
- });
423
-
424
- it("returns bot detection signal", async () => {
425
- const plugin = new MockPlugin();
426
- plugin.navResult = { botDetected: true };
427
- const result = await plugin.navigate(
428
- "https://protected.com",
429
- "task-1",
430
- 30000,
431
- );
432
- expect(result.botDetected).toBe(true);
433
- });
434
-
435
- it("returns custom evaluate result", async () => {
436
- const plugin = new MockPlugin();
437
- plugin.evalResult = { result: { key: "value" } };
438
- const result = await plugin.evaluate("task-1", "document.title");
439
- expect(result.result).toEqual({ key: "value" });
440
- });
441
-
442
- it("returns custom screenshot data URI", async () => {
443
- const plugin = new MockPlugin();
444
- plugin.screenshotResult = {
445
- dataUri: "data:image/png;base64,customdata",
446
- };
447
- const result = await plugin.screenshot("task-1");
448
- expect(result.dataUri).toBe("data:image/png;base64,customdata");
449
- });
450
- });