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
|
@@ -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
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
});
|
|
Binary file
|
package/node_modules/pi-lean-portal/__tests__/helpers/__pycache__/mock-python-bridge.cpython-313.pyc
CHANGED
|
Binary file
|
|
@@ -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
|
|
6
|
-
browser
|
|
7
|
-
|
|
8
|
-
|
|
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":
|
|
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":
|
|
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":
|
|
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": {
|
|
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
|
-
});
|