pi-lean-dimension 0.2.3 → 0.3.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 +1 -1
- package/node_modules/pi-lean-portal/AGENTS.md +15 -23
- package/node_modules/pi-lean-portal/README.md +31 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle-profile.test.ts +12 -1
- package/node_modules/pi-lean-portal/__tests__/browser-toggle.test.ts +263 -1102
- package/node_modules/pi-lean-portal/__tests__/fetch-backend.test.ts +6 -32
- package/node_modules/pi-lean-portal/__tests__/helpers/mock-python-bridge.py +1 -1
- package/node_modules/pi-lean-portal/__tests__/helpers/probe-user-backend.ts +1 -2
- package/node_modules/pi-lean-portal/__tests__/helpers/setup-temp-dir.ts +26 -0
- package/node_modules/pi-lean-portal/__tests__/plugin-config-browser.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-contract.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/plugin-loading.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/probe-user-backend.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/python-adapter.test.ts +65 -69
- package/node_modules/pi-lean-portal/__tests__/router-dispatch.test.ts +1 -14
- package/node_modules/pi-lean-portal/__tests__/run-contributed-suites.test.ts +31 -16
- package/node_modules/pi-lean-portal/__tests__/snapshot-cache.test.ts +1 -1
- package/node_modules/pi-lean-portal/__tests__/web-guides.test.ts +146 -19
- package/node_modules/pi-lean-portal/backends/chromium-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/firefox-py/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/playwright-base/playwright-plugin.ts +1 -1
- package/node_modules/pi-lean-portal/backends/python-adapter.ts +3 -3
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__init__.py +2 -4
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/__init__.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-312.pyc +0 -0
- 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/playwright_base.py +572 -81
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/conftest.cpython-313-pytest-9.1.1.pyc +0 -0
- 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_bot_detection.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.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_py_bridges.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/test_playwright_base_quirks.py +13 -43
- package/node_modules/pi-lean-portal/browser-profile.ts +2 -3
- package/node_modules/pi-lean-portal/browser-toggle.ts +168 -380
- package/node_modules/pi-lean-portal/contributed/CHOOSING.md +2 -2
- package/node_modules/pi-lean-portal/contributed/README.md +65 -6
- package/node_modules/pi-lean-portal/contributed/camoufox-py/__pycache__/bridge.cpython-312.pyc +0 -0
- package/node_modules/pi-lean-portal/contributed/camoufox-py/bridge.py +108 -14
- package/node_modules/pi-lean-portal/contributed/camoufox-py/pin.json +4 -0
- package/node_modules/pi-lean-portal/contributed/invisible-py/bridge.py +1 -1
- package/node_modules/pi-lean-portal/core/fetch-backend.ts +10 -30
- package/node_modules/pi-lean-portal/core/guides.ts +14 -18
- package/node_modules/pi-lean-portal/core/router.ts +2 -20
- package/node_modules/pi-lean-portal/core/shared/bot-detection.ts +19 -30
- package/node_modules/pi-lean-portal/core/shared/browser-data.ts +4 -4
- package/node_modules/pi-lean-portal/core/shared/nav-settle.ts +2 -2
- package/node_modules/pi-lean-portal/core/shared/paths.ts +16 -2
- package/node_modules/pi-lean-portal/core/shared/settings-reader.ts +2 -7
- package/node_modules/pi-lean-portal/core/shared/snapshot-cache.ts +1 -1
- package/node_modules/pi-lean-portal/index.ts +8 -1
- package/node_modules/pi-lean-portal/package.json +2 -1
- package/node_modules/pi-lean-portal/tools/web-learn.ts +1 -4
- package/node_modules/pi-lean-search/__tests__/web-search.test.ts +87 -0
- package/node_modules/pi-lean-search/index.ts +134 -64
- package/node_modules/pi-lean-search/package.json +2 -1
- package/node_modules/playwright/README.md +3 -3
- package/node_modules/playwright/lib/agents/copilot-setup-steps.yml +2 -2
- package/node_modules/playwright/lib/cli/reportActions.js +2 -2
- package/node_modules/playwright/lib/common/index.js +77 -52
- package/node_modules/playwright/lib/common/index.js.txt +10 -11
- package/node_modules/playwright/lib/errorContext.js +2 -3
- package/node_modules/playwright/lib/index.js +30 -8
- package/node_modules/playwright/lib/matchers/expect.js +4811 -4379
- package/node_modules/playwright/lib/matchers/expect.js.LICENSE +87 -140
- package/node_modules/playwright/lib/matchers/expect.js.txt +30 -35
- package/node_modules/playwright/lib/program.js +19 -0
- package/node_modules/playwright/lib/runner/index.js +309 -153
- package/node_modules/playwright/lib/runner/index.js.txt +13 -12
- package/node_modules/playwright/lib/transform/babelBundle.js +12899 -11654
- package/node_modules/playwright/lib/transform/babelBundle.js.LICENSE +379 -354
- package/node_modules/playwright/lib/transform/babelBundle.js.txt +39 -39
- package/node_modules/playwright/lib/transform/esmLoader.js +3129 -1116
- package/node_modules/playwright/lib/transform/esmLoader.js.LICENSE +81 -61
- package/node_modules/playwright/lib/transform/esmLoader.js.txt +21 -18
- package/node_modules/playwright/lib/util.js +2 -29
- package/node_modules/playwright/lib/worker/workerProcessEntry.js +14 -32
- package/node_modules/playwright/lib/worker/workerProcessEntry.js.txt +7 -8
- package/node_modules/playwright/package.json +3 -6
- package/node_modules/playwright/types/test.d.ts +391 -4
- package/node_modules/playwright/types/testReporter.d.ts +79 -4
- package/node_modules/playwright-core/browsers.json +12 -18
- package/node_modules/playwright-core/lib/bootstrap.js +1 -1
- package/node_modules/playwright-core/lib/coreBundle.js +11344 -9900
- package/node_modules/playwright-core/lib/server/electron/loader.js +5 -2
- package/node_modules/playwright-core/lib/serverRegistry.js +5 -1
- package/node_modules/playwright-core/lib/tools/cli-client/help.json +21 -6
- package/node_modules/playwright-core/lib/tools/cli-client/program.js +1 -1
- package/node_modules/playwright-core/lib/tools/cli-client/session.js +4 -0
- package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/SKILL.md +21 -5
- package/node_modules/playwright-core/lib/tools/{cli-client/skill/references/spec-driven-testing.md → skills/playwright-cli/references/test-generation.md} +135 -7
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/SKILL.md +143 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/gallery-spec.md +144 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/migration.md +85 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/react.md +67 -0
- package/node_modules/playwright-core/lib/tools/skills/playwright-component-testing/references/vue.md +75 -0
- package/node_modules/playwright-core/lib/tools/utils/extension.js +31 -8
- package/node_modules/playwright-core/lib/utilsBundle.js +37698 -33670
- package/node_modules/playwright-core/lib/utilsBundle.js.LICENSE +391 -268
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-CyWAfh-p.js +11 -0
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-DhC616m4.css +1 -0
- package/node_modules/playwright-core/lib/vite/dashboard/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/htmlReport/report.css +2 -1
- package/node_modules/playwright-core/lib/vite/htmlReport/report.js +15 -55
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule--QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-CwYYHbcZ.js +32 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-DYjdXIbE.js +129 -0
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-l_lX622x.css +1 -0
- package/node_modules/playwright-core/lib/vite/recorder/index.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-By56iMx7.js +32 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-B34OrIms.js +181 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-L3liM589.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-COQkjINf.js +7 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.-QdMvsKi.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.BLFoOugd.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.B_TqY17P.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.Dl36UVQT.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/index.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.B_Jk1wbt.js +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.html +2 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/sw.bundle.js +3 -4
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.C7UW1sC9.css +1 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.D962mr9b.js +5 -0
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.html +5 -5
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.kHJ-D0s7.css +1 -0
- package/node_modules/playwright-core/lib/webp_codec.LICENSE +173 -0
- package/node_modules/playwright-core/lib/webp_codec.wasm +0 -0
- package/node_modules/playwright-core/lib/xdg-open +338 -137
- package/node_modules/playwright-core/package.json +2 -2
- package/node_modules/playwright-core/types/protocol.d.ts +249 -36
- package/node_modules/playwright-core/types/structs.d.ts +3 -1
- package/node_modules/playwright-core/types/types.d.ts +2009 -266
- package/package.json +3 -3
- package/node_modules/pi-lean-portal/__tests__/url-safety.test.ts +0 -188
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/bridge.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/__pycache__/patch_playwright.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/pi_browser_bridge/bridge.py +0 -525
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_accessibility.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_bot_detection.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_browser_data.cpython-313.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_chromium_py_bridge.cpython-313-pytest-9.1.1.pyc +0 -0
- package/node_modules/pi-lean-portal/backends/python-base/tests/__pycache__/test_firefox_py_bridge.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/core/shared/url-safety.ts +0 -164
- package/node_modules/playwright-core/lib/server/deviceDescriptorsSource.json +0 -2739
- package/node_modules/playwright-core/lib/tools/cli-client/skill/references/test-generation.md +0 -134
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-BY2S1tHT.css +0 -1
- package/node_modules/playwright-core/lib/vite/dashboard/assets/index-C_5TMfeg.js +0 -52
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/codeMirrorModule-DeBYQozu.js +0 -32
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-4ZiSSCmn.css +0 -1
- package/node_modules/playwright-core/lib/vite/recorder/assets/index-Bq-mQf8S.js +0 -193
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/codeMirrorModule-LEHpjmcn.js +0 -32
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/defaultSettingsView-BNmKHKpQ.js +0 -264
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/urlMatch-BYQrIQwR.js +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/assets/xtermModule-CsJ4vdCR.js +0 -9
- package/node_modules/playwright-core/lib/vite/traceViewer/codeMirrorModule.DYBRYzYX.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/defaultSettingsView.CjdS-WJx.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.CzXZzn5A.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/index.DMMX1gXU.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/snapshot.v8KI4P3m.js +0 -2
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.BZQ54Kgt.css +0 -1
- package/node_modules/playwright-core/lib/vite/traceViewer/uiMode.Ut8wwJNp.js +0 -6
- package/node_modules/playwright-core/lib/vite/traceViewer/xtermModule.DYP7pi_n.css +0 -32
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/element-attributes.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/playwright-tests.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/request-mocking.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/running-code.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/session-management.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/storage-state.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/tracing.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{cli-client/skill → skills/playwright-cli}/references/video-recording.md +0 -0
- /package/node_modules/playwright-core/lib/tools/{trace → skills/playwright-trace}/SKILL.md +0 -0
|
@@ -1,47 +1,43 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* /web on | off | status commands for toggling browser automation tools.
|
|
2
|
+
* Integration tests for browser-toggle.ts — portal-specific concerns.
|
|
4
3
|
*
|
|
5
|
-
*
|
|
6
|
-
* -
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - Command registration and handler dispatch
|
|
4
|
+
* Invariant tests (peer composition, persist shape, restore) live in the
|
|
5
|
+
* pi-tool-masking library. These tests cover portal-specific wiring:
|
|
6
|
+
* - /web command dispatch
|
|
7
|
+
* - defaultProfile persistence under portal-conversation-state
|
|
8
|
+
* - glyph render on session_start
|
|
9
|
+
* - cached state (getToggleState / getLearnState) reflects library state
|
|
12
10
|
*/
|
|
13
11
|
|
|
14
12
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
15
|
-
import {
|
|
13
|
+
import { EventEmitter } from "node:events";
|
|
16
14
|
import type {
|
|
17
15
|
ExtensionAPI,
|
|
18
16
|
ExtensionContext,
|
|
19
17
|
} from "@earendil-works/pi-coding-agent";
|
|
20
|
-
import
|
|
21
|
-
import {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
persistState,
|
|
26
|
-
restoreFromBranch,
|
|
18
|
+
import { TOOLSET_EVENTS, setDefaultResolutionMode } from "pi-tool-masking";
|
|
19
|
+
import browserToggle, {
|
|
20
|
+
getToggleState,
|
|
21
|
+
getLearnState,
|
|
22
|
+
getConversationDefaultProfile,
|
|
27
23
|
_resetToggleStateForTest,
|
|
28
|
-
readBrowserToggleConfig,
|
|
29
|
-
applyConfigDefault,
|
|
30
|
-
isLearnEnabled,
|
|
31
|
-
applyLearnState,
|
|
32
|
-
type BrowserToggleState,
|
|
33
24
|
} from "../browser-toggle.js";
|
|
34
25
|
|
|
35
|
-
//
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
26
|
+
// Clean globalThis registry between test files
|
|
27
|
+
const REGISTRY_KEY = "__piToolMaskingRegistry";
|
|
28
|
+
const RESTORE_EVENT_KEY = "__piToolMaskingLastRestoreEvent";
|
|
29
|
+
const MODULE_STATE_KEY = "__piToolMaskingModuleState";
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
_resetToggleStateForTest();
|
|
33
|
+
delete (globalThis as any)[REGISTRY_KEY];
|
|
34
|
+
delete (globalThis as any)[RESTORE_EVENT_KEY];
|
|
35
|
+
delete (globalThis as any)[MODULE_STATE_KEY];
|
|
36
|
+
});
|
|
41
37
|
|
|
42
38
|
// ─── Fixtures ────────────────────────────────────────────────────
|
|
43
39
|
|
|
44
|
-
const
|
|
40
|
+
const ALL_TOOLS = [
|
|
45
41
|
{ name: "web-fetch", description: "fetch" },
|
|
46
42
|
{ name: "browser-navigate", description: "navigate" },
|
|
47
43
|
{ name: "browser-snapshot", description: "snapshot" },
|
|
@@ -53,1195 +49,360 @@ const ALL_BROWSER_TOOLS = [
|
|
|
53
49
|
{ name: "browser-console", description: "console" },
|
|
54
50
|
{ name: "browser-inspect", description: "inspect" },
|
|
55
51
|
{ name: "web-guide", description: "guide" },
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
const SOME_BROWSER_TOOLS = [
|
|
59
|
-
{ name: "web-fetch", description: "fetch" },
|
|
60
|
-
{ name: "browser-navigate", description: "navigate" },
|
|
61
|
-
{ name: "browser-console", description: "console" },
|
|
62
|
-
];
|
|
63
|
-
|
|
64
|
-
const NON_BROWSER_TOOLS = [
|
|
52
|
+
{ name: "web-learn", description: "learn" },
|
|
65
53
|
{ name: "read", description: "read files" },
|
|
66
54
|
{ name: "bash", description: "shell" },
|
|
67
55
|
{ name: "edit", description: "edit files" },
|
|
68
56
|
{ name: "write", description: "write files" },
|
|
69
|
-
{ name: "grep", description: "search" },
|
|
70
|
-
{ name: "find", description: "find files" },
|
|
71
|
-
{ name: "ls", description: "list files" },
|
|
72
57
|
];
|
|
73
58
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
59
|
+
const BROWSER_TOOL_NAMES = new Set(
|
|
60
|
+
ALL_TOOLS.filter(
|
|
61
|
+
(t) =>
|
|
62
|
+
t.name !== "web-learn" &&
|
|
63
|
+
t.name !== "read" &&
|
|
64
|
+
t.name !== "bash" &&
|
|
65
|
+
t.name !== "edit" &&
|
|
66
|
+
t.name !== "write",
|
|
67
|
+
).map((t) => t.name),
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
// ─── Mock builder ────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
interface MockPi {
|
|
73
|
+
pi: ExtensionAPI;
|
|
74
|
+
events: EventEmitter;
|
|
75
|
+
handlers: Map<string, Array<(...args: any[]) => void>>;
|
|
76
|
+
entryCalls: Array<{ customType: string; data: unknown }>;
|
|
79
77
|
}
|
|
80
78
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
const allTools = opts.tools ?? [];
|
|
87
|
-
let active = opts.activeTools ?? allTools.map((t) => t.name);
|
|
79
|
+
function mockPi(initialTools?: string[]): MockPi {
|
|
80
|
+
let active = initialTools ?? ALL_TOOLS.map((t) => t.name);
|
|
81
|
+
const eventEmitter = new EventEmitter();
|
|
82
|
+
const handlers = new Map<string, Array<(...args: any[]) => void>>();
|
|
83
|
+
const entryCalls: Array<{ customType: string; data: unknown }> = [];
|
|
88
84
|
|
|
89
|
-
|
|
90
|
-
getAllTools: vi.fn(() =>
|
|
91
|
-
getActiveTools: vi.fn(() => active),
|
|
85
|
+
const pi = {
|
|
86
|
+
getAllTools: vi.fn(() => ALL_TOOLS as any),
|
|
87
|
+
getActiveTools: vi.fn(() => [...active]),
|
|
92
88
|
setActiveTools: vi.fn((names: string[]) => {
|
|
93
|
-
active = names;
|
|
89
|
+
active = [...names];
|
|
90
|
+
}),
|
|
91
|
+
appendEntry: vi.fn((customType: string, data?: unknown) => {
|
|
92
|
+
entryCalls.push({ customType, data });
|
|
94
93
|
}),
|
|
95
|
-
appendEntry: vi.fn(),
|
|
96
94
|
registerCommand: vi.fn(),
|
|
97
|
-
|
|
95
|
+
registerTool: vi.fn(),
|
|
96
|
+
on: vi.fn(<T>(event: string, handler: (event: T, ctx: any) => void) => {
|
|
97
|
+
if (!handlers.has(event)) handlers.set(event, []);
|
|
98
|
+
handlers.get(event)!.push(handler as any);
|
|
99
|
+
}),
|
|
100
|
+
get events() {
|
|
101
|
+
return {
|
|
102
|
+
emit: (channel: string, data: unknown) =>
|
|
103
|
+
eventEmitter.emit(channel, data),
|
|
104
|
+
on: (channel: string, handler: (data: unknown) => void) => {
|
|
105
|
+
eventEmitter.on(channel, handler);
|
|
106
|
+
return () => eventEmitter.off(channel, handler);
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
},
|
|
98
110
|
} as unknown as ExtensionAPI;
|
|
111
|
+
|
|
112
|
+
return { pi, events: eventEmitter, handlers, entryCalls };
|
|
99
113
|
}
|
|
100
114
|
|
|
101
|
-
|
|
102
|
-
* Build a minimal ExtensionContext mock for restoreFromBranch tests.
|
|
103
|
-
*/
|
|
104
|
-
function mockContext(
|
|
105
|
-
branchEntries: Array<{
|
|
106
|
-
type: string;
|
|
107
|
-
customType?: string;
|
|
108
|
-
data?: unknown;
|
|
109
|
-
}>,
|
|
110
|
-
): ExtensionContext {
|
|
115
|
+
function mockCtx(overrides: Partial<ExtensionContext> = {}): any {
|
|
111
116
|
return {
|
|
112
|
-
sessionManager: {
|
|
113
|
-
|
|
117
|
+
sessionManager: { getBranch: () => [] },
|
|
118
|
+
ui: {
|
|
119
|
+
setStatus: vi.fn(),
|
|
120
|
+
theme: { fg: (_c: string, t: string) => t },
|
|
121
|
+
notify: vi.fn(),
|
|
114
122
|
},
|
|
115
|
-
|
|
116
|
-
|
|
123
|
+
mode: "tui",
|
|
124
|
+
cwd: "/mock",
|
|
125
|
+
hasUI: true,
|
|
126
|
+
isIdle: () => true,
|
|
127
|
+
modelRegistry: {} as any,
|
|
128
|
+
...overrides,
|
|
129
|
+
};
|
|
117
130
|
}
|
|
118
131
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
"browser-navigate",
|
|
126
|
-
"browser-snapshot",
|
|
127
|
-
"browser-click",
|
|
128
|
-
"browser-type",
|
|
129
|
-
"browser-scroll",
|
|
130
|
-
"browser-back",
|
|
131
|
-
"browser-press",
|
|
132
|
-
"browser-console",
|
|
133
|
-
"browser-inspect",
|
|
134
|
-
"web-guide",
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
it("returns empty array when no tools are registered", () => {
|
|
138
|
-
const pi = mockPi({ tools: [] });
|
|
139
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([]);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it("returns empty array when only non-matching tools are registered", () => {
|
|
143
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
144
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([]);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it("returns only matching tools when mixed with non-matching tools", () => {
|
|
148
|
-
const pi = mockPi({
|
|
149
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
150
|
-
});
|
|
151
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
152
|
-
ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
153
|
-
);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it("returns all matching tools when all are present", () => {
|
|
157
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
158
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
159
|
-
ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
160
|
-
);
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
it("returns a subset when only some matching tools are registered", () => {
|
|
164
|
-
const pi = mockPi({ tools: SOME_BROWSER_TOOLS });
|
|
165
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(
|
|
166
|
-
SOME_BROWSER_TOOLS.map((t) => t.name),
|
|
167
|
-
);
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
it("is case-sensitive (tool names are always lowercase)", () => {
|
|
171
|
-
const pi = mockPi({
|
|
172
|
-
tools: [
|
|
173
|
-
{ name: "Web-Fetch" },
|
|
174
|
-
{ name: "BROWSER-NAVIGATE" },
|
|
175
|
-
{ name: "web-fetch" },
|
|
176
|
-
],
|
|
177
|
-
});
|
|
178
|
-
// Only the exact-lowercase match should be returned
|
|
179
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual(["web-fetch"]);
|
|
180
|
-
});
|
|
181
|
-
|
|
182
|
-
it("ignores tool metadata beyond the name field", () => {
|
|
183
|
-
const tools = [
|
|
184
|
-
{ name: "web-fetch", description: "a", extra: 1 },
|
|
185
|
-
{ name: "browser-scroll", description: "b", promptGuidelines: [] },
|
|
186
|
-
];
|
|
187
|
-
const pi = mockPi({ tools: tools as any });
|
|
188
|
-
expect(getRegisteredIn(pi, BROWSER_NAMES)).toEqual([
|
|
189
|
-
"web-fetch",
|
|
190
|
-
"browser-scroll",
|
|
191
|
-
]);
|
|
192
|
-
});
|
|
193
|
-
});
|
|
132
|
+
/** Capture the /web command handler from registerCommand. */
|
|
133
|
+
function captureWebHandler(
|
|
134
|
+
pi: ExtensionAPI,
|
|
135
|
+
): (args: string, ctx: any) => Promise<void> {
|
|
136
|
+
return (pi.registerCommand as any).mock.calls[0][1].handler;
|
|
137
|
+
}
|
|
194
138
|
|
|
195
139
|
// ==================================================================
|
|
196
|
-
//
|
|
140
|
+
// Default export
|
|
197
141
|
// ==================================================================
|
|
198
|
-
describe("
|
|
199
|
-
it("
|
|
200
|
-
const pi = mockPi(
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
const pi = mockPi({
|
|
209
|
-
tools: ALL_BROWSER_TOOLS,
|
|
210
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
211
|
-
});
|
|
212
|
-
expect(isBrowserEnabled(pi)).toBe(false);
|
|
213
|
-
});
|
|
214
|
-
|
|
215
|
-
it("returns true when at least one browser tool is active (partial)", () => {
|
|
216
|
-
const pi = mockPi({
|
|
217
|
-
tools: ALL_BROWSER_TOOLS,
|
|
218
|
-
activeTools: ["read", "bash", "web-fetch", "browser-navigate"],
|
|
219
|
-
});
|
|
220
|
-
expect(isBrowserEnabled(pi)).toBe(true);
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it("returns false when no browser tools exist", () => {
|
|
224
|
-
const pi = mockPi({
|
|
225
|
-
tools: NON_BROWSER_TOOLS,
|
|
226
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
227
|
-
});
|
|
228
|
-
expect(isBrowserEnabled(pi)).toBe(false);
|
|
142
|
+
describe("initBrowserToggle", () => {
|
|
143
|
+
it("registers the /web command", () => {
|
|
144
|
+
const { pi } = mockPi();
|
|
145
|
+
browserToggle(pi);
|
|
146
|
+
expect(pi.registerCommand).toHaveBeenCalledWith(
|
|
147
|
+
"web",
|
|
148
|
+
expect.objectContaining({
|
|
149
|
+
description: expect.stringContaining("browser"),
|
|
150
|
+
}),
|
|
151
|
+
);
|
|
229
152
|
});
|
|
230
153
|
|
|
231
|
-
it("
|
|
232
|
-
const pi = mockPi(
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
expect(isBrowserEnabled(pi)).toBe(false);
|
|
154
|
+
it("registers session_start and session_tree handlers", () => {
|
|
155
|
+
const { pi, handlers } = mockPi();
|
|
156
|
+
browserToggle(pi);
|
|
157
|
+
expect(handlers.has("session_start")).toBe(true);
|
|
158
|
+
expect(handlers.has("session_tree")).toBe(true);
|
|
237
159
|
});
|
|
238
160
|
});
|
|
239
161
|
|
|
240
162
|
// ==================================================================
|
|
241
|
-
//
|
|
163
|
+
// /web command dispatch
|
|
242
164
|
// ==================================================================
|
|
243
|
-
describe("
|
|
244
|
-
it("
|
|
245
|
-
const pi = mockPi(
|
|
246
|
-
|
|
247
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
248
|
-
});
|
|
249
|
-
applyBrowserState(pi, true);
|
|
250
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
251
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
252
|
-
expect(calledWith).toEqual(
|
|
253
|
-
expect.arrayContaining(ALL_BROWSER_TOOLS.map((t) => t.name)),
|
|
254
|
-
);
|
|
255
|
-
});
|
|
165
|
+
describe("/web command dispatch", () => {
|
|
166
|
+
it("on — enables web tools, disables learn tools", async () => {
|
|
167
|
+
const { pi } = mockPi([]);
|
|
168
|
+
browserToggle(pi);
|
|
256
169
|
|
|
257
|
-
|
|
258
|
-
const pi = mockPi({
|
|
259
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
260
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
261
|
-
});
|
|
262
|
-
applyBrowserState(pi, true);
|
|
263
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
264
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
265
|
-
for (const tool of NON_BROWSER_TOOLS.map((t) => t.name)) {
|
|
266
|
-
expect(calledWith).toContain(tool);
|
|
267
|
-
}
|
|
268
|
-
});
|
|
170
|
+
await captureWebHandler(pi)("on", mockCtx());
|
|
269
171
|
|
|
270
|
-
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
activeTools: [
|
|
274
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
275
|
-
"web-fetch",
|
|
276
|
-
"browser-navigate",
|
|
277
|
-
],
|
|
278
|
-
});
|
|
279
|
-
applyBrowserState(pi, true);
|
|
280
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
281
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
282
|
-
// Each browser tool appears exactly once
|
|
283
|
-
for (const tool of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
284
|
-
expect(calledWith.filter((n) => n === tool)).toHaveLength(1);
|
|
172
|
+
const finalActive = pi.getActiveTools();
|
|
173
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
174
|
+
expect(finalActive).toContain(name);
|
|
285
175
|
}
|
|
176
|
+
expect(finalActive).not.toContain("web-learn");
|
|
286
177
|
});
|
|
287
178
|
|
|
288
|
-
it("
|
|
289
|
-
const pi = mockPi(
|
|
290
|
-
|
|
291
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
292
|
-
});
|
|
293
|
-
applyBrowserState(pi, true);
|
|
294
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
295
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
296
|
-
const expected = [
|
|
297
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
298
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
299
|
-
];
|
|
300
|
-
expect(new Set(calledWith)).toEqual(new Set(expected));
|
|
301
|
-
});
|
|
179
|
+
it("off — disables web tools (learn cascades off via requires)", async () => {
|
|
180
|
+
const { pi } = mockPi();
|
|
181
|
+
browserToggle(pi);
|
|
302
182
|
|
|
303
|
-
|
|
304
|
-
const pi = mockPi({
|
|
305
|
-
tools: NON_BROWSER_TOOLS,
|
|
306
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
307
|
-
});
|
|
308
|
-
applyBrowserState(pi, true);
|
|
309
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
310
|
-
});
|
|
311
|
-
});
|
|
183
|
+
await captureWebHandler(pi)("off", mockCtx());
|
|
312
184
|
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
317
|
-
activeTools: [
|
|
318
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
319
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
320
|
-
],
|
|
321
|
-
});
|
|
322
|
-
applyBrowserState(pi, false);
|
|
323
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
324
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
325
|
-
for (const tool of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
326
|
-
expect(calledWith).not.toContain(tool);
|
|
185
|
+
const finalActive = pi.getActiveTools();
|
|
186
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
187
|
+
expect(finalActive).not.toContain(name);
|
|
327
188
|
}
|
|
189
|
+
expect(finalActive).not.toContain("web-learn");
|
|
328
190
|
});
|
|
329
191
|
|
|
330
|
-
it("
|
|
331
|
-
const pi = mockPi(
|
|
332
|
-
|
|
333
|
-
activeTools: [
|
|
334
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
335
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
336
|
-
],
|
|
337
|
-
});
|
|
338
|
-
applyBrowserState(pi, false);
|
|
339
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
340
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
341
|
-
for (const tool of NON_BROWSER_TOOLS.map((t) => t.name)) {
|
|
342
|
-
expect(calledWith).toContain(tool);
|
|
343
|
-
}
|
|
344
|
-
});
|
|
345
|
-
|
|
346
|
-
it("is a no-op when no browser tools are registered", () => {
|
|
347
|
-
const pi = mockPi({
|
|
348
|
-
tools: NON_BROWSER_TOOLS,
|
|
349
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
350
|
-
});
|
|
351
|
-
applyBrowserState(pi, false);
|
|
352
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
353
|
-
});
|
|
192
|
+
it("learn — enables both web and learn tools", async () => {
|
|
193
|
+
const { pi } = mockPi([]);
|
|
194
|
+
browserToggle(pi);
|
|
354
195
|
|
|
355
|
-
|
|
356
|
-
const pi = mockPi({
|
|
357
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
358
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
359
|
-
});
|
|
360
|
-
applyBrowserState(pi, false);
|
|
361
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
362
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
363
|
-
expect(calledWith).toEqual(NON_BROWSER_TOOLS.map((t) => t.name));
|
|
364
|
-
});
|
|
196
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
365
197
|
|
|
366
|
-
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
activeTools: [
|
|
370
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
371
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
372
|
-
],
|
|
373
|
-
});
|
|
374
|
-
applyBrowserState(pi, false);
|
|
375
|
-
applyBrowserState(pi, false);
|
|
376
|
-
expect(pi.setActiveTools).toHaveBeenCalledTimes(2);
|
|
377
|
-
// Second call should also produce a clean set (no browser tools)
|
|
378
|
-
const calledWith: string[] = (pi.setActiveTools as ReturnType<typeof vi.fn>)
|
|
379
|
-
.mock.calls[1]![0] as string[];
|
|
380
|
-
for (const tool of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
381
|
-
expect(calledWith).not.toContain(tool);
|
|
198
|
+
const finalActive = pi.getActiveTools();
|
|
199
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
200
|
+
expect(finalActive).toContain(name);
|
|
382
201
|
}
|
|
383
|
-
|
|
384
|
-
});
|
|
385
|
-
|
|
386
|
-
// ==================================================================
|
|
387
|
-
// persistState
|
|
388
|
-
// ==================================================================
|
|
389
|
-
describe("persistState", () => {
|
|
390
|
-
it("calls appendEntry with customType 'web-toggle-state'", () => {
|
|
391
|
-
const pi = mockPi();
|
|
392
|
-
persistState(pi, {
|
|
393
|
-
browserToolsEnabled: true,
|
|
394
|
-
learnToolsEnabled: false,
|
|
395
|
-
defaultProfile: "none",
|
|
396
|
-
});
|
|
397
|
-
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
398
|
-
"web-toggle-state",
|
|
399
|
-
expect.any(Object),
|
|
400
|
-
);
|
|
202
|
+
expect(finalActive).toContain("web-learn");
|
|
401
203
|
});
|
|
402
204
|
|
|
403
|
-
it("
|
|
404
|
-
const pi = mockPi();
|
|
405
|
-
|
|
406
|
-
browserToolsEnabled: true,
|
|
407
|
-
learnToolsEnabled: false,
|
|
408
|
-
defaultProfile: "none",
|
|
409
|
-
});
|
|
410
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
411
|
-
browserToolsEnabled: true,
|
|
412
|
-
learnToolsEnabled: false,
|
|
413
|
-
defaultProfile: "none",
|
|
414
|
-
});
|
|
415
|
-
});
|
|
416
|
-
|
|
417
|
-
it("stores browserToolsEnabled: false and learnToolsEnabled: false on /web off", () => {
|
|
418
|
-
const pi = mockPi();
|
|
419
|
-
persistState(pi, {
|
|
420
|
-
browserToolsEnabled: false,
|
|
421
|
-
learnToolsEnabled: false,
|
|
422
|
-
defaultProfile: "none",
|
|
423
|
-
});
|
|
424
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
425
|
-
browserToolsEnabled: false,
|
|
426
|
-
learnToolsEnabled: false,
|
|
427
|
-
defaultProfile: "none",
|
|
428
|
-
});
|
|
429
|
-
});
|
|
430
|
-
|
|
431
|
-
it("stores browserToolsEnabled: true and learnToolsEnabled: true on /web learn", () => {
|
|
432
|
-
const pi = mockPi();
|
|
433
|
-
persistState(pi, {
|
|
434
|
-
browserToolsEnabled: true,
|
|
435
|
-
learnToolsEnabled: true,
|
|
436
|
-
defaultProfile: "none",
|
|
437
|
-
});
|
|
438
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
439
|
-
browserToolsEnabled: true,
|
|
440
|
-
learnToolsEnabled: true,
|
|
441
|
-
defaultProfile: "none",
|
|
442
|
-
});
|
|
443
|
-
});
|
|
444
|
-
});
|
|
445
|
-
|
|
446
|
-
// ==================================================================
|
|
447
|
-
// getToggleState
|
|
448
|
-
// ==================================================================
|
|
449
|
-
describe("getToggleState", () => {
|
|
450
|
-
beforeEach(() => {
|
|
451
|
-
_resetToggleStateForTest();
|
|
452
|
-
});
|
|
453
|
-
|
|
454
|
-
it("returns true by default (browser tools start enabled)", () => {
|
|
455
|
-
expect(getToggleState()).toBe(true);
|
|
456
|
-
});
|
|
457
|
-
|
|
458
|
-
it("returns false after disabling via applyBrowserState", () => {
|
|
459
|
-
const pi = mockPi({
|
|
460
|
-
tools: ALL_BROWSER_TOOLS,
|
|
461
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
462
|
-
});
|
|
463
|
-
applyBrowserState(pi, false);
|
|
464
|
-
expect(getToggleState()).toBe(false);
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
it("returns true after re-enabling via applyBrowserState", () => {
|
|
468
|
-
const pi = mockPi({
|
|
469
|
-
tools: ALL_BROWSER_TOOLS,
|
|
470
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
471
|
-
});
|
|
472
|
-
applyBrowserState(pi, true);
|
|
473
|
-
expect(getToggleState()).toBe(true);
|
|
474
|
-
});
|
|
475
|
-
|
|
476
|
-
it("is not affected by persistState", () => {
|
|
477
|
-
persistState(mockPi(), {
|
|
478
|
-
browserToolsEnabled: true,
|
|
479
|
-
learnToolsEnabled: false,
|
|
480
|
-
defaultProfile: "none",
|
|
481
|
-
});
|
|
482
|
-
// persistState doesn't call applyBrowserState, so state is unchanged
|
|
483
|
-
expect(getToggleState()).toBe(true);
|
|
484
|
-
});
|
|
485
|
-
|
|
486
|
-
it("returns true when browserToolsEnabled is true (regardless of learn state)", () => {
|
|
487
|
-
const pi = mockPi({
|
|
488
|
-
tools: ALL_BROWSER_TOOLS,
|
|
489
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
490
|
-
});
|
|
491
|
-
// getToggleState returns browserToolsEnabled only (status bar compat)
|
|
492
|
-
applyBrowserState(pi, true);
|
|
493
|
-
applyLearnState(pi, true);
|
|
494
|
-
expect(getToggleState()).toBe(true);
|
|
495
|
-
});
|
|
205
|
+
it("handles unknown arg — shows status, no state change", async () => {
|
|
206
|
+
const { pi } = mockPi([]);
|
|
207
|
+
browserToggle(pi);
|
|
496
208
|
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
tools: ALL_BROWSER_TOOLS,
|
|
500
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
501
|
-
});
|
|
502
|
-
applyBrowserState(pi, false);
|
|
503
|
-
expect(getToggleState()).toBe(false);
|
|
504
|
-
applyBrowserState(pi, true);
|
|
505
|
-
expect(getToggleState()).toBe(true);
|
|
506
|
-
applyBrowserState(pi, false);
|
|
507
|
-
expect(getToggleState()).toBe(false);
|
|
508
|
-
});
|
|
509
|
-
});
|
|
209
|
+
(pi.setActiveTools as any).mockClear();
|
|
210
|
+
await captureWebHandler(pi)("xyz", mockCtx());
|
|
510
211
|
|
|
511
|
-
// ==================================================================
|
|
512
|
-
// restoreFromBranch
|
|
513
|
-
// ==================================================================
|
|
514
|
-
describe("restoreFromBranch", () => {
|
|
515
|
-
it("does nothing when the branch has no saved state", () => {
|
|
516
|
-
const pi = mockPi({
|
|
517
|
-
tools: ALL_BROWSER_TOOLS,
|
|
518
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
519
|
-
});
|
|
520
|
-
const ctx = mockContext([
|
|
521
|
-
{ type: "message", data: { role: "user" } },
|
|
522
|
-
{ type: "message", data: { role: "assistant" } },
|
|
523
|
-
]);
|
|
524
|
-
vi.spyOn(pi, "setActiveTools");
|
|
525
|
-
restoreFromBranch(pi, ctx);
|
|
526
212
|
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
527
213
|
});
|
|
528
214
|
|
|
529
|
-
it("
|
|
530
|
-
const
|
|
531
|
-
|
|
532
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
533
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
534
|
-
});
|
|
535
|
-
const ctx = mockContext([
|
|
536
|
-
{
|
|
537
|
-
type: "custom",
|
|
538
|
-
customType: "web-toggle-state",
|
|
539
|
-
data: {
|
|
540
|
-
browserToolsEnabled: true,
|
|
541
|
-
learnToolsEnabled: false,
|
|
542
|
-
defaultProfile: "none",
|
|
543
|
-
} satisfies BrowserToggleState,
|
|
544
|
-
},
|
|
545
|
-
]);
|
|
546
|
-
restoreFromBranch(pi, ctx);
|
|
547
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
548
|
-
expect(setActive).toHaveBeenCalledWith(expect.arrayContaining(allNames));
|
|
549
|
-
});
|
|
550
|
-
|
|
551
|
-
it("applies saved browserToolsEnabled=false learnToolsEnabled=false state", () => {
|
|
552
|
-
const pi = mockPi({
|
|
553
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
554
|
-
activeTools: [
|
|
555
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
556
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
557
|
-
],
|
|
558
|
-
});
|
|
559
|
-
const ctx = mockContext([
|
|
560
|
-
{
|
|
561
|
-
type: "custom",
|
|
562
|
-
customType: "web-toggle-state",
|
|
563
|
-
data: {
|
|
564
|
-
browserToolsEnabled: false,
|
|
565
|
-
learnToolsEnabled: false,
|
|
566
|
-
defaultProfile: "none",
|
|
567
|
-
} satisfies BrowserToggleState,
|
|
568
|
-
},
|
|
569
|
-
]);
|
|
570
|
-
restoreFromBranch(pi, ctx);
|
|
571
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
572
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
573
|
-
for (const tool of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
574
|
-
expect(calledWith).not.toContain(tool);
|
|
575
|
-
}
|
|
576
|
-
});
|
|
215
|
+
it("handles empty args — shows status, no state change", async () => {
|
|
216
|
+
const { pi } = mockPi([]);
|
|
217
|
+
browserToggle(pi);
|
|
577
218
|
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
const pi = mockPi({
|
|
581
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
582
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
583
|
-
});
|
|
584
|
-
const ctx = mockContext([
|
|
585
|
-
{
|
|
586
|
-
type: "custom",
|
|
587
|
-
customType: "web-toggle-state",
|
|
588
|
-
data: { browserToolsEnabled: true, learnToolsEnabled: false },
|
|
589
|
-
},
|
|
590
|
-
{
|
|
591
|
-
type: "custom",
|
|
592
|
-
customType: "web-toggle-state",
|
|
593
|
-
data: { browserToolsEnabled: false, learnToolsEnabled: false },
|
|
594
|
-
},
|
|
595
|
-
{
|
|
596
|
-
type: "custom",
|
|
597
|
-
customType: "web-toggle-state",
|
|
598
|
-
data: { browserToolsEnabled: true, learnToolsEnabled: true },
|
|
599
|
-
},
|
|
600
|
-
]);
|
|
601
|
-
restoreFromBranch(pi, ctx);
|
|
602
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
603
|
-
expect(setActive).toHaveBeenCalledWith(expect.arrayContaining(allNames));
|
|
604
|
-
});
|
|
219
|
+
(pi.setActiveTools as any).mockClear();
|
|
220
|
+
await captureWebHandler(pi)("", mockCtx());
|
|
605
221
|
|
|
606
|
-
it("does nothing when no browser tools are registered", () => {
|
|
607
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
608
|
-
const ctx = mockContext([
|
|
609
|
-
{
|
|
610
|
-
type: "custom",
|
|
611
|
-
customType: "web-toggle-state",
|
|
612
|
-
data: { enabled: false },
|
|
613
|
-
},
|
|
614
|
-
]);
|
|
615
|
-
vi.spyOn(pi, "setActiveTools");
|
|
616
|
-
restoreFromBranch(pi, ctx);
|
|
617
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
618
|
-
});
|
|
619
|
-
|
|
620
|
-
it("gracefully handles stale state (tool no longer registered)", () => {
|
|
621
|
-
const pi = mockPi({
|
|
622
|
-
tools: NON_BROWSER_TOOLS, // only web-fetch removed from all tools
|
|
623
|
-
activeTools: [...NON_BROWSER_TOOLS.map((t) => t.name), "web-fetch"],
|
|
624
|
-
});
|
|
625
|
-
const ctx = mockContext([
|
|
626
|
-
{
|
|
627
|
-
type: "custom",
|
|
628
|
-
customType: "web-toggle-state",
|
|
629
|
-
data: {
|
|
630
|
-
browserToolsEnabled: false,
|
|
631
|
-
learnToolsEnabled: false,
|
|
632
|
-
defaultProfile: "none",
|
|
633
|
-
} satisfies BrowserToggleState,
|
|
634
|
-
},
|
|
635
|
-
]);
|
|
636
|
-
// Should not throw — with no browser tools currently registered,
|
|
637
|
-
// restoreFromBranch returns early (no-op).
|
|
638
|
-
expect(() => restoreFromBranch(pi, ctx)).not.toThrow();
|
|
639
222
|
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
640
223
|
});
|
|
641
224
|
});
|
|
642
225
|
|
|
643
226
|
// ==================================================================
|
|
644
|
-
//
|
|
227
|
+
// getToggleState / getLearnState
|
|
645
228
|
// ==================================================================
|
|
646
|
-
describe("
|
|
647
|
-
it("
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
651
|
-
});
|
|
652
|
-
browserToggle(pi);
|
|
653
|
-
expect(pi.registerCommand).toHaveBeenCalledWith(
|
|
654
|
-
"web",
|
|
655
|
-
expect.objectContaining({
|
|
656
|
-
description: expect.stringContaining("browser"),
|
|
657
|
-
}),
|
|
658
|
-
);
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
it("registers session_start handler", () => {
|
|
662
|
-
const pi = mockPi();
|
|
663
|
-
browserToggle(pi);
|
|
664
|
-
expect(pi.on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
665
|
-
});
|
|
666
|
-
|
|
667
|
-
it("registers session_tree handler", () => {
|
|
668
|
-
const pi = mockPi();
|
|
669
|
-
browserToggle(pi);
|
|
670
|
-
expect(pi.on).toHaveBeenCalledWith("session_tree", expect.any(Function));
|
|
671
|
-
});
|
|
672
|
-
});
|
|
673
|
-
|
|
674
|
-
describe("command handler dispatch (via factory closure)", () => {
|
|
675
|
-
it("handles 'on' when browsers are off — updates state", () => {
|
|
676
|
-
const pi = mockPi({
|
|
677
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
678
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
679
|
-
});
|
|
680
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
681
|
-
opts.handler("on", {
|
|
682
|
-
ui: {
|
|
683
|
-
notify: vi.fn(),
|
|
684
|
-
setStatus: vi.fn(),
|
|
685
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
686
|
-
},
|
|
687
|
-
} as any);
|
|
688
|
-
}) as any;
|
|
689
|
-
pi.setActiveTools = vi.fn();
|
|
690
|
-
|
|
691
|
-
browserToggle(pi);
|
|
692
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
693
|
-
});
|
|
694
|
-
|
|
695
|
-
it("handles 'on' — does not short-circuit (no early return)", () => {
|
|
696
|
-
// In the new three-state version, 'on' always calls applyBrowserState/applyLearnState
|
|
697
|
-
const pi = mockPi({
|
|
698
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
699
|
-
activeTools: [
|
|
700
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
701
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
702
|
-
],
|
|
703
|
-
});
|
|
704
|
-
pi.setActiveTools = vi.fn();
|
|
705
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
706
|
-
opts.handler("on", {
|
|
707
|
-
ui: {
|
|
708
|
-
notify: vi.fn(),
|
|
709
|
-
setStatus: vi.fn(),
|
|
710
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
711
|
-
},
|
|
712
|
-
} as any);
|
|
713
|
-
}) as any;
|
|
714
|
-
|
|
715
|
-
browserToggle(pi);
|
|
716
|
-
// In the new three-state version, /web on always applies (no short-circuit)
|
|
717
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
it("handles 'off' when browsers are on — updates state", () => {
|
|
721
|
-
const nonBrowserNames = NON_BROWSER_TOOLS.map((t) => t.name);
|
|
722
|
-
const allNames = ALL_BROWSER_TOOLS.map((t) => t.name);
|
|
723
|
-
const pi = mockPi({
|
|
724
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
725
|
-
activeTools: [...nonBrowserNames, ...allNames],
|
|
726
|
-
});
|
|
727
|
-
pi.setActiveTools = vi.fn();
|
|
728
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
729
|
-
opts.handler("off", {
|
|
730
|
-
ui: {
|
|
731
|
-
notify: vi.fn(),
|
|
732
|
-
setStatus: vi.fn(),
|
|
733
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
734
|
-
},
|
|
735
|
-
} as any);
|
|
736
|
-
}) as any;
|
|
737
|
-
|
|
738
|
-
browserToggle(pi);
|
|
739
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
229
|
+
describe("getToggleState / getLearnState", () => {
|
|
230
|
+
it("start true/false by default", () => {
|
|
231
|
+
expect(getToggleState()).toBe(true);
|
|
232
|
+
expect(getLearnState()).toBe(false);
|
|
740
233
|
});
|
|
741
234
|
|
|
742
|
-
it("
|
|
743
|
-
|
|
744
|
-
const pi = mockPi({
|
|
745
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
746
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
747
|
-
});
|
|
748
|
-
pi.setActiveTools = vi.fn();
|
|
749
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
750
|
-
opts.handler("off", {
|
|
751
|
-
ui: {
|
|
752
|
-
notify: vi.fn(),
|
|
753
|
-
setStatus: vi.fn(),
|
|
754
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
755
|
-
},
|
|
756
|
-
} as any);
|
|
757
|
-
}) as any;
|
|
758
|
-
|
|
235
|
+
it("update after /web on and /web off", async () => {
|
|
236
|
+
const { pi } = mockPi();
|
|
759
237
|
browserToggle(pi);
|
|
760
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
761
|
-
});
|
|
762
238
|
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
767
|
-
});
|
|
768
|
-
pi.setActiveTools = vi.fn();
|
|
769
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
770
|
-
opts.handler("learn", {
|
|
771
|
-
ui: {
|
|
772
|
-
notify: vi.fn(),
|
|
773
|
-
setStatus: vi.fn(),
|
|
774
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
775
|
-
},
|
|
776
|
-
} as any);
|
|
777
|
-
}) as any;
|
|
239
|
+
const handler = captureWebHandler(pi);
|
|
240
|
+
await handler("off", mockCtx());
|
|
241
|
+
expect(getToggleState()).toBe(false);
|
|
778
242
|
|
|
779
|
-
|
|
780
|
-
expect(
|
|
243
|
+
await handler("on", mockCtx());
|
|
244
|
+
expect(getToggleState()).toBe(true);
|
|
781
245
|
});
|
|
782
246
|
|
|
783
|
-
it("
|
|
784
|
-
const pi = mockPi(
|
|
785
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
786
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
787
|
-
});
|
|
788
|
-
pi.setActiveTools = vi.fn();
|
|
789
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
790
|
-
opts.handler("xyz", {
|
|
791
|
-
ui: {
|
|
792
|
-
notify: vi.fn(),
|
|
793
|
-
setStatus: vi.fn(),
|
|
794
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
795
|
-
},
|
|
796
|
-
} as any);
|
|
797
|
-
}) as any;
|
|
798
|
-
|
|
247
|
+
it("getLearnState true after /web learn", async () => {
|
|
248
|
+
const { pi } = mockPi([]);
|
|
799
249
|
browserToggle(pi);
|
|
800
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
801
|
-
});
|
|
802
|
-
|
|
803
|
-
it("handles missing pi-lean-portal — does not throw", () => {
|
|
804
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
805
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
806
|
-
opts.handler("on", {
|
|
807
|
-
ui: {
|
|
808
|
-
notify: vi.fn(),
|
|
809
|
-
setStatus: vi.fn(),
|
|
810
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
811
|
-
},
|
|
812
|
-
} as any);
|
|
813
|
-
}) as any;
|
|
814
|
-
// Should not throw even though no browser tools exist
|
|
815
|
-
expect(() => browserToggle(pi)).not.toThrow();
|
|
816
|
-
});
|
|
817
250
|
|
|
818
|
-
|
|
819
|
-
const pi = mockPi({
|
|
820
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
821
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
822
|
-
});
|
|
823
|
-
pi.setActiveTools = vi.fn();
|
|
824
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
825
|
-
opts.handler("", {
|
|
826
|
-
ui: {
|
|
827
|
-
notify: vi.fn(),
|
|
828
|
-
setStatus: vi.fn(),
|
|
829
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
830
|
-
},
|
|
831
|
-
} as any);
|
|
832
|
-
}) as any;
|
|
251
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
833
252
|
|
|
834
|
-
|
|
835
|
-
expect(
|
|
253
|
+
expect(getToggleState()).toBe(true);
|
|
254
|
+
expect(getLearnState()).toBe(true);
|
|
836
255
|
});
|
|
837
256
|
});
|
|
838
257
|
|
|
839
258
|
// ==================================================================
|
|
840
|
-
//
|
|
259
|
+
// appendEntry persist keys
|
|
841
260
|
// ==================================================================
|
|
842
|
-
describe("
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
847
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual([]);
|
|
848
|
-
});
|
|
849
|
-
|
|
850
|
-
it("returns web-learn when it's in the tool list", () => {
|
|
851
|
-
const tools = [
|
|
852
|
-
...ALL_BROWSER_TOOLS,
|
|
853
|
-
{ name: "web-learn", description: "learn" },
|
|
854
|
-
];
|
|
855
|
-
const pi = mockPi({ tools });
|
|
856
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
857
|
-
});
|
|
858
|
-
|
|
859
|
-
it("is case-sensitive (tool names are always lowercase)", () => {
|
|
860
|
-
const tools = [
|
|
861
|
-
...ALL_BROWSER_TOOLS,
|
|
862
|
-
{ name: "Web-Learn" },
|
|
863
|
-
{ name: "web-learn" },
|
|
864
|
-
];
|
|
865
|
-
const pi = mockPi({ tools });
|
|
866
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
867
|
-
});
|
|
868
|
-
});
|
|
261
|
+
describe("persistence key split", () => {
|
|
262
|
+
it("appends toolset-state:portal.web on enable", async () => {
|
|
263
|
+
const { pi } = mockPi([]);
|
|
264
|
+
browserToggle(pi);
|
|
869
265
|
|
|
870
|
-
|
|
871
|
-
it("returns true when web-learn is active", () => {
|
|
872
|
-
const tools = [
|
|
873
|
-
...ALL_BROWSER_TOOLS,
|
|
874
|
-
{ name: "web-learn", description: "learn" },
|
|
875
|
-
];
|
|
876
|
-
const pi = mockPi({
|
|
877
|
-
tools,
|
|
878
|
-
activeTools: [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"],
|
|
879
|
-
});
|
|
880
|
-
expect(isLearnEnabled(pi)).toBe(true);
|
|
881
|
-
});
|
|
266
|
+
await captureWebHandler(pi)("on", mockCtx());
|
|
882
267
|
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
];
|
|
888
|
-
const pi = mockPi({
|
|
889
|
-
tools,
|
|
890
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
891
|
-
});
|
|
892
|
-
expect(isLearnEnabled(pi)).toBe(false);
|
|
268
|
+
const webCalls = (pi.appendEntry as any).mock.calls.filter(
|
|
269
|
+
(c: any) => c[0] === "toolset-state:portal.web",
|
|
270
|
+
);
|
|
271
|
+
expect(webCalls.length).toBeGreaterThanOrEqual(1);
|
|
893
272
|
});
|
|
894
273
|
|
|
895
|
-
it("
|
|
896
|
-
const pi = mockPi(
|
|
897
|
-
|
|
898
|
-
});
|
|
899
|
-
});
|
|
274
|
+
it("appends toolset-state:portal.learn on learn", async () => {
|
|
275
|
+
const { pi } = mockPi([]);
|
|
276
|
+
browserToggle(pi);
|
|
900
277
|
|
|
901
|
-
|
|
902
|
-
it("enable adds web-learn to active set", () => {
|
|
903
|
-
const tools = [
|
|
904
|
-
...ALL_BROWSER_TOOLS,
|
|
905
|
-
{ name: "web-learn", description: "learn" },
|
|
906
|
-
];
|
|
907
|
-
const pi = mockPi({
|
|
908
|
-
tools,
|
|
909
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
910
|
-
});
|
|
911
|
-
applyLearnState(pi, true);
|
|
912
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
913
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
914
|
-
expect(calledWith).toContain("web-learn");
|
|
915
|
-
});
|
|
278
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
916
279
|
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
];
|
|
922
|
-
const allNames = [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"];
|
|
923
|
-
const pi = mockPi({
|
|
924
|
-
tools,
|
|
925
|
-
activeTools: allNames,
|
|
926
|
-
});
|
|
927
|
-
applyLearnState(pi, false);
|
|
928
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
929
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
930
|
-
expect(calledWith).not.toContain("web-learn");
|
|
931
|
-
});
|
|
932
|
-
|
|
933
|
-
it("is a no-op when no learn tools are registered", () => {
|
|
934
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
935
|
-
applyLearnState(pi, true);
|
|
936
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
280
|
+
const learnCalls = (pi.appendEntry as any).mock.calls.filter(
|
|
281
|
+
(c: any) => c[0] === "toolset-state:portal.learn",
|
|
282
|
+
);
|
|
283
|
+
expect(learnCalls.length).toBeGreaterThanOrEqual(1);
|
|
937
284
|
});
|
|
938
285
|
});
|
|
939
286
|
|
|
940
287
|
// ==================================================================
|
|
941
|
-
//
|
|
288
|
+
// session_start — renders glyph
|
|
942
289
|
// ==================================================================
|
|
943
|
-
describe("
|
|
944
|
-
|
|
290
|
+
describe("session_start integration", () => {
|
|
291
|
+
it("renders browser glyph on session_start", async () => {
|
|
292
|
+
const { pi, handlers } = mockPi();
|
|
293
|
+
browserToggle(pi);
|
|
945
294
|
|
|
946
|
-
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
{ name: "web-search", description: "SearXNG search" },
|
|
950
|
-
];
|
|
951
|
-
const pi = mockPi({ tools });
|
|
952
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
953
|
-
});
|
|
295
|
+
const startHandlers = handlers.get("session_start")!;
|
|
296
|
+
const eventObj = {};
|
|
297
|
+
const ctx = mockCtx();
|
|
954
298
|
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
{ name: "web-search-news", description: "news" },
|
|
959
|
-
{ name: "web-search-images", description: "images" },
|
|
960
|
-
];
|
|
961
|
-
const pi = mockPi({ tools });
|
|
962
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual([]);
|
|
963
|
-
});
|
|
299
|
+
for (const h of startHandlers) {
|
|
300
|
+
await h(eventObj, ctx);
|
|
301
|
+
}
|
|
964
302
|
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
{ name: "web-search" },
|
|
970
|
-
];
|
|
971
|
-
const pi = mockPi({ tools });
|
|
972
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
303
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
304
|
+
"browser",
|
|
305
|
+
expect.any(String),
|
|
306
|
+
);
|
|
973
307
|
});
|
|
974
308
|
});
|
|
975
309
|
|
|
976
310
|
// ==================================================================
|
|
977
|
-
//
|
|
311
|
+
// conversation-default-profile
|
|
978
312
|
// ==================================================================
|
|
979
|
-
describe("
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
it("learn state starts disabled", () => {
|
|
985
|
-
// applyConfigDefault should set learnToolsEnabled to false
|
|
986
|
-
const tools = [
|
|
987
|
-
...ALL_BROWSER_TOOLS,
|
|
988
|
-
{ name: "web-learn", description: "learn" },
|
|
989
|
-
];
|
|
990
|
-
const pi = mockPi({
|
|
991
|
-
tools,
|
|
992
|
-
activeTools: [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"],
|
|
993
|
-
});
|
|
994
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
995
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
996
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
997
|
-
);
|
|
998
|
-
applyConfigDefault(pi);
|
|
999
|
-
|
|
1000
|
-
// applyConfigDefault calls setActiveTools twice:
|
|
1001
|
-
// 1. applyBrowserState(true) — adds browser tools
|
|
1002
|
-
// 2. applyLearnState(false) — removes web-learn
|
|
1003
|
-
// Check the *last* call's result (not the first)
|
|
1004
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
1005
|
-
const lastCall = setActive.mock.calls[setActive.mock.calls.length - 1]!;
|
|
1006
|
-
const calledWith: string[] = lastCall[0] as string[];
|
|
1007
|
-
expect(calledWith).not.toContain("web-learn");
|
|
1008
|
-
});
|
|
1009
|
-
|
|
1010
|
-
it("persists learnToolsEnabled: false", () => {
|
|
1011
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
1012
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1013
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
1014
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
1015
|
-
);
|
|
1016
|
-
applyConfigDefault(pi);
|
|
1017
|
-
|
|
1018
|
-
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
1019
|
-
"web-toggle-state",
|
|
1020
|
-
expect.objectContaining({ learnToolsEnabled: false }),
|
|
1021
|
-
);
|
|
313
|
+
describe("getConversationDefaultProfile", () => {
|
|
314
|
+
it("returns undefined initially", () => {
|
|
315
|
+
expect(getConversationDefaultProfile()).toBeUndefined();
|
|
1022
316
|
});
|
|
1023
317
|
});
|
|
1024
318
|
|
|
1025
319
|
// ==================================================================
|
|
1026
|
-
//
|
|
320
|
+
// Focus-mode guard (Fix 3) — /web on/off/learn refuse during inclusion
|
|
1027
321
|
// ==================================================================
|
|
1028
|
-
describe("
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
322
|
+
describe("/web focus-mode guard", () => {
|
|
323
|
+
it("refuses /web on/off/learn while inclusion focus is active", async () => {
|
|
324
|
+
const { pi } = mockPi([]);
|
|
325
|
+
browserToggle(pi);
|
|
326
|
+
setDefaultResolutionMode(pi, "inclusion");
|
|
327
|
+
|
|
328
|
+
for (const sub of ["on", "off", "learn"]) {
|
|
329
|
+
(pi.setActiveTools as any).mockClear();
|
|
330
|
+
const ctx = mockCtx();
|
|
331
|
+
await captureWebHandler(pi)(sub, ctx);
|
|
332
|
+
|
|
333
|
+
expect(ctx.ui.notify).toHaveBeenCalledWith(
|
|
334
|
+
expect.stringContaining("Another plugin has active inclusion mode"),
|
|
335
|
+
"warning",
|
|
336
|
+
);
|
|
337
|
+
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
338
|
+
}
|
|
1042
339
|
});
|
|
1043
340
|
|
|
1044
|
-
it("
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
);
|
|
1048
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1049
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1050
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: false } })
|
|
1051
|
-
: JSON.stringify({ theme: "light" }),
|
|
1052
|
-
);
|
|
1053
|
-
expect(readBrowserToggleConfig()).toBe(false);
|
|
1054
|
-
});
|
|
341
|
+
it("read-only subcommands unaffected by inclusion focus", async () => {
|
|
342
|
+
const { pi } = mockPi([]);
|
|
343
|
+
browserToggle(pi);
|
|
344
|
+
setDefaultResolutionMode(pi, "inclusion");
|
|
1055
345
|
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1060
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: true } })
|
|
1061
|
-
: JSON.stringify({}),
|
|
1062
|
-
);
|
|
1063
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
1064
|
-
});
|
|
346
|
+
for (const sub of ["status", "profile", "cookies", ""]) {
|
|
347
|
+
const ctx = mockCtx();
|
|
348
|
+
await captureWebHandler(pi)(sub, ctx);
|
|
1065
349
|
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
: JSON.stringify({ browserToggle: { defaultEnabled: false } }),
|
|
1072
|
-
);
|
|
1073
|
-
expect(readBrowserToggleConfig()).toBe(false);
|
|
350
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(
|
|
351
|
+
expect.stringContaining("Another plugin has active inclusion mode"),
|
|
352
|
+
"warning",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
1074
355
|
});
|
|
1075
356
|
|
|
1076
|
-
it("
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
});
|
|
357
|
+
it("actuating subcommands work when focus is off (exclusion)", async () => {
|
|
358
|
+
const { pi } = mockPi([]);
|
|
359
|
+
browserToggle(pi);
|
|
360
|
+
// default mode is exclusion after beforeEach reset
|
|
1081
361
|
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1085
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1086
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: "yes" } })
|
|
1087
|
-
: JSON.stringify({}),
|
|
1088
|
-
);
|
|
1089
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
1090
|
-
});
|
|
362
|
+
const ctx = mockCtx();
|
|
363
|
+
await captureWebHandler(pi)("on", ctx);
|
|
1091
364
|
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1096
|
-
? JSON.stringify({ browserToggle: "on" })
|
|
1097
|
-
: JSON.stringify({}),
|
|
365
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(
|
|
366
|
+
expect.stringContaining("Another plugin has active inclusion mode"),
|
|
367
|
+
"warning",
|
|
1098
368
|
);
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1104
|
-
vi.mocked(readFileSync).mockReturnValue("[]");
|
|
1105
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
369
|
+
const finalActive = pi.getActiveTools();
|
|
370
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
371
|
+
expect(finalActive).toContain(name);
|
|
372
|
+
}
|
|
1106
373
|
});
|
|
1107
374
|
});
|
|
1108
375
|
|
|
1109
376
|
// ==================================================================
|
|
1110
|
-
//
|
|
377
|
+
// Glyph sync on external-plugin changed events
|
|
1111
378
|
// ==================================================================
|
|
1112
|
-
describe("
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
it("calls applyBrowserState with the config value (true)", () => {
|
|
1118
|
-
// Config says enabled
|
|
1119
|
-
vi.mocked(existsSync).mockImplementation(
|
|
1120
|
-
(path) => typeof path === "string" && path.includes("settings.json"),
|
|
1121
|
-
);
|
|
1122
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1123
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1124
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: true } })
|
|
1125
|
-
: JSON.stringify({}),
|
|
1126
|
-
);
|
|
1127
|
-
|
|
1128
|
-
const pi = mockPi({
|
|
1129
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
1130
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
1131
|
-
});
|
|
1132
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1133
|
-
|
|
1134
|
-
applyConfigDefault(pi);
|
|
1135
|
-
|
|
1136
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(
|
|
1137
|
-
expect.arrayContaining(ALL_BROWSER_TOOLS.map((t) => t.name)),
|
|
1138
|
-
);
|
|
1139
|
-
});
|
|
1140
|
-
|
|
1141
|
-
it("calls applyBrowserState with the config value (false)", () => {
|
|
1142
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1143
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1144
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1145
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: false } })
|
|
1146
|
-
: JSON.stringify({}),
|
|
1147
|
-
);
|
|
1148
|
-
|
|
1149
|
-
const pi = mockPi({
|
|
1150
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
1151
|
-
activeTools: [
|
|
1152
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
1153
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
1154
|
-
],
|
|
1155
|
-
});
|
|
1156
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1157
|
-
|
|
1158
|
-
applyConfigDefault(pi);
|
|
1159
|
-
|
|
1160
|
-
const calledWith = (pi.setActiveTools as ReturnType<typeof vi.fn>).mock
|
|
1161
|
-
.calls[0]![0] as string[];
|
|
1162
|
-
expect(calledWith).not.toContain("web-fetch");
|
|
1163
|
-
expect(calledWith).not.toContain("browser-navigate");
|
|
1164
|
-
});
|
|
1165
|
-
|
|
1166
|
-
it("persists the applied state", () => {
|
|
1167
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1168
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
1169
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
1170
|
-
);
|
|
379
|
+
describe("external-plugin glyph sync", () => {
|
|
380
|
+
it("re-renders browser glyph off when external plugin fires portal.web disabled", async () => {
|
|
381
|
+
const { pi, handlers, events } = mockPi();
|
|
382
|
+
browserToggle(pi);
|
|
1171
383
|
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
384
|
+
// Fire session_start once to establish _lastCtx.
|
|
385
|
+
// After this, the glyph is rendered correctly for default state.
|
|
386
|
+
const startCtx = mockCtx();
|
|
387
|
+
for (const h of handlers.get("session_start") ?? []) {
|
|
388
|
+
await h({}, startCtx);
|
|
389
|
+
}
|
|
1176
390
|
|
|
1177
|
-
|
|
391
|
+
// Clear the initial render call so we only see the external-plugin re-render.
|
|
392
|
+
vi.clearAllMocks();
|
|
1178
393
|
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
learnToolsEnabled: false,
|
|
1184
|
-
}),
|
|
394
|
+
// Simulate an external plugin disabling portal.web — it has
|
|
395
|
+
// already removed browser tools from the active set.
|
|
396
|
+
const nonBrowser = ALL_TOOLS.map((t) => t.name).filter(
|
|
397
|
+
(n) => !BROWSER_TOOL_NAMES.has(n),
|
|
1185
398
|
);
|
|
1186
|
-
|
|
399
|
+
(pi.getActiveTools as any).mockReturnValue(nonBrowser);
|
|
1187
400
|
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
401
|
+
events.emit(TOOLSET_EVENTS.changed, {
|
|
402
|
+
id: "portal.web",
|
|
403
|
+
enabled: false,
|
|
1192
404
|
});
|
|
1193
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1194
|
-
|
|
1195
|
-
applyConfigDefault(pi);
|
|
1196
|
-
|
|
1197
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
1198
|
-
});
|
|
1199
|
-
});
|
|
1200
|
-
|
|
1201
|
-
// ==================================================================
|
|
1202
|
-
// restoreFromBranch — updated return value
|
|
1203
|
-
// ==================================================================
|
|
1204
|
-
describe("restoreFromBranch (return value)", () => {
|
|
1205
|
-
it("returns true when saved state was found and applied", () => {
|
|
1206
|
-
const pi = mockPi({
|
|
1207
|
-
tools: ALL_BROWSER_TOOLS,
|
|
1208
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
1209
|
-
});
|
|
1210
|
-
const ctx = mockContext([
|
|
1211
|
-
{
|
|
1212
|
-
type: "custom",
|
|
1213
|
-
customType: "web-toggle-state",
|
|
1214
|
-
data: {
|
|
1215
|
-
browserToolsEnabled: true,
|
|
1216
|
-
learnToolsEnabled: false,
|
|
1217
|
-
defaultProfile: "none",
|
|
1218
|
-
} satisfies BrowserToggleState,
|
|
1219
|
-
},
|
|
1220
|
-
]);
|
|
1221
|
-
expect(restoreFromBranch(pi, ctx)).toBe(true);
|
|
1222
|
-
});
|
|
1223
|
-
|
|
1224
|
-
it("returns false when no saved state in branch", () => {
|
|
1225
|
-
const pi = mockPi({
|
|
1226
|
-
tools: ALL_BROWSER_TOOLS,
|
|
1227
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
1228
|
-
});
|
|
1229
|
-
const ctx = mockContext([
|
|
1230
|
-
{ type: "message", data: { role: "user" } },
|
|
1231
|
-
{ type: "message", data: { role: "assistant" } },
|
|
1232
|
-
]);
|
|
1233
|
-
expect(restoreFromBranch(pi, ctx)).toBe(false);
|
|
1234
|
-
});
|
|
1235
405
|
|
|
1236
|
-
|
|
1237
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
1238
|
-
const ctx = mockContext([
|
|
1239
|
-
{
|
|
1240
|
-
type: "custom",
|
|
1241
|
-
customType: "web-toggle-state",
|
|
1242
|
-
data: { browserToolsEnabled: false, learnToolsEnabled: false },
|
|
1243
|
-
},
|
|
1244
|
-
]);
|
|
1245
|
-
expect(restoreFromBranch(pi, ctx)).toBe(false);
|
|
406
|
+
expect(startCtx.ui.setStatus).toHaveBeenCalledWith("browser", "○ web off");
|
|
1246
407
|
});
|
|
1247
408
|
});
|