@tooluminati/testing 0.1.0 → 0.2.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/dist/browser-helpers-V33B4UD5.js +8 -0
- package/dist/browser-helpers.d.ts +5 -1
- package/dist/browser-helpers.d.ts.map +1 -1
- package/dist/browser-helpers.js +5 -4
- package/dist/chunk-K6KLEZGY.js +146 -0
- package/dist/chunk-M554RXAL.js +34 -0
- package/dist/comparative-proof-IVDBBIIG.js +11 -0
- package/dist/devtools-mcp-helper.d.ts +4 -3
- package/dist/devtools-mcp-helper.d.ts.map +1 -1
- package/dist/devtools-mcp-helper.js +7 -5
- package/dist/index.cjs +36 -11
- package/dist/index.js +31 -11
- package/dist/invocation.test.d.ts +2 -0
- package/dist/invocation.test.d.ts.map +1 -0
- package/dist/invocation.test.js +95 -0
- package/dist/mock-model-context.d.ts +4 -3
- package/dist/mock-model-context.d.ts.map +1 -1
- package/dist/mock-model-context.js +9 -3
- package/dist/mock-model-context.test.d.ts +2 -0
- package/dist/mock-model-context.test.d.ts.map +1 -0
- package/dist/mock-model-context.test.js +106 -0
- package/dist/model-context-mock-script.d.ts +1 -1
- package/dist/model-context-mock-script.d.ts.map +1 -1
- package/dist/model-context-mock-script.js +11 -2
- package/package.json +2 -2
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
export interface WebMcpInvocationOptions {
|
|
2
|
+
/** Select explicitly for browsers that require JSON string input. Never retries execution. */
|
|
3
|
+
inputFormat?: 'object' | 'json-string';
|
|
4
|
+
}
|
|
1
5
|
export interface WebMcpTestPage {
|
|
2
6
|
evaluate<T, A>(callback: (arg: A) => T | Promise<T>, arg: A): Promise<T>;
|
|
3
7
|
}
|
|
4
8
|
export declare function expectWebMcpTool(page: WebMcpTestPage, name: string): Promise<void>;
|
|
5
|
-
export declare function invokeWebMcpTool<T>(page: WebMcpTestPage, name: string, args: unknown): Promise<T>;
|
|
9
|
+
export declare function invokeWebMcpTool<T>(page: WebMcpTestPage, name: string, args: unknown, options?: WebMcpInvocationOptions): Promise<T>;
|
|
6
10
|
//# sourceMappingURL=browser-helpers.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"browser-helpers.d.ts","sourceRoot":"","sources":["../src/browser-helpers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,CAAC,
|
|
1
|
+
{"version":3,"file":"browser-helpers.d.ts","sourceRoot":"","sources":["../src/browser-helpers.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,uBAAuB;IACtC,8FAA8F;IAC9F,WAAW,CAAC,EAAE,QAAQ,GAAG,aAAa,CAAC;CACxC;AAED,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,QAAQ,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,EAAE,GAAG,EAAE,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;CAC1E;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC,IAAI,CAAC,CAef;AAED,wBAAsB,gBAAgB,CAAC,CAAC,EACtC,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,OAAO,EACb,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,CAAC,CAAC,CA4BZ"}
|
package/dist/browser-helpers.js
CHANGED
|
@@ -9,8 +9,8 @@ export async function expectWebMcpTool(page, name) {
|
|
|
9
9
|
throw new Error(`Expected WebMCP tool "${name}" to be registered.`);
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
-
export async function invokeWebMcpTool(page, name, args) {
|
|
13
|
-
return page.evaluate(async ({ toolName, input }) => {
|
|
12
|
+
export async function invokeWebMcpTool(page, name, args, options = {}) {
|
|
13
|
+
return page.evaluate(async ({ toolName, input, inputFormat }) => {
|
|
14
14
|
const context = document
|
|
15
15
|
.modelContext;
|
|
16
16
|
const tools = (await context?.getTools?.()) ?? [];
|
|
@@ -18,6 +18,7 @@ export async function invokeWebMcpTool(page, name, args) {
|
|
|
18
18
|
if (!tool || !context?.executeTool) {
|
|
19
19
|
throw new Error(`WebMCP tool "${toolName}" cannot be invoked.`);
|
|
20
20
|
}
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
const payload = input && typeof input === 'object' ? input : {};
|
|
22
|
+
return context.executeTool(tool, inputFormat === 'json-string' ? JSON.stringify(payload) : payload);
|
|
23
|
+
}, { toolName: name, input: args, inputFormat: options.inputFormat });
|
|
23
24
|
}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import {
|
|
2
|
+
invokeWebMcpTool
|
|
3
|
+
} from "./chunk-M554RXAL.js";
|
|
4
|
+
|
|
5
|
+
// src/dom-only-inspection.ts
|
|
6
|
+
function pushUnique(target, value) {
|
|
7
|
+
const trimmed = value?.trim();
|
|
8
|
+
if (!trimmed || target.includes(trimmed)) {
|
|
9
|
+
return;
|
|
10
|
+
}
|
|
11
|
+
target.push(trimmed);
|
|
12
|
+
}
|
|
13
|
+
function collectDisabledActionReasonsFromDom(root, buttonLabel) {
|
|
14
|
+
const buttons = [...root.querySelectorAll("button")];
|
|
15
|
+
const button = buttons.find(
|
|
16
|
+
(candidate) => candidate.textContent?.includes(buttonLabel)
|
|
17
|
+
);
|
|
18
|
+
if (!button) {
|
|
19
|
+
return {
|
|
20
|
+
label: buttonLabel,
|
|
21
|
+
disabled: false,
|
|
22
|
+
visibleBlockerReasons: []
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
const visibleBlockerReasons = [];
|
|
26
|
+
pushUnique(visibleBlockerReasons, button.getAttribute("title"));
|
|
27
|
+
pushUnique(
|
|
28
|
+
visibleBlockerReasons,
|
|
29
|
+
button.getAttribute("aria-label")?.includes("because") ? button.getAttribute("aria-label") : null
|
|
30
|
+
);
|
|
31
|
+
const describedBy = button.getAttribute("aria-describedby");
|
|
32
|
+
if (describedBy) {
|
|
33
|
+
for (const id of describedBy.split(/\s+/)) {
|
|
34
|
+
pushUnique(
|
|
35
|
+
visibleBlockerReasons,
|
|
36
|
+
root.getElementById(id)?.textContent
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const fieldset = button.closest("fieldset");
|
|
41
|
+
if (fieldset) {
|
|
42
|
+
pushUnique(
|
|
43
|
+
visibleBlockerReasons,
|
|
44
|
+
fieldset.querySelector("legend")?.textContent
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const row = button.closest('[data-testid="checkout-row"]');
|
|
48
|
+
if (row) {
|
|
49
|
+
pushUnique(
|
|
50
|
+
visibleBlockerReasons,
|
|
51
|
+
row.querySelector("[data-dom-blocker-hint]")?.textContent
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
label: buttonLabel,
|
|
56
|
+
disabled: button.disabled,
|
|
57
|
+
visibleBlockerReasons
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async function inspectDisabledActionFromDom(page, buttonLabel) {
|
|
61
|
+
return page.evaluate((label) => {
|
|
62
|
+
const buttons = [...document.querySelectorAll("button")];
|
|
63
|
+
const button = buttons.find(
|
|
64
|
+
(candidate) => candidate.textContent?.includes(label)
|
|
65
|
+
);
|
|
66
|
+
if (!button) {
|
|
67
|
+
return {
|
|
68
|
+
label,
|
|
69
|
+
disabled: false,
|
|
70
|
+
visibleBlockerReasons: []
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const visibleBlockerReasons = [];
|
|
74
|
+
const push = (value) => {
|
|
75
|
+
const trimmed = value?.trim();
|
|
76
|
+
if (trimmed && !visibleBlockerReasons.includes(trimmed)) {
|
|
77
|
+
visibleBlockerReasons.push(trimmed);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
push(button.getAttribute("title"));
|
|
81
|
+
const ariaLabel = button.getAttribute("aria-label");
|
|
82
|
+
if (ariaLabel?.includes("because")) {
|
|
83
|
+
push(ariaLabel);
|
|
84
|
+
}
|
|
85
|
+
const describedBy = button.getAttribute("aria-describedby");
|
|
86
|
+
if (describedBy) {
|
|
87
|
+
for (const id of describedBy.split(/\s+/)) {
|
|
88
|
+
push(document.getElementById(id)?.textContent);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const fieldset = button.closest("fieldset");
|
|
92
|
+
if (fieldset) {
|
|
93
|
+
push(fieldset.querySelector("legend")?.textContent);
|
|
94
|
+
}
|
|
95
|
+
const row = button.closest('[data-testid="checkout-row"]');
|
|
96
|
+
if (row) {
|
|
97
|
+
push(row.querySelector("[data-dom-blocker-hint]")?.textContent);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
label,
|
|
101
|
+
disabled: button.disabled,
|
|
102
|
+
visibleBlockerReasons
|
|
103
|
+
};
|
|
104
|
+
}, buttonLabel);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// src/comparative-proof.ts
|
|
108
|
+
async function diagnoseCheckoutBlockerFromDomOnly(page, buttonLabel) {
|
|
109
|
+
return inspectDisabledActionFromDom(page, buttonLabel);
|
|
110
|
+
}
|
|
111
|
+
async function diagnoseCheckoutBlockerFromWebMcp(page, actionId, toolName = "why_is_action_unavailable") {
|
|
112
|
+
const result = await invokeWebMcpTool(page, toolName, { actionId });
|
|
113
|
+
return {
|
|
114
|
+
actionId: result.actionId,
|
|
115
|
+
available: result.available,
|
|
116
|
+
reasons: result.reasons ?? []
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
async function runComparativeProof(page, options) {
|
|
120
|
+
const domOnly = await diagnoseCheckoutBlockerFromDomOnly(
|
|
121
|
+
page,
|
|
122
|
+
options.buttonLabel
|
|
123
|
+
);
|
|
124
|
+
const webMcp = await diagnoseCheckoutBlockerFromWebMcp(
|
|
125
|
+
page,
|
|
126
|
+
options.actionId,
|
|
127
|
+
options.toolName
|
|
128
|
+
);
|
|
129
|
+
const domBlockerCount = domOnly.visibleBlockerReasons.length;
|
|
130
|
+
const webMcpBlockerCount = webMcp.reasons.length;
|
|
131
|
+
return {
|
|
132
|
+
domOnly,
|
|
133
|
+
webMcp,
|
|
134
|
+
domBlockerCount,
|
|
135
|
+
webMcpBlockerCount,
|
|
136
|
+
webMcpIsStrictlyMoreInformative: domOnly.disabled && domBlockerCount === 0 && webMcpBlockerCount > 0 && !webMcp.available
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export {
|
|
141
|
+
collectDisabledActionReasonsFromDom,
|
|
142
|
+
inspectDisabledActionFromDom,
|
|
143
|
+
diagnoseCheckoutBlockerFromDomOnly,
|
|
144
|
+
diagnoseCheckoutBlockerFromWebMcp,
|
|
145
|
+
runComparativeProof
|
|
146
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/browser-helpers.ts
|
|
2
|
+
async function expectWebMcpTool(page, name) {
|
|
3
|
+
const exists = await page.evaluate(async (toolName) => {
|
|
4
|
+
const context = document.modelContext;
|
|
5
|
+
const tools = await context?.getTools?.() ?? [];
|
|
6
|
+
return tools.some((tool) => tool.name === toolName);
|
|
7
|
+
}, name);
|
|
8
|
+
if (!exists) {
|
|
9
|
+
throw new Error(`Expected WebMCP tool "${name}" to be registered.`);
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
async function invokeWebMcpTool(page, name, args, options = {}) {
|
|
13
|
+
return page.evaluate(
|
|
14
|
+
async ({ toolName, input, inputFormat }) => {
|
|
15
|
+
const context = document.modelContext;
|
|
16
|
+
const tools = await context?.getTools?.() ?? [];
|
|
17
|
+
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
18
|
+
if (!tool || !context?.executeTool) {
|
|
19
|
+
throw new Error(`WebMCP tool "${toolName}" cannot be invoked.`);
|
|
20
|
+
}
|
|
21
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
22
|
+
return context.executeTool(
|
|
23
|
+
tool,
|
|
24
|
+
inputFormat === "json-string" ? JSON.stringify(payload) : payload
|
|
25
|
+
);
|
|
26
|
+
},
|
|
27
|
+
{ toolName: name, input: args, inputFormat: options.inputFormat }
|
|
28
|
+
);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export {
|
|
32
|
+
expectWebMcpTool,
|
|
33
|
+
invokeWebMcpTool
|
|
34
|
+
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import {
|
|
2
|
+
diagnoseCheckoutBlockerFromDomOnly,
|
|
3
|
+
diagnoseCheckoutBlockerFromWebMcp,
|
|
4
|
+
runComparativeProof
|
|
5
|
+
} from "./chunk-K6KLEZGY.js";
|
|
6
|
+
import "./chunk-M554RXAL.js";
|
|
7
|
+
export {
|
|
8
|
+
diagnoseCheckoutBlockerFromDomOnly,
|
|
9
|
+
diagnoseCheckoutBlockerFromWebMcp,
|
|
10
|
+
runComparativeProof
|
|
11
|
+
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WebMcpInvocationOptions } from './browser-helpers';
|
|
1
2
|
export interface WebMcpToolListing {
|
|
2
3
|
name: string;
|
|
3
4
|
description?: string | undefined;
|
|
@@ -5,13 +6,13 @@ export interface WebMcpToolListing {
|
|
|
5
6
|
type ModelContextHost = Document | Navigator;
|
|
6
7
|
/**
|
|
7
8
|
* Lists registered WebMCP tools from the browser model context.
|
|
8
|
-
* Returns an empty array when WebMCP is unavailable
|
|
9
|
+
* Returns an empty array when WebMCP is unavailable.
|
|
9
10
|
*/
|
|
10
11
|
export declare function listWebMcpTools(host?: ModelContextHost): Promise<WebMcpToolListing[]>;
|
|
11
12
|
/**
|
|
12
13
|
* Executes a registered WebMCP tool by name.
|
|
13
|
-
*
|
|
14
|
+
* Uses object input by default; legacy JSON string input must be explicitly selected.
|
|
14
15
|
*/
|
|
15
|
-
export declare function executeWebMcpTool(name: string, args?: unknown, host?: ModelContextHost): Promise<unknown>;
|
|
16
|
+
export declare function executeWebMcpTool(name: string, args?: unknown, host?: ModelContextHost, options?: WebMcpInvocationOptions): Promise<unknown>;
|
|
16
17
|
export {};
|
|
17
18
|
//# sourceMappingURL=devtools-mcp-helper.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"devtools-mcp-helper.d.ts","sourceRoot":"","sources":["../src/devtools-mcp-helper.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,KAAK,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;
|
|
1
|
+
{"version":3,"file":"devtools-mcp-helper.d.ts","sourceRoot":"","sources":["../src/devtools-mcp-helper.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AAGjE,MAAM,WAAW,iBAAiB;IAChC,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAClC;AAED,KAAK,gBAAgB,GAAG,QAAQ,GAAG,SAAS,CAAC;AA6B7C;;;GAGG;AACH,wBAAsB,eAAe,CACnC,IAAI,GAAE,gBAA2B,GAChC,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAW9B;AAED;;;GAGG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,EACZ,IAAI,GAAE,OAAY,EAClB,IAAI,GAAE,gBAA2B,EACjC,OAAO,GAAE,uBAA4B,GACpC,OAAO,CAAC,OAAO,CAAC,CAmBlB"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
function getModelContext(host = document) {
|
|
2
|
-
return host
|
|
2
|
+
return host
|
|
3
|
+
.modelContext;
|
|
3
4
|
}
|
|
4
5
|
/**
|
|
5
6
|
* Lists registered WebMCP tools from the browser model context.
|
|
6
|
-
* Returns an empty array when WebMCP is unavailable
|
|
7
|
+
* Returns an empty array when WebMCP is unavailable.
|
|
7
8
|
*/
|
|
8
9
|
export async function listWebMcpTools(host = document) {
|
|
9
10
|
const context = getModelContext(host);
|
|
@@ -18,9 +19,9 @@ export async function listWebMcpTools(host = document) {
|
|
|
18
19
|
}
|
|
19
20
|
/**
|
|
20
21
|
* Executes a registered WebMCP tool by name.
|
|
21
|
-
*
|
|
22
|
+
* Uses object input by default; legacy JSON string input must be explicitly selected.
|
|
22
23
|
*/
|
|
23
|
-
export async function executeWebMcpTool(name, args = {}, host = document) {
|
|
24
|
+
export async function executeWebMcpTool(name, args = {}, host = document, options = {}) {
|
|
24
25
|
const context = getModelContext(host);
|
|
25
26
|
if (!context?.getTools || !context.executeTool) {
|
|
26
27
|
throw new Error('WebMCP model context is unavailable. Use installModelContextMock() in tests.');
|
|
@@ -30,5 +31,6 @@ export async function executeWebMcpTool(name, args = {}, host = document) {
|
|
|
30
31
|
if (!tool) {
|
|
31
32
|
throw new Error(`WebMCP tool not found: ${name}`);
|
|
32
33
|
}
|
|
33
|
-
|
|
34
|
+
const payload = args && typeof args === 'object' ? args : {};
|
|
35
|
+
return context.executeTool(tool, options.inputFormat === 'json-string' ? JSON.stringify(payload) : payload);
|
|
34
36
|
}
|
package/dist/index.cjs
CHANGED
|
@@ -36,18 +36,22 @@ async function expectWebMcpTool(page, name) {
|
|
|
36
36
|
throw new Error(`Expected WebMCP tool "${name}" to be registered.`);
|
|
37
37
|
}
|
|
38
38
|
}
|
|
39
|
-
async function invokeWebMcpTool(page, name, args) {
|
|
39
|
+
async function invokeWebMcpTool(page, name, args, options = {}) {
|
|
40
40
|
return page.evaluate(
|
|
41
|
-
async ({ toolName, input }) => {
|
|
41
|
+
async ({ toolName, input, inputFormat }) => {
|
|
42
42
|
const context = document.modelContext;
|
|
43
43
|
const tools = await context?.getTools?.() ?? [];
|
|
44
44
|
const tool = tools.find((candidate) => candidate.name === toolName);
|
|
45
45
|
if (!tool || !context?.executeTool) {
|
|
46
46
|
throw new Error(`WebMCP tool "${toolName}" cannot be invoked.`);
|
|
47
47
|
}
|
|
48
|
-
|
|
48
|
+
const payload = input && typeof input === "object" ? input : {};
|
|
49
|
+
return context.executeTool(
|
|
50
|
+
tool,
|
|
51
|
+
inputFormat === "json-string" ? JSON.stringify(payload) : payload
|
|
52
|
+
);
|
|
49
53
|
},
|
|
50
|
-
{ toolName: name, input: args }
|
|
54
|
+
{ toolName: name, input: args, inputFormat: options.inputFormat }
|
|
51
55
|
);
|
|
52
56
|
}
|
|
53
57
|
var init_browser_helpers = __esm({
|
|
@@ -225,6 +229,7 @@ __export(index_exports, {
|
|
|
225
229
|
installModelContextMock: () => installModelContextMock,
|
|
226
230
|
invokeWebMcpTool: () => invokeWebMcpTool,
|
|
227
231
|
listWebMcpTools: () => listWebMcpTools,
|
|
232
|
+
parseExecuteToolInput: () => parseExecuteToolInput,
|
|
228
233
|
runComparativeProof: () => runComparativeProof,
|
|
229
234
|
runTimelineComparativeProof: () => runTimelineComparativeProof,
|
|
230
235
|
runToolSelectionSmokeTest: () => runToolSelectionSmokeTest,
|
|
@@ -249,7 +254,7 @@ async function listWebMcpTools(host = document) {
|
|
|
249
254
|
description: tool.description
|
|
250
255
|
}));
|
|
251
256
|
}
|
|
252
|
-
async function executeWebMcpTool(name, args = {}, host = document) {
|
|
257
|
+
async function executeWebMcpTool(name, args = {}, host = document, options = {}) {
|
|
253
258
|
const context = getModelContext(host);
|
|
254
259
|
if (!context?.getTools || !context.executeTool) {
|
|
255
260
|
throw new Error(
|
|
@@ -261,13 +266,23 @@ async function executeWebMcpTool(name, args = {}, host = document) {
|
|
|
261
266
|
if (!tool) {
|
|
262
267
|
throw new Error(`WebMCP tool not found: ${name}`);
|
|
263
268
|
}
|
|
264
|
-
|
|
269
|
+
const payload = args && typeof args === "object" ? args : {};
|
|
270
|
+
return context.executeTool(
|
|
271
|
+
tool,
|
|
272
|
+
options.inputFormat === "json-string" ? JSON.stringify(payload) : payload
|
|
273
|
+
);
|
|
265
274
|
}
|
|
266
275
|
|
|
267
276
|
// src/index.ts
|
|
268
277
|
init_dom_only_inspection();
|
|
269
278
|
|
|
270
279
|
// src/mock-model-context.ts
|
|
280
|
+
function parseExecuteToolInput(input) {
|
|
281
|
+
if (typeof input === "string") {
|
|
282
|
+
return JSON.parse(input || "{}");
|
|
283
|
+
}
|
|
284
|
+
return input ?? {};
|
|
285
|
+
}
|
|
271
286
|
var MockModelContext = class extends EventTarget {
|
|
272
287
|
tools = /* @__PURE__ */ new Map();
|
|
273
288
|
invocations = [];
|
|
@@ -292,7 +307,7 @@ var MockModelContext = class extends EventTarget {
|
|
|
292
307
|
async getTools() {
|
|
293
308
|
return [...this.tools.values()];
|
|
294
309
|
}
|
|
295
|
-
async executeTool(toolOrName,
|
|
310
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
296
311
|
const name = typeof toolOrName === "string" ? toolOrName : toolOrName.name;
|
|
297
312
|
if (!name) {
|
|
298
313
|
throw new Error("Tool name is required.");
|
|
@@ -301,10 +316,10 @@ var MockModelContext = class extends EventTarget {
|
|
|
301
316
|
if (!tool) {
|
|
302
317
|
throw new Error(`Tool not found: ${name}`);
|
|
303
318
|
}
|
|
304
|
-
const args =
|
|
319
|
+
const args = parseExecuteToolInput(input);
|
|
305
320
|
try {
|
|
306
321
|
const result = await tool.execute(args, {
|
|
307
|
-
signal: new AbortController().signal
|
|
322
|
+
signal: options.signal ?? new AbortController().signal
|
|
308
323
|
});
|
|
309
324
|
this.invocations.push({ name, args, result });
|
|
310
325
|
return result;
|
|
@@ -331,6 +346,13 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
331
346
|
(() => {
|
|
332
347
|
const tools = new Map();
|
|
333
348
|
|
|
349
|
+
function parseInput(input) {
|
|
350
|
+
if (typeof input === 'string') {
|
|
351
|
+
return JSON.parse(input || '{}');
|
|
352
|
+
}
|
|
353
|
+
return input ?? {};
|
|
354
|
+
}
|
|
355
|
+
|
|
334
356
|
Object.defineProperty(document, 'modelContext', {
|
|
335
357
|
configurable: true,
|
|
336
358
|
value: {
|
|
@@ -351,7 +373,7 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
351
373
|
async getTools() {
|
|
352
374
|
return [...tools.values()];
|
|
353
375
|
},
|
|
354
|
-
async executeTool(toolOrName,
|
|
376
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
355
377
|
const name =
|
|
356
378
|
typeof toolOrName === 'string'
|
|
357
379
|
? toolOrName
|
|
@@ -361,7 +383,9 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
361
383
|
throw new Error('Tool not found: ' + name);
|
|
362
384
|
}
|
|
363
385
|
|
|
364
|
-
return tool.execute(
|
|
386
|
+
return tool.execute(parseInput(input), {
|
|
387
|
+
signal: options.signal ?? new AbortController().signal,
|
|
388
|
+
});
|
|
365
389
|
},
|
|
366
390
|
},
|
|
367
391
|
});
|
|
@@ -466,6 +490,7 @@ async function runTimelineComparativeProof(page, options) {
|
|
|
466
490
|
installModelContextMock,
|
|
467
491
|
invokeWebMcpTool,
|
|
468
492
|
listWebMcpTools,
|
|
493
|
+
parseExecuteToolInput,
|
|
469
494
|
runComparativeProof,
|
|
470
495
|
runTimelineComparativeProof,
|
|
471
496
|
runToolSelectionSmokeTest,
|
package/dist/index.js
CHANGED
|
@@ -4,11 +4,11 @@ import {
|
|
|
4
4
|
diagnoseCheckoutBlockerFromWebMcp,
|
|
5
5
|
inspectDisabledActionFromDom,
|
|
6
6
|
runComparativeProof
|
|
7
|
-
} from "./chunk-
|
|
7
|
+
} from "./chunk-K6KLEZGY.js";
|
|
8
8
|
import {
|
|
9
9
|
expectWebMcpTool,
|
|
10
10
|
invokeWebMcpTool
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-M554RXAL.js";
|
|
12
12
|
|
|
13
13
|
// src/devtools-mcp-helper.ts
|
|
14
14
|
function getModelContext(host = document) {
|
|
@@ -25,7 +25,7 @@ async function listWebMcpTools(host = document) {
|
|
|
25
25
|
description: tool.description
|
|
26
26
|
}));
|
|
27
27
|
}
|
|
28
|
-
async function executeWebMcpTool(name, args = {}, host = document) {
|
|
28
|
+
async function executeWebMcpTool(name, args = {}, host = document, options = {}) {
|
|
29
29
|
const context = getModelContext(host);
|
|
30
30
|
if (!context?.getTools || !context.executeTool) {
|
|
31
31
|
throw new Error(
|
|
@@ -37,10 +37,20 @@ async function executeWebMcpTool(name, args = {}, host = document) {
|
|
|
37
37
|
if (!tool) {
|
|
38
38
|
throw new Error(`WebMCP tool not found: ${name}`);
|
|
39
39
|
}
|
|
40
|
-
|
|
40
|
+
const payload = args && typeof args === "object" ? args : {};
|
|
41
|
+
return context.executeTool(
|
|
42
|
+
tool,
|
|
43
|
+
options.inputFormat === "json-string" ? JSON.stringify(payload) : payload
|
|
44
|
+
);
|
|
41
45
|
}
|
|
42
46
|
|
|
43
47
|
// src/mock-model-context.ts
|
|
48
|
+
function parseExecuteToolInput(input) {
|
|
49
|
+
if (typeof input === "string") {
|
|
50
|
+
return JSON.parse(input || "{}");
|
|
51
|
+
}
|
|
52
|
+
return input ?? {};
|
|
53
|
+
}
|
|
44
54
|
var MockModelContext = class extends EventTarget {
|
|
45
55
|
tools = /* @__PURE__ */ new Map();
|
|
46
56
|
invocations = [];
|
|
@@ -65,7 +75,7 @@ var MockModelContext = class extends EventTarget {
|
|
|
65
75
|
async getTools() {
|
|
66
76
|
return [...this.tools.values()];
|
|
67
77
|
}
|
|
68
|
-
async executeTool(toolOrName,
|
|
78
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
69
79
|
const name = typeof toolOrName === "string" ? toolOrName : toolOrName.name;
|
|
70
80
|
if (!name) {
|
|
71
81
|
throw new Error("Tool name is required.");
|
|
@@ -74,10 +84,10 @@ var MockModelContext = class extends EventTarget {
|
|
|
74
84
|
if (!tool) {
|
|
75
85
|
throw new Error(`Tool not found: ${name}`);
|
|
76
86
|
}
|
|
77
|
-
const args =
|
|
87
|
+
const args = parseExecuteToolInput(input);
|
|
78
88
|
try {
|
|
79
89
|
const result = await tool.execute(args, {
|
|
80
|
-
signal: new AbortController().signal
|
|
90
|
+
signal: options.signal ?? new AbortController().signal
|
|
81
91
|
});
|
|
82
92
|
this.invocations.push({ name, args, result });
|
|
83
93
|
return result;
|
|
@@ -104,6 +114,13 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
104
114
|
(() => {
|
|
105
115
|
const tools = new Map();
|
|
106
116
|
|
|
117
|
+
function parseInput(input) {
|
|
118
|
+
if (typeof input === 'string') {
|
|
119
|
+
return JSON.parse(input || '{}');
|
|
120
|
+
}
|
|
121
|
+
return input ?? {};
|
|
122
|
+
}
|
|
123
|
+
|
|
107
124
|
Object.defineProperty(document, 'modelContext', {
|
|
108
125
|
configurable: true,
|
|
109
126
|
value: {
|
|
@@ -124,7 +141,7 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
124
141
|
async getTools() {
|
|
125
142
|
return [...tools.values()];
|
|
126
143
|
},
|
|
127
|
-
async executeTool(toolOrName,
|
|
144
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
128
145
|
const name =
|
|
129
146
|
typeof toolOrName === 'string'
|
|
130
147
|
? toolOrName
|
|
@@ -134,7 +151,9 @@ var MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
134
151
|
throw new Error('Tool not found: ' + name);
|
|
135
152
|
}
|
|
136
153
|
|
|
137
|
-
return tool.execute(
|
|
154
|
+
return tool.execute(parseInput(input), {
|
|
155
|
+
signal: options.signal ?? new AbortController().signal,
|
|
156
|
+
});
|
|
138
157
|
},
|
|
139
158
|
},
|
|
140
159
|
});
|
|
@@ -208,8 +227,8 @@ function runToolSelectionSmokeTest(tools, prompt, expected) {
|
|
|
208
227
|
return haystack.includes(expected.toLowerCase()) || tools.some((t) => t.name === expected);
|
|
209
228
|
}
|
|
210
229
|
async function runTimelineComparativeProof(page, options) {
|
|
211
|
-
const { runComparativeProof: runComparativeProof2 } = await import("./comparative-proof-
|
|
212
|
-
const { invokeWebMcpTool: invokeWebMcpTool2 } = await import("./browser-helpers-
|
|
230
|
+
const { runComparativeProof: runComparativeProof2 } = await import("./comparative-proof-IVDBBIIG.js");
|
|
231
|
+
const { invokeWebMcpTool: invokeWebMcpTool2 } = await import("./browser-helpers-V33B4UD5.js");
|
|
213
232
|
const proof = await runComparativeProof2(page, {
|
|
214
233
|
buttonLabel: options.buttonLabel,
|
|
215
234
|
actionId: options.actionId
|
|
@@ -238,6 +257,7 @@ export {
|
|
|
238
257
|
installModelContextMock,
|
|
239
258
|
invokeWebMcpTool,
|
|
240
259
|
listWebMcpTools,
|
|
260
|
+
parseExecuteToolInput,
|
|
241
261
|
runComparativeProof,
|
|
242
262
|
runTimelineComparativeProof,
|
|
243
263
|
runToolSelectionSmokeTest,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"invocation.test.d.ts","sourceRoot":"","sources":["../src/invocation.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { expectWebMcpTool, invokeWebMcpTool, } from './browser-helpers';
|
|
3
|
+
import { executeWebMcpTool, listWebMcpTools } from './devtools-mcp-helper';
|
|
4
|
+
const page = {
|
|
5
|
+
evaluate: async (callback, arg) => callback(arg),
|
|
6
|
+
};
|
|
7
|
+
const original = Object.getOwnPropertyDescriptor(document, 'modelContext');
|
|
8
|
+
afterEach(() => {
|
|
9
|
+
if (original)
|
|
10
|
+
Object.defineProperty(document, 'modelContext', original);
|
|
11
|
+
else
|
|
12
|
+
Reflect.deleteProperty(document, 'modelContext');
|
|
13
|
+
});
|
|
14
|
+
function install(context) {
|
|
15
|
+
Object.defineProperty(document, 'modelContext', {
|
|
16
|
+
configurable: true,
|
|
17
|
+
value: context,
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
const tool = { name: 'write_item', description: 'Writes an item.' };
|
|
21
|
+
const callers = [
|
|
22
|
+
[
|
|
23
|
+
'DevTools',
|
|
24
|
+
(args, options) => executeWebMcpTool(tool.name, args, document, options),
|
|
25
|
+
],
|
|
26
|
+
[
|
|
27
|
+
'Playwright',
|
|
28
|
+
(args, options) => invokeWebMcpTool(page, tool.name, args, options),
|
|
29
|
+
],
|
|
30
|
+
];
|
|
31
|
+
for (const [label, invoke] of callers) {
|
|
32
|
+
describe(label, () => {
|
|
33
|
+
it('passes the discovered descriptor and object exactly once', async () => {
|
|
34
|
+
const executeTool = vi.fn().mockResolvedValue({ saved: true });
|
|
35
|
+
install({ getTools: async () => [tool], executeTool });
|
|
36
|
+
await expect(invoke({ id: 3 })).resolves.toEqual({ saved: true });
|
|
37
|
+
expect(executeTool).toHaveBeenCalledExactlyOnceWith(tool, { id: 3 });
|
|
38
|
+
});
|
|
39
|
+
it('selects legacy string encoding before executing', async () => {
|
|
40
|
+
const executeTool = vi.fn().mockResolvedValue('saved');
|
|
41
|
+
install({ getTools: async () => [tool], executeTool });
|
|
42
|
+
await expect(invoke({ id: 3 }, { inputFormat: 'json-string' })).resolves.toBe('saved');
|
|
43
|
+
expect(executeTool).toHaveBeenCalledExactlyOnceWith(tool, '{"id":3}');
|
|
44
|
+
});
|
|
45
|
+
it.each([
|
|
46
|
+
new TypeError('handler failed after write'),
|
|
47
|
+
new Error('failed'),
|
|
48
|
+
new DOMException('cancelled', 'AbortError'),
|
|
49
|
+
])('never retries a failed execution (%s)', async (error) => {
|
|
50
|
+
const executeTool = vi.fn().mockRejectedValue(error);
|
|
51
|
+
install({ getTools: async () => [tool], executeTool });
|
|
52
|
+
await expect(invoke({})).rejects.toBe(error);
|
|
53
|
+
expect(executeTool).toHaveBeenCalledTimes(1);
|
|
54
|
+
});
|
|
55
|
+
it('propagates legacy failures without retrying', async () => {
|
|
56
|
+
const error = new TypeError('legacy failure');
|
|
57
|
+
const executeTool = vi.fn().mockRejectedValue(error);
|
|
58
|
+
install({ getTools: async () => [tool], executeTool });
|
|
59
|
+
await expect(invoke({}, { inputFormat: 'json-string' })).rejects.toBe(error);
|
|
60
|
+
expect(executeTool).toHaveBeenCalledTimes(1);
|
|
61
|
+
});
|
|
62
|
+
it.each([undefined, null, 5, 'text'])('normalizes non-object input %s to empty input', async (input) => {
|
|
63
|
+
const executeTool = vi.fn();
|
|
64
|
+
install({ getTools: async () => [tool], executeTool });
|
|
65
|
+
await invoke(input);
|
|
66
|
+
expect(executeTool).toHaveBeenCalledExactlyOnceWith(tool, {});
|
|
67
|
+
});
|
|
68
|
+
it.each([undefined, {}, { getTools: async () => [] }])('rejects unavailable tools or APIs', async (context) => {
|
|
69
|
+
install(context);
|
|
70
|
+
await expect(invoke({})).rejects.toThrow();
|
|
71
|
+
});
|
|
72
|
+
it('never invokes a missing tool', async () => {
|
|
73
|
+
const executeTool = vi.fn();
|
|
74
|
+
install({ getTools: async () => [], executeTool });
|
|
75
|
+
await expect(invoke({})).rejects.toThrow();
|
|
76
|
+
expect(executeTool).not.toHaveBeenCalled();
|
|
77
|
+
});
|
|
78
|
+
it('propagates discovery errors', async () => {
|
|
79
|
+
const error = new Error('discovery failed');
|
|
80
|
+
const executeTool = vi.fn();
|
|
81
|
+
install({ getTools: vi.fn().mockRejectedValue(error), executeTool });
|
|
82
|
+
await expect(invoke({})).rejects.toBe(error);
|
|
83
|
+
expect(executeTool).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
it('lists safe metadata and checks tool presence', async () => {
|
|
88
|
+
install({ getTools: async () => [{ ...tool, execute: () => undefined }] });
|
|
89
|
+
await expect(listWebMcpTools()).resolves.toEqual([tool]);
|
|
90
|
+
await expect(expectWebMcpTool(page, tool.name)).resolves.toBeUndefined();
|
|
91
|
+
await expect(expectWebMcpTool(page, 'missing')).rejects.toThrow('missing');
|
|
92
|
+
install(undefined);
|
|
93
|
+
await expect(listWebMcpTools()).resolves.toEqual([]);
|
|
94
|
+
await expect(expectWebMcpTool(page, tool.name)).rejects.toThrow(tool.name);
|
|
95
|
+
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
export declare
|
|
1
|
+
import type { BrowserModelContext, BrowserWebMcpToolDescriptor, WebMcpExecuteToolOptions, WebMcpRegisterToolOptions } from '@tooluminati/core';
|
|
2
|
+
export declare function parseExecuteToolInput(input: unknown): unknown;
|
|
3
|
+
export declare class MockModelContext extends EventTarget implements BrowserModelContext {
|
|
3
4
|
readonly tools: Map<string, BrowserWebMcpToolDescriptor>;
|
|
4
5
|
readonly invocations: Array<{
|
|
5
6
|
name: string;
|
|
@@ -9,6 +10,6 @@ export declare class MockModelContext extends EventTarget implements BrowserMode
|
|
|
9
10
|
}>;
|
|
10
11
|
registerTool(tool: BrowserWebMcpToolDescriptor, options?: WebMcpRegisterToolOptions): void;
|
|
11
12
|
getTools(): Promise<BrowserWebMcpToolDescriptor[]>;
|
|
12
|
-
executeTool(toolOrName: unknown,
|
|
13
|
+
executeTool(toolOrName: unknown, input?: object | string, options?: WebMcpExecuteToolOptions): Promise<unknown>;
|
|
13
14
|
}
|
|
14
15
|
//# sourceMappingURL=mock-model-context.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mock-model-context.d.ts","sourceRoot":"","sources":["../src/mock-model-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,
|
|
1
|
+
{"version":3,"file":"mock-model-context.d.ts","sourceRoot":"","sources":["../src/mock-model-context.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,mBAAmB,EACnB,2BAA2B,EAC3B,wBAAwB,EACxB,yBAAyB,EAC1B,MAAM,mBAAmB,CAAC;AAE3B,wBAAgB,qBAAqB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAM7D;AAED,qBAAa,gBACX,SAAQ,WACR,YAAW,mBAAmB;IAE9B,QAAQ,CAAC,KAAK,2CAAkD;IAChE,QAAQ,CAAC,WAAW,EAAE,KAAK,CAAC;QAC1B,IAAI,EAAE,MAAM,CAAC;QACb,IAAI,EAAE,OAAO,CAAC;QACd,MAAM,CAAC,EAAE,OAAO,CAAC;QACjB,KAAK,CAAC,EAAE,OAAO,CAAC;KACjB,CAAC,CAAM;IAER,YAAY,CACV,IAAI,EAAE,2BAA2B,EACjC,OAAO,GAAE,yBAA8B,GACtC,IAAI;IAsBD,QAAQ,IAAI,OAAO,CAAC,2BAA2B,EAAE,CAAC;IAIlD,WAAW,CACf,UAAU,EAAE,OAAO,EACnB,KAAK,GAAE,MAAM,GAAG,MAAW,EAC3B,OAAO,GAAE,wBAA6B,GACrC,OAAO,CAAC,OAAO,CAAC;CA0BpB"}
|
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
export function parseExecuteToolInput(input) {
|
|
2
|
+
if (typeof input === 'string') {
|
|
3
|
+
return JSON.parse(input || '{}');
|
|
4
|
+
}
|
|
5
|
+
return input ?? {};
|
|
6
|
+
}
|
|
1
7
|
export class MockModelContext extends EventTarget {
|
|
2
8
|
tools = new Map();
|
|
3
9
|
invocations = [];
|
|
@@ -18,7 +24,7 @@ export class MockModelContext extends EventTarget {
|
|
|
18
24
|
async getTools() {
|
|
19
25
|
return [...this.tools.values()];
|
|
20
26
|
}
|
|
21
|
-
async executeTool(toolOrName,
|
|
27
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
22
28
|
const name = typeof toolOrName === 'string'
|
|
23
29
|
? toolOrName
|
|
24
30
|
: toolOrName.name;
|
|
@@ -29,10 +35,10 @@ export class MockModelContext extends EventTarget {
|
|
|
29
35
|
if (!tool) {
|
|
30
36
|
throw new Error(`Tool not found: ${name}`);
|
|
31
37
|
}
|
|
32
|
-
const args =
|
|
38
|
+
const args = parseExecuteToolInput(input);
|
|
33
39
|
try {
|
|
34
40
|
const result = await tool.execute(args, {
|
|
35
|
-
signal: new AbortController().signal,
|
|
41
|
+
signal: options.signal ?? new AbortController().signal,
|
|
36
42
|
});
|
|
37
43
|
this.invocations.push({ name, args, result });
|
|
38
44
|
return result;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mock-model-context.test.d.ts","sourceRoot":"","sources":["../src/mock-model-context.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { afterEach, describe, expect, expectTypeOf, it, vi } from 'vitest';
|
|
2
|
+
import { MockModelContext, parseExecuteToolInput } from './mock-model-context';
|
|
3
|
+
import { MODEL_CONTEXT_MOCK_INIT_SCRIPT } from './model-context-mock-script';
|
|
4
|
+
const original = Object.getOwnPropertyDescriptor(document, 'modelContext');
|
|
5
|
+
afterEach(() => {
|
|
6
|
+
if (original)
|
|
7
|
+
Object.defineProperty(document, 'modelContext', original);
|
|
8
|
+
else
|
|
9
|
+
Reflect.deleteProperty(document, 'modelContext');
|
|
10
|
+
});
|
|
11
|
+
it.each([
|
|
12
|
+
[{}, {}],
|
|
13
|
+
['{"id":2}', { id: 2 }],
|
|
14
|
+
['', {}],
|
|
15
|
+
[undefined, {}],
|
|
16
|
+
[null, {}],
|
|
17
|
+
])('parses %s', (input, expected) => {
|
|
18
|
+
expect(parseExecuteToolInput(input)).toEqual(expected);
|
|
19
|
+
});
|
|
20
|
+
it('preserves object identity and rejects invalid JSON', () => {
|
|
21
|
+
const args = { id: 2 };
|
|
22
|
+
expect(parseExecuteToolInput(args)).toBe(args);
|
|
23
|
+
expect(() => parseExecuteToolInput('{broken')).toThrow(SyntaxError);
|
|
24
|
+
});
|
|
25
|
+
it('accepts fromOrigins on the public discovery contract', () => {
|
|
26
|
+
expectTypeOf().toEqualTypeOf();
|
|
27
|
+
});
|
|
28
|
+
for (const variant of ['class', 'init script']) {
|
|
29
|
+
const create = () => {
|
|
30
|
+
if (variant === 'class')
|
|
31
|
+
return new MockModelContext();
|
|
32
|
+
window.eval(MODEL_CONTEXT_MOCK_INIT_SCRIPT);
|
|
33
|
+
return document
|
|
34
|
+
.modelContext;
|
|
35
|
+
};
|
|
36
|
+
describe(variant, () => {
|
|
37
|
+
it.each([{}, '{"id":2}', undefined])('executes descriptor input %s and forwards client cancellation', async (input) => {
|
|
38
|
+
const context = create();
|
|
39
|
+
const client = new AbortController();
|
|
40
|
+
const execute = vi.fn().mockResolvedValue('ok');
|
|
41
|
+
context.registerTool({ name: 'read', description: 'Reads.', execute });
|
|
42
|
+
const [tool] = await context.getTools();
|
|
43
|
+
await expect(context.executeTool(tool, input, { signal: client.signal })).resolves.toBe('ok');
|
|
44
|
+
expect(execute).toHaveBeenCalledExactlyOnceWith(parseExecuteToolInput(input), { signal: client.signal });
|
|
45
|
+
client.abort();
|
|
46
|
+
expect(execute.mock.calls[0][1].signal.aborted).toBe(true);
|
|
47
|
+
expect(await context.getTools()).toHaveLength(1);
|
|
48
|
+
});
|
|
49
|
+
it('unregisters without aborting an executing callback', async () => {
|
|
50
|
+
const context = create();
|
|
51
|
+
const controller = new AbortController();
|
|
52
|
+
let finish;
|
|
53
|
+
let signal;
|
|
54
|
+
context.registerTool({
|
|
55
|
+
name: 'read',
|
|
56
|
+
description: 'Reads.',
|
|
57
|
+
execute: (_args, client) => {
|
|
58
|
+
signal = client.signal;
|
|
59
|
+
return new Promise((resolve) => {
|
|
60
|
+
finish = resolve;
|
|
61
|
+
});
|
|
62
|
+
},
|
|
63
|
+
}, { signal: controller.signal });
|
|
64
|
+
const result = context.executeTool('read', {});
|
|
65
|
+
controller.abort();
|
|
66
|
+
expect(await context.getTools()).toEqual([]);
|
|
67
|
+
expect(signal.aborted).toBe(false);
|
|
68
|
+
finish('finished');
|
|
69
|
+
await expect(result).resolves.toBe('finished');
|
|
70
|
+
});
|
|
71
|
+
it('skips pre-aborted registrations and rejects missing tools and invalid JSON', async () => {
|
|
72
|
+
const context = create();
|
|
73
|
+
const controller = new AbortController();
|
|
74
|
+
controller.abort();
|
|
75
|
+
const execute = vi.fn();
|
|
76
|
+
const tool = { name: 'read', description: 'Reads.', execute };
|
|
77
|
+
context.registerTool(tool, { signal: controller.signal });
|
|
78
|
+
expect(await context.getTools()).toEqual([]);
|
|
79
|
+
await expect(context.executeTool('absent')).rejects.toThrow();
|
|
80
|
+
await expect(context.executeTool({})).rejects.toThrow();
|
|
81
|
+
context.registerTool(tool);
|
|
82
|
+
await expect(context.executeTool('read', '{broken')).rejects.toThrow(SyntaxError);
|
|
83
|
+
expect(execute).not.toHaveBeenCalled();
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
it('records successful and failed invocations and emits toolchange on lifecycle changes', async () => {
|
|
88
|
+
const context = new MockModelContext();
|
|
89
|
+
const changed = vi.fn();
|
|
90
|
+
context.addEventListener('toolchange', changed);
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const error = new Error('failed');
|
|
93
|
+
const execute = vi
|
|
94
|
+
.fn()
|
|
95
|
+
.mockResolvedValueOnce('ok')
|
|
96
|
+
.mockRejectedValueOnce(error);
|
|
97
|
+
context.registerTool({ name: 'read', description: 'Reads.', execute }, { signal: controller.signal });
|
|
98
|
+
await context.executeTool('read', {});
|
|
99
|
+
await expect(context.executeTool('read', {})).rejects.toBe(error);
|
|
100
|
+
expect(context.invocations).toEqual([
|
|
101
|
+
{ name: 'read', args: {}, result: 'ok' },
|
|
102
|
+
{ name: 'read', args: {}, error },
|
|
103
|
+
]);
|
|
104
|
+
controller.abort();
|
|
105
|
+
expect(changed).toHaveBeenCalledTimes(2);
|
|
106
|
+
});
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const MODEL_CONTEXT_MOCK_INIT_SCRIPT = "\n(() => {\n const tools = new Map();\n\n Object.defineProperty(document, 'modelContext', {\n configurable: true,\n value: {\n registerTool(tool, options = {}) {\n if (options.signal?.aborted) {\n return;\n }\n\n tools.set(tool.name, tool);\n options.signal?.addEventListener(\n 'abort',\n () => {\n tools.delete(tool.name);\n },\n { once: true },\n );\n },\n async getTools() {\n return [...tools.values()];\n },\n async executeTool(toolOrName,
|
|
1
|
+
export declare const MODEL_CONTEXT_MOCK_INIT_SCRIPT = "\n(() => {\n const tools = new Map();\n\n function parseInput(input) {\n if (typeof input === 'string') {\n return JSON.parse(input || '{}');\n }\n return input ?? {};\n }\n\n Object.defineProperty(document, 'modelContext', {\n configurable: true,\n value: {\n registerTool(tool, options = {}) {\n if (options.signal?.aborted) {\n return;\n }\n\n tools.set(tool.name, tool);\n options.signal?.addEventListener(\n 'abort',\n () => {\n tools.delete(tool.name);\n },\n { once: true },\n );\n },\n async getTools() {\n return [...tools.values()];\n },\n async executeTool(toolOrName, input = {}, options = {}) {\n const name =\n typeof toolOrName === 'string'\n ? toolOrName\n : toolOrName?.name;\n const tool = name ? tools.get(name) : undefined;\n if (!tool) {\n throw new Error('Tool not found: ' + name);\n }\n\n return tool.execute(parseInput(input), {\n signal: options.signal ?? new AbortController().signal,\n });\n },\n },\n });\n})();\n";
|
|
2
2
|
//# sourceMappingURL=model-context-mock-script.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"model-context-mock-script.d.ts","sourceRoot":"","sources":["../src/model-context-mock-script.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,
|
|
1
|
+
{"version":3,"file":"model-context-mock-script.d.ts","sourceRoot":"","sources":["../src/model-context-mock-script.ts"],"names":[],"mappings":"AAAA,eAAO,MAAM,8BAA8B,0qCAgD1C,CAAC"}
|
|
@@ -2,6 +2,13 @@ export const MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
2
2
|
(() => {
|
|
3
3
|
const tools = new Map();
|
|
4
4
|
|
|
5
|
+
function parseInput(input) {
|
|
6
|
+
if (typeof input === 'string') {
|
|
7
|
+
return JSON.parse(input || '{}');
|
|
8
|
+
}
|
|
9
|
+
return input ?? {};
|
|
10
|
+
}
|
|
11
|
+
|
|
5
12
|
Object.defineProperty(document, 'modelContext', {
|
|
6
13
|
configurable: true,
|
|
7
14
|
value: {
|
|
@@ -22,7 +29,7 @@ export const MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
22
29
|
async getTools() {
|
|
23
30
|
return [...tools.values()];
|
|
24
31
|
},
|
|
25
|
-
async executeTool(toolOrName,
|
|
32
|
+
async executeTool(toolOrName, input = {}, options = {}) {
|
|
26
33
|
const name =
|
|
27
34
|
typeof toolOrName === 'string'
|
|
28
35
|
? toolOrName
|
|
@@ -32,7 +39,9 @@ export const MODEL_CONTEXT_MOCK_INIT_SCRIPT = `
|
|
|
32
39
|
throw new Error('Tool not found: ' + name);
|
|
33
40
|
}
|
|
34
41
|
|
|
35
|
-
return tool.execute(
|
|
42
|
+
return tool.execute(parseInput(input), {
|
|
43
|
+
signal: options.signal ?? new AbortController().signal,
|
|
44
|
+
});
|
|
36
45
|
},
|
|
37
46
|
},
|
|
38
47
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tooluminati/testing",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Testing utilities for Tooluminati.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
}
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tooluminati/core": "0.
|
|
18
|
+
"@tooluminati/core": "0.2.0"
|
|
19
19
|
},
|
|
20
20
|
"files": [
|
|
21
21
|
"dist"
|