Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

<!DOCTYPE html>
<html>
<head>
<title>WebMCP executeTool with Caller Navigation (Abort)</title>
<link rel="author" href="mailto:dom@chromium.org">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<script src="/common/get-host-info.sub.js"></script>
<script src="resources/helpers.js"></script>
</head>
<body>
<script>
function timeoutPromise(t, msg) {
return new Promise((_, reject) => {
t.step_timeout(() => reject(msg), 3000);
});
}
const childOrigins = [self.origin, get_host_info().HTTPS_REMOTE_ORIGIN];
for (const origin of childOrigins) {
runTest(origin);
}
function runTest(origin) {
const originForDescription =
origin === self.origin ? 'same-origin' : 'cross-origin';
promise_test(async t => {
const iframe = document.createElement('iframe');
iframe.src = origin + '/webmcp/imperative/resources/iframe-caller.html';
iframe.allow = `tools ${origin}`;
const { promise: loadPromise, resolve: resolveLoad } = Promise.withResolvers();
iframe.onload = resolveLoad;
document.body.appendChild(iframe);
t.add_cleanup(() => iframe.remove());
await loadPromise;
const { promise: toolStartedPromise, resolve: resolveToolStarted } = Promise.withResolvers();
const {
promise: signalAbortedPromise,
resolve: resolveSignalAborted,
reject: rejectSignalAborted
} = Promise.withResolvers();
const controller = new AbortController();
await document.modelContext.registerTool({
name: 'parent_tool',
description: 'Parent tool',
execute: async (input, opts) => {
resolveToolStarted();
// If an `AbortSignal` is not passed into the `execute()` callback, reject
// `signalAbortedPromise`.
if (!opts || !opts.signal) {
rejectSignalAborted('signal not provided to execute()');
return;
}
opts.signal.onabort = resolveSignalAborted;
// The parent's tool runs "forever", waiting for the signal to abort.
await new Promise(() => {});
}
}, { exposedTo: [origin], signal: controller.signal });
t.add_cleanup(() => controller.abort());
iframe.contentWindow.postMessage(
{
action: 'invoke',
name: 'parent_tool',
options: {fromOrigins: [self.origin]}
},
'*');
await Promise.race([toolStartedPromise, timeoutPromise(t, 'tool never ran')]);
const toolCancelPromise = waitForToolCancel('parent_tool');
// The iframe navigates away before the tool has finished executing. Instead
// of navigating the iframe with its `src` attribute, we request it initiate
// its own navigation, just so if the `signal` above aborts, we're sure that
// it's not incidentally aborting due to any bookkeeping `this` Window is
// doing regarding navigations and tool executions.
iframe.contentWindow.postMessage({action: 'navigate'}, '*');
await Promise.race([
Promise.all([signalAbortedPromise, toolCancelPromise]),
timeoutPromise(t, 'signal or toolcancel never fired')
]);
}, `executeTool() aborts target signal and fires toolcancel when the invoker document (${originForDescription}) navigates away`);
promise_test(async t => {
const iframe = document.createElement('iframe');
iframe.src = origin + '/webmcp/imperative/resources/iframe-caller.html';
iframe.allow = `tools ${origin}`;
const { promise: loadPromise, resolve: resolveLoad } = Promise.withResolvers();
iframe.onload = resolveLoad;
document.body.appendChild(iframe);
t.add_cleanup(() => iframe.remove());
await loadPromise;
const { promise: toolStartedPromise, resolve: resolveToolStarted } = Promise.withResolvers();
const { promise: signalAbortedPromise, resolve: resolveSignalAborted } = Promise.withResolvers();
const toolCancelPromise = waitForToolCancel('parent_tool_detach');
const controller = new AbortController();
t.add_cleanup(() => controller.abort());
await document.modelContext.registerTool({
name: 'parent_tool_detach',
description: 'Parent tool for caller detachment',
execute: async (input, opts) => {
opts.signal.onabort = resolveSignalAborted;
resolveToolStarted();
await new Promise(() => {});
}
}, { exposedTo: [origin], signal: controller.signal });
iframe.contentWindow.postMessage(
{
action: 'invoke',
name: 'parent_tool_detach',
options: {fromOrigins: [self.origin]}
},
'*');
await Promise.race([toolStartedPromise, timeoutPromise(t, 'tool never ran')]);
// Detach the caller iframe from the DOM while the parent tool is executing.
iframe.remove();
await Promise.race([
Promise.all([signalAbortedPromise, toolCancelPromise]),
timeoutPromise(t, 'signal or toolcancel never fired after caller detachment')
]);
}, `executeTool() aborts target signal and fires toolcancel when the invoker frame (${originForDescription}) is detached from the DOM`);
}
</script>
</body>
</html>