pi-lean-dimension 0.2.4 → 0.3.1
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 +35 -7
- 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 -1126
- 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/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__/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__/test_playwright_base_quirks.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 -385
- 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/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/bridge.py +0 -525
- 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,1219 +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
|
-
});
|
|
354
|
-
|
|
355
|
-
it("is a no-op when no browser tools are active (removes nothing)", () => {
|
|
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
|
-
});
|
|
192
|
+
it("learn — enables both web and learn tools", async () => {
|
|
193
|
+
const { pi } = mockPi([]);
|
|
194
|
+
browserToggle(pi);
|
|
365
195
|
|
|
366
|
-
|
|
367
|
-
const pi = mockPi({
|
|
368
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
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);
|
|
382
|
-
}
|
|
383
|
-
});
|
|
196
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
384
197
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
// must stay inactive — a toggle affects no tools outside its own set.
|
|
389
|
-
// Regression guard: the previous implementation rebuilt the set from
|
|
390
|
-
// getAllTools(), silently re-activating every registered non-browser
|
|
391
|
-
// tool. We now filter from getActiveTools() so peer toggles compose.
|
|
392
|
-
const OTHER_TOOL = { name: "peer-ext-tool", description: "x" };
|
|
393
|
-
const pi = mockPi({
|
|
394
|
-
tools: [...ALL_BROWSER_TOOLS, OTHER_TOOL, ...NON_BROWSER_TOOLS],
|
|
395
|
-
activeTools: [
|
|
396
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
397
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
398
|
-
], // OTHER_TOOL intentionally absent (off)
|
|
399
|
-
});
|
|
400
|
-
applyBrowserState(pi, false);
|
|
401
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
402
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
403
|
-
expect(calledWith).not.toContain("peer-ext-tool");
|
|
404
|
-
for (const name of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
405
|
-
expect(calledWith).not.toContain(name);
|
|
198
|
+
const finalActive = pi.getActiveTools();
|
|
199
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
200
|
+
expect(finalActive).toContain(name);
|
|
406
201
|
}
|
|
407
|
-
|
|
408
|
-
});
|
|
409
|
-
|
|
410
|
-
// ==================================================================
|
|
411
|
-
// persistState
|
|
412
|
-
// ==================================================================
|
|
413
|
-
describe("persistState", () => {
|
|
414
|
-
it("calls appendEntry with customType 'web-toggle-state'", () => {
|
|
415
|
-
const pi = mockPi();
|
|
416
|
-
persistState(pi, {
|
|
417
|
-
browserToolsEnabled: true,
|
|
418
|
-
learnToolsEnabled: false,
|
|
419
|
-
defaultProfile: "none",
|
|
420
|
-
});
|
|
421
|
-
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
422
|
-
"web-toggle-state",
|
|
423
|
-
expect.any(Object),
|
|
424
|
-
);
|
|
425
|
-
});
|
|
426
|
-
|
|
427
|
-
it("stores browserToolsEnabled: true and learnToolsEnabled: false on /web on", () => {
|
|
428
|
-
const pi = mockPi();
|
|
429
|
-
persistState(pi, {
|
|
430
|
-
browserToolsEnabled: true,
|
|
431
|
-
learnToolsEnabled: false,
|
|
432
|
-
defaultProfile: "none",
|
|
433
|
-
});
|
|
434
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
435
|
-
browserToolsEnabled: true,
|
|
436
|
-
learnToolsEnabled: false,
|
|
437
|
-
defaultProfile: "none",
|
|
438
|
-
});
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
it("stores browserToolsEnabled: false and learnToolsEnabled: false on /web off", () => {
|
|
442
|
-
const pi = mockPi();
|
|
443
|
-
persistState(pi, {
|
|
444
|
-
browserToolsEnabled: false,
|
|
445
|
-
learnToolsEnabled: false,
|
|
446
|
-
defaultProfile: "none",
|
|
447
|
-
});
|
|
448
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
449
|
-
browserToolsEnabled: false,
|
|
450
|
-
learnToolsEnabled: false,
|
|
451
|
-
defaultProfile: "none",
|
|
452
|
-
});
|
|
453
|
-
});
|
|
454
|
-
|
|
455
|
-
it("stores browserToolsEnabled: true and learnToolsEnabled: true on /web learn", () => {
|
|
456
|
-
const pi = mockPi();
|
|
457
|
-
persistState(pi, {
|
|
458
|
-
browserToolsEnabled: true,
|
|
459
|
-
learnToolsEnabled: true,
|
|
460
|
-
defaultProfile: "none",
|
|
461
|
-
});
|
|
462
|
-
expect(pi.appendEntry).toHaveBeenCalledWith("web-toggle-state", {
|
|
463
|
-
browserToolsEnabled: true,
|
|
464
|
-
learnToolsEnabled: true,
|
|
465
|
-
defaultProfile: "none",
|
|
466
|
-
});
|
|
467
|
-
});
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// ==================================================================
|
|
471
|
-
// getToggleState
|
|
472
|
-
// ==================================================================
|
|
473
|
-
describe("getToggleState", () => {
|
|
474
|
-
beforeEach(() => {
|
|
475
|
-
_resetToggleStateForTest();
|
|
476
|
-
});
|
|
477
|
-
|
|
478
|
-
it("returns true by default (browser tools start enabled)", () => {
|
|
479
|
-
expect(getToggleState()).toBe(true);
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
it("returns false after disabling via applyBrowserState", () => {
|
|
483
|
-
const pi = mockPi({
|
|
484
|
-
tools: ALL_BROWSER_TOOLS,
|
|
485
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
486
|
-
});
|
|
487
|
-
applyBrowserState(pi, false);
|
|
488
|
-
expect(getToggleState()).toBe(false);
|
|
489
|
-
});
|
|
490
|
-
|
|
491
|
-
it("returns true after re-enabling via applyBrowserState", () => {
|
|
492
|
-
const pi = mockPi({
|
|
493
|
-
tools: ALL_BROWSER_TOOLS,
|
|
494
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
495
|
-
});
|
|
496
|
-
applyBrowserState(pi, true);
|
|
497
|
-
expect(getToggleState()).toBe(true);
|
|
202
|
+
expect(finalActive).toContain("web-learn");
|
|
498
203
|
});
|
|
499
204
|
|
|
500
|
-
it("
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
learnToolsEnabled: false,
|
|
504
|
-
defaultProfile: "none",
|
|
505
|
-
});
|
|
506
|
-
// persistState doesn't call applyBrowserState, so state is unchanged
|
|
507
|
-
expect(getToggleState()).toBe(true);
|
|
508
|
-
});
|
|
205
|
+
it("handles unknown arg — shows status, no state change", async () => {
|
|
206
|
+
const { pi } = mockPi([]);
|
|
207
|
+
browserToggle(pi);
|
|
509
208
|
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
tools: ALL_BROWSER_TOOLS,
|
|
513
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
514
|
-
});
|
|
515
|
-
// getToggleState returns browserToolsEnabled only (status bar compat)
|
|
516
|
-
applyBrowserState(pi, true);
|
|
517
|
-
applyLearnState(pi, true);
|
|
518
|
-
expect(getToggleState()).toBe(true);
|
|
519
|
-
});
|
|
209
|
+
(pi.setActiveTools as any).mockClear();
|
|
210
|
+
await captureWebHandler(pi)("xyz", mockCtx());
|
|
520
211
|
|
|
521
|
-
it("reflects the most recent call to applyBrowserState", () => {
|
|
522
|
-
const pi = mockPi({
|
|
523
|
-
tools: ALL_BROWSER_TOOLS,
|
|
524
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
525
|
-
});
|
|
526
|
-
applyBrowserState(pi, false);
|
|
527
|
-
expect(getToggleState()).toBe(false);
|
|
528
|
-
applyBrowserState(pi, true);
|
|
529
|
-
expect(getToggleState()).toBe(true);
|
|
530
|
-
applyBrowserState(pi, false);
|
|
531
|
-
expect(getToggleState()).toBe(false);
|
|
532
|
-
});
|
|
533
|
-
});
|
|
534
|
-
|
|
535
|
-
// ==================================================================
|
|
536
|
-
// restoreFromBranch
|
|
537
|
-
// ==================================================================
|
|
538
|
-
describe("restoreFromBranch", () => {
|
|
539
|
-
it("does nothing when the branch has no saved state", () => {
|
|
540
|
-
const pi = mockPi({
|
|
541
|
-
tools: ALL_BROWSER_TOOLS,
|
|
542
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
543
|
-
});
|
|
544
|
-
const ctx = mockContext([
|
|
545
|
-
{ type: "message", data: { role: "user" } },
|
|
546
|
-
{ type: "message", data: { role: "assistant" } },
|
|
547
|
-
]);
|
|
548
|
-
vi.spyOn(pi, "setActiveTools");
|
|
549
|
-
restoreFromBranch(pi, ctx);
|
|
550
212
|
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
551
213
|
});
|
|
552
214
|
|
|
553
|
-
it("
|
|
554
|
-
const
|
|
555
|
-
|
|
556
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
557
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
558
|
-
});
|
|
559
|
-
const ctx = mockContext([
|
|
560
|
-
{
|
|
561
|
-
type: "custom",
|
|
562
|
-
customType: "web-toggle-state",
|
|
563
|
-
data: {
|
|
564
|
-
browserToolsEnabled: true,
|
|
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
|
-
expect(setActive).toHaveBeenCalledWith(expect.arrayContaining(allNames));
|
|
573
|
-
});
|
|
574
|
-
|
|
575
|
-
it("applies saved browserToolsEnabled=false learnToolsEnabled=false state", () => {
|
|
576
|
-
const pi = mockPi({
|
|
577
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
578
|
-
activeTools: [
|
|
579
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
580
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
581
|
-
],
|
|
582
|
-
});
|
|
583
|
-
const ctx = mockContext([
|
|
584
|
-
{
|
|
585
|
-
type: "custom",
|
|
586
|
-
customType: "web-toggle-state",
|
|
587
|
-
data: {
|
|
588
|
-
browserToolsEnabled: false,
|
|
589
|
-
learnToolsEnabled: false,
|
|
590
|
-
defaultProfile: "none",
|
|
591
|
-
} satisfies BrowserToggleState,
|
|
592
|
-
},
|
|
593
|
-
]);
|
|
594
|
-
restoreFromBranch(pi, ctx);
|
|
595
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
596
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
597
|
-
for (const tool of ALL_BROWSER_TOOLS.map((t) => t.name)) {
|
|
598
|
-
expect(calledWith).not.toContain(tool);
|
|
599
|
-
}
|
|
600
|
-
});
|
|
601
|
-
|
|
602
|
-
it("picks the last saved state when multiple toggles exist in branch", () => {
|
|
603
|
-
const allNames = ALL_BROWSER_TOOLS.map((t) => t.name);
|
|
604
|
-
const pi = mockPi({
|
|
605
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
606
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
607
|
-
});
|
|
608
|
-
const ctx = mockContext([
|
|
609
|
-
{
|
|
610
|
-
type: "custom",
|
|
611
|
-
customType: "web-toggle-state",
|
|
612
|
-
data: { browserToolsEnabled: true, learnToolsEnabled: false },
|
|
613
|
-
},
|
|
614
|
-
{
|
|
615
|
-
type: "custom",
|
|
616
|
-
customType: "web-toggle-state",
|
|
617
|
-
data: { browserToolsEnabled: false, learnToolsEnabled: false },
|
|
618
|
-
},
|
|
619
|
-
{
|
|
620
|
-
type: "custom",
|
|
621
|
-
customType: "web-toggle-state",
|
|
622
|
-
data: { browserToolsEnabled: true, learnToolsEnabled: true },
|
|
623
|
-
},
|
|
624
|
-
]);
|
|
625
|
-
restoreFromBranch(pi, ctx);
|
|
626
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
627
|
-
expect(setActive).toHaveBeenCalledWith(expect.arrayContaining(allNames));
|
|
628
|
-
});
|
|
215
|
+
it("handles empty args — shows status, no state change", async () => {
|
|
216
|
+
const { pi } = mockPi([]);
|
|
217
|
+
browserToggle(pi);
|
|
629
218
|
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
const ctx = mockContext([
|
|
633
|
-
{
|
|
634
|
-
type: "custom",
|
|
635
|
-
customType: "web-toggle-state",
|
|
636
|
-
data: { enabled: false },
|
|
637
|
-
},
|
|
638
|
-
]);
|
|
639
|
-
vi.spyOn(pi, "setActiveTools");
|
|
640
|
-
restoreFromBranch(pi, ctx);
|
|
641
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
642
|
-
});
|
|
219
|
+
(pi.setActiveTools as any).mockClear();
|
|
220
|
+
await captureWebHandler(pi)("", mockCtx());
|
|
643
221
|
|
|
644
|
-
it("gracefully handles stale state (tool no longer registered)", () => {
|
|
645
|
-
const pi = mockPi({
|
|
646
|
-
tools: NON_BROWSER_TOOLS, // only web-fetch removed from all tools
|
|
647
|
-
activeTools: [...NON_BROWSER_TOOLS.map((t) => t.name), "web-fetch"],
|
|
648
|
-
});
|
|
649
|
-
const ctx = mockContext([
|
|
650
|
-
{
|
|
651
|
-
type: "custom",
|
|
652
|
-
customType: "web-toggle-state",
|
|
653
|
-
data: {
|
|
654
|
-
browserToolsEnabled: false,
|
|
655
|
-
learnToolsEnabled: false,
|
|
656
|
-
defaultProfile: "none",
|
|
657
|
-
} satisfies BrowserToggleState,
|
|
658
|
-
},
|
|
659
|
-
]);
|
|
660
|
-
// Should not throw — with no browser tools currently registered,
|
|
661
|
-
// restoreFromBranch returns early (no-op).
|
|
662
|
-
expect(() => restoreFromBranch(pi, ctx)).not.toThrow();
|
|
663
222
|
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
664
223
|
});
|
|
665
224
|
});
|
|
666
225
|
|
|
667
226
|
// ==================================================================
|
|
668
|
-
//
|
|
227
|
+
// getToggleState / getLearnState
|
|
669
228
|
// ==================================================================
|
|
670
|
-
describe("
|
|
671
|
-
it("
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
675
|
-
});
|
|
676
|
-
browserToggle(pi);
|
|
677
|
-
expect(pi.registerCommand).toHaveBeenCalledWith(
|
|
678
|
-
"web",
|
|
679
|
-
expect.objectContaining({
|
|
680
|
-
description: expect.stringContaining("browser"),
|
|
681
|
-
}),
|
|
682
|
-
);
|
|
683
|
-
});
|
|
684
|
-
|
|
685
|
-
it("registers session_start handler", () => {
|
|
686
|
-
const pi = mockPi();
|
|
687
|
-
browserToggle(pi);
|
|
688
|
-
expect(pi.on).toHaveBeenCalledWith("session_start", expect.any(Function));
|
|
689
|
-
});
|
|
690
|
-
|
|
691
|
-
it("registers session_tree handler", () => {
|
|
692
|
-
const pi = mockPi();
|
|
693
|
-
browserToggle(pi);
|
|
694
|
-
expect(pi.on).toHaveBeenCalledWith("session_tree", expect.any(Function));
|
|
695
|
-
});
|
|
696
|
-
});
|
|
697
|
-
|
|
698
|
-
describe("command handler dispatch (via factory closure)", () => {
|
|
699
|
-
it("handles 'on' when browsers are off — updates state", () => {
|
|
700
|
-
const pi = mockPi({
|
|
701
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
702
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
703
|
-
});
|
|
704
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
705
|
-
opts.handler("on", {
|
|
706
|
-
ui: {
|
|
707
|
-
notify: vi.fn(),
|
|
708
|
-
setStatus: vi.fn(),
|
|
709
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
710
|
-
},
|
|
711
|
-
} as any);
|
|
712
|
-
}) as any;
|
|
713
|
-
pi.setActiveTools = vi.fn();
|
|
714
|
-
|
|
715
|
-
browserToggle(pi);
|
|
716
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
717
|
-
});
|
|
718
|
-
|
|
719
|
-
it("handles 'on' — does not short-circuit (no early return)", () => {
|
|
720
|
-
// In the new three-state version, 'on' always calls applyBrowserState/applyLearnState
|
|
721
|
-
const pi = mockPi({
|
|
722
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
723
|
-
activeTools: [
|
|
724
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
725
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
726
|
-
],
|
|
727
|
-
});
|
|
728
|
-
pi.setActiveTools = vi.fn();
|
|
729
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
730
|
-
opts.handler("on", {
|
|
731
|
-
ui: {
|
|
732
|
-
notify: vi.fn(),
|
|
733
|
-
setStatus: vi.fn(),
|
|
734
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
735
|
-
},
|
|
736
|
-
} as any);
|
|
737
|
-
}) as any;
|
|
738
|
-
|
|
739
|
-
browserToggle(pi);
|
|
740
|
-
// In the new three-state version, /web on always applies (no short-circuit)
|
|
741
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
742
|
-
});
|
|
743
|
-
|
|
744
|
-
it("handles 'off' when browsers are on — updates state", () => {
|
|
745
|
-
const nonBrowserNames = NON_BROWSER_TOOLS.map((t) => t.name);
|
|
746
|
-
const allNames = ALL_BROWSER_TOOLS.map((t) => t.name);
|
|
747
|
-
const pi = mockPi({
|
|
748
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
749
|
-
activeTools: [...nonBrowserNames, ...allNames],
|
|
750
|
-
});
|
|
751
|
-
pi.setActiveTools = vi.fn();
|
|
752
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
753
|
-
opts.handler("off", {
|
|
754
|
-
ui: {
|
|
755
|
-
notify: vi.fn(),
|
|
756
|
-
setStatus: vi.fn(),
|
|
757
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
758
|
-
},
|
|
759
|
-
} as any);
|
|
760
|
-
}) as any;
|
|
761
|
-
|
|
762
|
-
browserToggle(pi);
|
|
763
|
-
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);
|
|
764
233
|
});
|
|
765
234
|
|
|
766
|
-
it("
|
|
767
|
-
|
|
768
|
-
const pi = mockPi({
|
|
769
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
770
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
771
|
-
});
|
|
772
|
-
pi.setActiveTools = vi.fn();
|
|
773
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
774
|
-
opts.handler("off", {
|
|
775
|
-
ui: {
|
|
776
|
-
notify: vi.fn(),
|
|
777
|
-
setStatus: vi.fn(),
|
|
778
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
779
|
-
},
|
|
780
|
-
} as any);
|
|
781
|
-
}) as any;
|
|
782
|
-
|
|
235
|
+
it("update after /web on and /web off", async () => {
|
|
236
|
+
const { pi } = mockPi();
|
|
783
237
|
browserToggle(pi);
|
|
784
|
-
expect(pi.setActiveTools).toHaveBeenCalled();
|
|
785
|
-
});
|
|
786
238
|
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
791
|
-
});
|
|
792
|
-
pi.setActiveTools = vi.fn();
|
|
793
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
794
|
-
opts.handler("learn", {
|
|
795
|
-
ui: {
|
|
796
|
-
notify: vi.fn(),
|
|
797
|
-
setStatus: vi.fn(),
|
|
798
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
799
|
-
},
|
|
800
|
-
} as any);
|
|
801
|
-
}) as any;
|
|
239
|
+
const handler = captureWebHandler(pi);
|
|
240
|
+
await handler("off", mockCtx());
|
|
241
|
+
expect(getToggleState()).toBe(false);
|
|
802
242
|
|
|
803
|
-
|
|
804
|
-
expect(
|
|
243
|
+
await handler("on", mockCtx());
|
|
244
|
+
expect(getToggleState()).toBe(true);
|
|
805
245
|
});
|
|
806
246
|
|
|
807
|
-
it("
|
|
808
|
-
const pi = mockPi(
|
|
809
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
810
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
811
|
-
});
|
|
812
|
-
pi.setActiveTools = vi.fn();
|
|
813
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
814
|
-
opts.handler("xyz", {
|
|
815
|
-
ui: {
|
|
816
|
-
notify: vi.fn(),
|
|
817
|
-
setStatus: vi.fn(),
|
|
818
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
819
|
-
},
|
|
820
|
-
} as any);
|
|
821
|
-
}) as any;
|
|
822
|
-
|
|
247
|
+
it("getLearnState true after /web learn", async () => {
|
|
248
|
+
const { pi } = mockPi([]);
|
|
823
249
|
browserToggle(pi);
|
|
824
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it("handles missing pi-lean-portal — does not throw", () => {
|
|
828
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
829
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
830
|
-
opts.handler("on", {
|
|
831
|
-
ui: {
|
|
832
|
-
notify: vi.fn(),
|
|
833
|
-
setStatus: vi.fn(),
|
|
834
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
835
|
-
},
|
|
836
|
-
} as any);
|
|
837
|
-
}) as any;
|
|
838
|
-
// Should not throw even though no browser tools exist
|
|
839
|
-
expect(() => browserToggle(pi)).not.toThrow();
|
|
840
|
-
});
|
|
841
250
|
|
|
842
|
-
|
|
843
|
-
const pi = mockPi({
|
|
844
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
845
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
846
|
-
});
|
|
847
|
-
pi.setActiveTools = vi.fn();
|
|
848
|
-
pi.registerCommand = vi.fn((_name, opts) => {
|
|
849
|
-
opts.handler("", {
|
|
850
|
-
ui: {
|
|
851
|
-
notify: vi.fn(),
|
|
852
|
-
setStatus: vi.fn(),
|
|
853
|
-
theme: { fg: vi.fn((_c: string, t: string) => t) },
|
|
854
|
-
},
|
|
855
|
-
} as any);
|
|
856
|
-
}) as any;
|
|
251
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
857
252
|
|
|
858
|
-
|
|
859
|
-
expect(
|
|
253
|
+
expect(getToggleState()).toBe(true);
|
|
254
|
+
expect(getLearnState()).toBe(true);
|
|
860
255
|
});
|
|
861
256
|
});
|
|
862
257
|
|
|
863
258
|
// ==================================================================
|
|
864
|
-
//
|
|
259
|
+
// appendEntry persist keys
|
|
865
260
|
// ==================================================================
|
|
866
|
-
describe("
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
871
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual([]);
|
|
872
|
-
});
|
|
873
|
-
|
|
874
|
-
it("returns web-learn when it's in the tool list", () => {
|
|
875
|
-
const tools = [
|
|
876
|
-
...ALL_BROWSER_TOOLS,
|
|
877
|
-
{ name: "web-learn", description: "learn" },
|
|
878
|
-
];
|
|
879
|
-
const pi = mockPi({ tools });
|
|
880
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
881
|
-
});
|
|
882
|
-
|
|
883
|
-
it("is case-sensitive (tool names are always lowercase)", () => {
|
|
884
|
-
const tools = [
|
|
885
|
-
...ALL_BROWSER_TOOLS,
|
|
886
|
-
{ name: "Web-Learn" },
|
|
887
|
-
{ name: "web-learn" },
|
|
888
|
-
];
|
|
889
|
-
const pi = mockPi({ tools });
|
|
890
|
-
expect(getRegisteredIn(pi, LEARN_NAMES)).toEqual(["web-learn"]);
|
|
891
|
-
});
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
describe("isLearnEnabled", () => {
|
|
895
|
-
it("returns true when web-learn is active", () => {
|
|
896
|
-
const tools = [
|
|
897
|
-
...ALL_BROWSER_TOOLS,
|
|
898
|
-
{ name: "web-learn", description: "learn" },
|
|
899
|
-
];
|
|
900
|
-
const pi = mockPi({
|
|
901
|
-
tools,
|
|
902
|
-
activeTools: [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"],
|
|
903
|
-
});
|
|
904
|
-
expect(isLearnEnabled(pi)).toBe(true);
|
|
905
|
-
});
|
|
261
|
+
describe("persistence key split", () => {
|
|
262
|
+
it("appends toolset-state:pi-lean-dimension.web on enable", async () => {
|
|
263
|
+
const { pi } = mockPi([]);
|
|
264
|
+
browserToggle(pi);
|
|
906
265
|
|
|
907
|
-
|
|
908
|
-
const tools = [
|
|
909
|
-
...ALL_BROWSER_TOOLS,
|
|
910
|
-
{ name: "web-learn", description: "learn" },
|
|
911
|
-
];
|
|
912
|
-
const pi = mockPi({
|
|
913
|
-
tools,
|
|
914
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
915
|
-
});
|
|
916
|
-
expect(isLearnEnabled(pi)).toBe(false);
|
|
917
|
-
});
|
|
266
|
+
await captureWebHandler(pi)("on", mockCtx());
|
|
918
267
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
268
|
+
const webCalls = (pi.appendEntry as any).mock.calls.filter(
|
|
269
|
+
(c: any) => c[0] === "toolset-state:pi-lean-dimension.web",
|
|
270
|
+
);
|
|
271
|
+
expect(webCalls.length).toBeGreaterThanOrEqual(1);
|
|
922
272
|
});
|
|
923
|
-
});
|
|
924
273
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
...ALL_BROWSER_TOOLS,
|
|
929
|
-
{ name: "web-learn", description: "learn" },
|
|
930
|
-
];
|
|
931
|
-
const pi = mockPi({
|
|
932
|
-
tools,
|
|
933
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
934
|
-
});
|
|
935
|
-
applyLearnState(pi, true);
|
|
936
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
937
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
938
|
-
expect(calledWith).toContain("web-learn");
|
|
939
|
-
});
|
|
274
|
+
it("appends toolset-state:pi-lean-dimension.web-learn on learn", async () => {
|
|
275
|
+
const { pi } = mockPi([]);
|
|
276
|
+
browserToggle(pi);
|
|
940
277
|
|
|
941
|
-
|
|
942
|
-
const tools = [
|
|
943
|
-
...ALL_BROWSER_TOOLS,
|
|
944
|
-
{ name: "web-learn", description: "learn" },
|
|
945
|
-
];
|
|
946
|
-
const allNames = [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"];
|
|
947
|
-
const pi = mockPi({
|
|
948
|
-
tools,
|
|
949
|
-
activeTools: allNames,
|
|
950
|
-
});
|
|
951
|
-
applyLearnState(pi, false);
|
|
952
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
953
|
-
const calledWith: string[] = setActive.mock.calls[0]![0] as string[];
|
|
954
|
-
expect(calledWith).not.toContain("web-learn");
|
|
955
|
-
});
|
|
278
|
+
await captureWebHandler(pi)("learn", mockCtx());
|
|
956
279
|
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
expect(
|
|
280
|
+
const learnCalls = (pi.appendEntry as any).mock.calls.filter(
|
|
281
|
+
(c: any) => c[0] === "toolset-state:pi-lean-dimension.web-learn",
|
|
282
|
+
);
|
|
283
|
+
expect(learnCalls.length).toBeGreaterThanOrEqual(1);
|
|
961
284
|
});
|
|
962
285
|
});
|
|
963
286
|
|
|
964
287
|
// ==================================================================
|
|
965
|
-
//
|
|
288
|
+
// session_start — renders glyph
|
|
966
289
|
// ==================================================================
|
|
967
|
-
describe("
|
|
968
|
-
|
|
290
|
+
describe("session_start integration", () => {
|
|
291
|
+
it("renders browser glyph on session_start", async () => {
|
|
292
|
+
const { pi, handlers } = mockPi();
|
|
293
|
+
browserToggle(pi);
|
|
969
294
|
|
|
970
|
-
|
|
971
|
-
const
|
|
972
|
-
|
|
973
|
-
{ name: "web-search", description: "SearXNG search" },
|
|
974
|
-
];
|
|
975
|
-
const pi = mockPi({ tools });
|
|
976
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
977
|
-
});
|
|
295
|
+
const startHandlers = handlers.get("session_start")!;
|
|
296
|
+
const eventObj = {};
|
|
297
|
+
const ctx = mockCtx();
|
|
978
298
|
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
{ name: "web-search-news", description: "news" },
|
|
983
|
-
{ name: "web-search-images", description: "images" },
|
|
984
|
-
];
|
|
985
|
-
const pi = mockPi({ tools });
|
|
986
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual([]);
|
|
987
|
-
});
|
|
299
|
+
for (const h of startHandlers) {
|
|
300
|
+
await h(eventObj, ctx);
|
|
301
|
+
}
|
|
988
302
|
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
{ name: "web-search" },
|
|
994
|
-
];
|
|
995
|
-
const pi = mockPi({ tools });
|
|
996
|
-
expect(getRegisteredIn(pi, SIBLING_NAMES)).toEqual(["web-search"]);
|
|
303
|
+
expect(ctx.ui.setStatus).toHaveBeenCalledWith(
|
|
304
|
+
"browser",
|
|
305
|
+
expect.any(String),
|
|
306
|
+
);
|
|
997
307
|
});
|
|
998
308
|
});
|
|
999
309
|
|
|
1000
310
|
// ==================================================================
|
|
1001
|
-
//
|
|
311
|
+
// conversation-default-profile
|
|
1002
312
|
// ==================================================================
|
|
1003
|
-
describe("
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
});
|
|
1007
|
-
|
|
1008
|
-
it("learn state starts disabled", () => {
|
|
1009
|
-
// applyConfigDefault should set learnToolsEnabled to false
|
|
1010
|
-
const tools = [
|
|
1011
|
-
...ALL_BROWSER_TOOLS,
|
|
1012
|
-
{ name: "web-learn", description: "learn" },
|
|
1013
|
-
];
|
|
1014
|
-
const pi = mockPi({
|
|
1015
|
-
tools,
|
|
1016
|
-
activeTools: [...ALL_BROWSER_TOOLS.map((t) => t.name), "web-learn"],
|
|
1017
|
-
});
|
|
1018
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1019
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
1020
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
1021
|
-
);
|
|
1022
|
-
applyConfigDefault(pi);
|
|
1023
|
-
|
|
1024
|
-
// applyConfigDefault calls setActiveTools twice:
|
|
1025
|
-
// 1. applyBrowserState(true) — adds browser tools
|
|
1026
|
-
// 2. applyLearnState(false) — removes web-learn
|
|
1027
|
-
// Check the *last* call's result (not the first)
|
|
1028
|
-
const setActive = pi.setActiveTools as ReturnType<typeof vi.fn>;
|
|
1029
|
-
const lastCall = setActive.mock.calls[setActive.mock.calls.length - 1]!;
|
|
1030
|
-
const calledWith: string[] = lastCall[0] as string[];
|
|
1031
|
-
expect(calledWith).not.toContain("web-learn");
|
|
1032
|
-
});
|
|
1033
|
-
|
|
1034
|
-
it("persists learnToolsEnabled: false", () => {
|
|
1035
|
-
const pi = mockPi({ tools: ALL_BROWSER_TOOLS });
|
|
1036
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1037
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
1038
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
1039
|
-
);
|
|
1040
|
-
applyConfigDefault(pi);
|
|
1041
|
-
|
|
1042
|
-
expect(pi.appendEntry).toHaveBeenCalledWith(
|
|
1043
|
-
"web-toggle-state",
|
|
1044
|
-
expect.objectContaining({ learnToolsEnabled: false }),
|
|
1045
|
-
);
|
|
313
|
+
describe("getConversationDefaultProfile", () => {
|
|
314
|
+
it("returns undefined initially", () => {
|
|
315
|
+
expect(getConversationDefaultProfile()).toBeUndefined();
|
|
1046
316
|
});
|
|
1047
317
|
});
|
|
1048
318
|
|
|
1049
319
|
// ==================================================================
|
|
1050
|
-
//
|
|
320
|
+
// Focus-mode guard (Fix 3) — /web on/off/learn refuse during inclusion
|
|
1051
321
|
// ==================================================================
|
|
1052
|
-
describe("
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
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
|
+
}
|
|
1066
339
|
});
|
|
1067
340
|
|
|
1068
|
-
it("
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
);
|
|
1072
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1073
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1074
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: false } })
|
|
1075
|
-
: JSON.stringify({ theme: "light" }),
|
|
1076
|
-
);
|
|
1077
|
-
expect(readBrowserToggleConfig()).toBe(false);
|
|
1078
|
-
});
|
|
341
|
+
it("read-only subcommands unaffected by inclusion focus", async () => {
|
|
342
|
+
const { pi } = mockPi([]);
|
|
343
|
+
browserToggle(pi);
|
|
344
|
+
setDefaultResolutionMode(pi, "inclusion");
|
|
1079
345
|
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1084
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: true } })
|
|
1085
|
-
: JSON.stringify({}),
|
|
1086
|
-
);
|
|
1087
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
1088
|
-
});
|
|
346
|
+
for (const sub of ["status", "profile", "cookies", ""]) {
|
|
347
|
+
const ctx = mockCtx();
|
|
348
|
+
await captureWebHandler(pi)(sub, ctx);
|
|
1089
349
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
: JSON.stringify({ browserToggle: { defaultEnabled: false } }),
|
|
1096
|
-
);
|
|
1097
|
-
expect(readBrowserToggleConfig()).toBe(false);
|
|
350
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(
|
|
351
|
+
expect.stringContaining("Another plugin has active inclusion mode"),
|
|
352
|
+
"warning",
|
|
353
|
+
);
|
|
354
|
+
}
|
|
1098
355
|
});
|
|
1099
356
|
|
|
1100
|
-
it("
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
});
|
|
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
|
|
1105
361
|
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1109
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1110
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: "yes" } })
|
|
1111
|
-
: JSON.stringify({}),
|
|
1112
|
-
);
|
|
1113
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
1114
|
-
});
|
|
362
|
+
const ctx = mockCtx();
|
|
363
|
+
await captureWebHandler(pi)("on", ctx);
|
|
1115
364
|
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1120
|
-
? JSON.stringify({ browserToggle: "on" })
|
|
1121
|
-
: JSON.stringify({}),
|
|
365
|
+
expect(ctx.ui.notify).not.toHaveBeenCalledWith(
|
|
366
|
+
expect.stringContaining("Another plugin has active inclusion mode"),
|
|
367
|
+
"warning",
|
|
1122
368
|
);
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1128
|
-
vi.mocked(readFileSync).mockReturnValue("[]");
|
|
1129
|
-
expect(readBrowserToggleConfig()).toBe(true);
|
|
369
|
+
const finalActive = pi.getActiveTools();
|
|
370
|
+
for (const name of BROWSER_TOOL_NAMES) {
|
|
371
|
+
expect(finalActive).toContain(name);
|
|
372
|
+
}
|
|
1130
373
|
});
|
|
1131
374
|
});
|
|
1132
375
|
|
|
1133
376
|
// ==================================================================
|
|
1134
|
-
//
|
|
377
|
+
// Glyph sync on external-plugin changed events
|
|
1135
378
|
// ==================================================================
|
|
1136
|
-
describe("
|
|
1137
|
-
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
it("calls applyBrowserState with the config value (true)", () => {
|
|
1142
|
-
// Config says enabled
|
|
1143
|
-
vi.mocked(existsSync).mockImplementation(
|
|
1144
|
-
(path) => typeof path === "string" && path.includes("settings.json"),
|
|
1145
|
-
);
|
|
1146
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1147
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1148
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: true } })
|
|
1149
|
-
: JSON.stringify({}),
|
|
1150
|
-
);
|
|
1151
|
-
|
|
1152
|
-
const pi = mockPi({
|
|
1153
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
1154
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name), // browsers off
|
|
1155
|
-
});
|
|
1156
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1157
|
-
|
|
1158
|
-
applyConfigDefault(pi);
|
|
1159
|
-
|
|
1160
|
-
expect(pi.setActiveTools).toHaveBeenCalledWith(
|
|
1161
|
-
expect.arrayContaining(ALL_BROWSER_TOOLS.map((t) => t.name)),
|
|
1162
|
-
);
|
|
1163
|
-
});
|
|
1164
|
-
|
|
1165
|
-
it("calls applyBrowserState with the config value (false)", () => {
|
|
1166
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1167
|
-
vi.mocked(readFileSync).mockImplementation((path: unknown) =>
|
|
1168
|
-
typeof path === "string" && path.includes(".pi/agent/settings")
|
|
1169
|
-
? JSON.stringify({ browserToggle: { defaultEnabled: false } })
|
|
1170
|
-
: JSON.stringify({}),
|
|
1171
|
-
);
|
|
1172
|
-
|
|
1173
|
-
const pi = mockPi({
|
|
1174
|
-
tools: [...ALL_BROWSER_TOOLS, ...NON_BROWSER_TOOLS],
|
|
1175
|
-
activeTools: [
|
|
1176
|
-
...NON_BROWSER_TOOLS.map((t) => t.name),
|
|
1177
|
-
...ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
1178
|
-
],
|
|
1179
|
-
});
|
|
1180
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1181
|
-
|
|
1182
|
-
applyConfigDefault(pi);
|
|
1183
|
-
|
|
1184
|
-
const calledWith = (pi.setActiveTools as ReturnType<typeof vi.fn>).mock
|
|
1185
|
-
.calls[0]![0] as string[];
|
|
1186
|
-
expect(calledWith).not.toContain("web-fetch");
|
|
1187
|
-
expect(calledWith).not.toContain("browser-navigate");
|
|
1188
|
-
});
|
|
1189
|
-
|
|
1190
|
-
it("persists the applied state", () => {
|
|
1191
|
-
vi.mocked(existsSync).mockReturnValue(true);
|
|
1192
|
-
vi.mocked(readFileSync).mockReturnValue(
|
|
1193
|
-
JSON.stringify({ browserToggle: { defaultEnabled: true } }),
|
|
1194
|
-
);
|
|
379
|
+
describe("external-plugin glyph sync", () => {
|
|
380
|
+
it("re-renders browser glyph off when external plugin fires pi-lean-dimension.web disabled", async () => {
|
|
381
|
+
const { pi, handlers, events } = mockPi();
|
|
382
|
+
browserToggle(pi);
|
|
1195
383
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
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
|
+
}
|
|
1200
390
|
|
|
1201
|
-
|
|
391
|
+
// Clear the initial render call so we only see the external-plugin re-render.
|
|
392
|
+
vi.clearAllMocks();
|
|
1202
393
|
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
learnToolsEnabled: false,
|
|
1208
|
-
}),
|
|
394
|
+
// Simulate an external plugin disabling pi-lean-dimension.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),
|
|
1209
398
|
);
|
|
1210
|
-
|
|
399
|
+
(pi.getActiveTools as any).mockReturnValue(nonBrowser);
|
|
1211
400
|
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
401
|
+
events.emit(TOOLSET_EVENTS.changed, {
|
|
402
|
+
id: "pi-lean-dimension.web",
|
|
403
|
+
enabled: false,
|
|
1216
404
|
});
|
|
1217
|
-
vi.spyOn(pi, "setActiveTools");
|
|
1218
|
-
|
|
1219
|
-
applyConfigDefault(pi);
|
|
1220
|
-
|
|
1221
|
-
expect(pi.setActiveTools).not.toHaveBeenCalled();
|
|
1222
|
-
});
|
|
1223
|
-
});
|
|
1224
|
-
|
|
1225
|
-
// ==================================================================
|
|
1226
|
-
// restoreFromBranch — updated return value
|
|
1227
|
-
// ==================================================================
|
|
1228
|
-
describe("restoreFromBranch (return value)", () => {
|
|
1229
|
-
it("returns true when saved state was found and applied", () => {
|
|
1230
|
-
const pi = mockPi({
|
|
1231
|
-
tools: ALL_BROWSER_TOOLS,
|
|
1232
|
-
activeTools: NON_BROWSER_TOOLS.map((t) => t.name),
|
|
1233
|
-
});
|
|
1234
|
-
const ctx = mockContext([
|
|
1235
|
-
{
|
|
1236
|
-
type: "custom",
|
|
1237
|
-
customType: "web-toggle-state",
|
|
1238
|
-
data: {
|
|
1239
|
-
browserToolsEnabled: true,
|
|
1240
|
-
learnToolsEnabled: false,
|
|
1241
|
-
defaultProfile: "none",
|
|
1242
|
-
} satisfies BrowserToggleState,
|
|
1243
|
-
},
|
|
1244
|
-
]);
|
|
1245
|
-
expect(restoreFromBranch(pi, ctx)).toBe(true);
|
|
1246
|
-
});
|
|
1247
|
-
|
|
1248
|
-
it("returns false when no saved state in branch", () => {
|
|
1249
|
-
const pi = mockPi({
|
|
1250
|
-
tools: ALL_BROWSER_TOOLS,
|
|
1251
|
-
activeTools: ALL_BROWSER_TOOLS.map((t) => t.name),
|
|
1252
|
-
});
|
|
1253
|
-
const ctx = mockContext([
|
|
1254
|
-
{ type: "message", data: { role: "user" } },
|
|
1255
|
-
{ type: "message", data: { role: "assistant" } },
|
|
1256
|
-
]);
|
|
1257
|
-
expect(restoreFromBranch(pi, ctx)).toBe(false);
|
|
1258
|
-
});
|
|
1259
405
|
|
|
1260
|
-
|
|
1261
|
-
const pi = mockPi({ tools: NON_BROWSER_TOOLS });
|
|
1262
|
-
const ctx = mockContext([
|
|
1263
|
-
{
|
|
1264
|
-
type: "custom",
|
|
1265
|
-
customType: "web-toggle-state",
|
|
1266
|
-
data: { browserToolsEnabled: false, learnToolsEnabled: false },
|
|
1267
|
-
},
|
|
1268
|
-
]);
|
|
1269
|
-
expect(restoreFromBranch(pi, ctx)).toBe(false);
|
|
406
|
+
expect(startCtx.ui.setStatus).toHaveBeenCalledWith("browser", "○ web off");
|
|
1270
407
|
});
|
|
1271
408
|
});
|