Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 8 additions & 7 deletions TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,13 @@

## Results

**223 browser tests pass** across Chromium 153.0.8010.12, Firefox 155.0, and
**226 browser tests pass** across Chromium 153.0.8010.12, Firefox 155.0, and
Playwright WebKit 26.6. Package checks also pass: imports, types, SSR, and tarball contents.
CI runs these checks plus WPT in Chrome, Firefox, and actual Safari. Safari runs on
`macos-26`; Playwright WebKit is a separate build.

WPT covers **all 70 WebMCP testharness files, 190 subtests**, with zero unexpected results
in Chrome and Firefox:
in Chrome, Firefox, and Safari:

| Subtest result | Count |
| --- | ---: |
Expand All @@ -17,9 +17,10 @@ in Chrome and Firefox:
| Expected TIMEOUT | 26 |
| Expected NOTRUN | 20 |

Tested with Chrome Canary 157.0.8080.0 and Firefox Nightly 159.0a1 (20260930214513).
Safari has not yet run at this pin; none of the expectations are browser-specific.
At the file level: 43 OK, 26 expected timeouts, one expected error.
The [CI run for PR #8](https://github.com/webmachinelearning/webmcp-polyfill/actions/runs/36969192728)
used Chrome Canary 157.0.8081.0, Firefox Nightly 159.0a1, and Safari 26.6.2
(21624.5.1.11.3). All three have the counts above; none of the expectations are
browser-specific. At the file level: 43 OK, 26 expected timeouts, one expected error.

Expected failures are still failures. `NOTRUN` means an earlier timeout prevented
the test from running, including three abort cases. Passing declarative checks only
Expand Down Expand Up @@ -71,8 +72,8 @@ and `webmcp-types@0.1.9`.
- **Missing APIs:** declarative forms, CSS states, and lifecycle events
(`toolactivated`/`toolcancel`, their handlers, and `ToolActivatedEvent`/`ToolCancelEvent`)
are not implemented.
- **Draft differences:** results are JSON-serialized; some pinned tests expect raw
strings. Omitted or `undefined` input becomes `{}`; `null` and primitives reject.
- **Pinned WPT differences:** results are JSON-serialized as the draft requires;
some pinned tests expect raw strings.
- **Timing:** MessagePorts approximate native task ordering. Aborting before
dispatch skips the callback; the draft dispatches and then aborts its signal.
Delegated permission checks are asynchronous, so argument errors can precede
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,9 @@ class ModelContextPolyfill extends EventTarget implements WebMCP.ModelContext {
): Promise<string> {
const ownerDocument = this.#document;
const target = readExecutionTarget(tool);
if (!isObject(inputObject)) {
throw new TypeError("inputObject must be an object");
}
const settings = readDictionary(options);
const callerSignal = readAbortSignal(settings.signal);

Expand All @@ -251,9 +254,6 @@ class ModelContextPolyfill extends EventTarget implements WebMCP.ModelContext {
if (!expectedOrigin || expectedOrigin === "null") {
throw new NativeDOMException("Invalid or opaque origin", "NotSupportedError");
}
if (!isObject(inputObject)) {
throw new TypeError("inputObject must be an object");
}

const serializedInput = serializeJSON(inputObject);
callerSignal?.throwIfAborted();
Expand Down
38 changes: 38 additions & 0 deletions tests/execute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,44 @@ test("rejects legacy JSON strings and preserves input serialization errors", asy
expect(outcome).toEqual(["TypeError", "TypeError", "TypeError", "RangeError"]);
});

test("a non-object input rejects after the tool is converted and before options are read", async ({
page,
}) => {
const outcome = await page.evaluate(async () => {
const context = document.modelContext!;

await context.registerTool({ name: "x", description: "X", execute: () => null });

const tool = (await context.getTools())[0]!;
const reads: string[] = [];
const descriptor = {
...tool,
get name() {
reads.push("tool");
return tool.name;
},
};
const options = {
get signal() {
reads.push("options");
return undefined;
},
};
try {
// @ts-expect-error Exercise a null input from JavaScript callers.
await context.executeTool(descriptor, null, options);
return { error: "resolved", reads };
} catch (error) {
if (!(error instanceof Error)) {
throw error;
}
return { error: error.name, reads };
}
});

expect(outcome).toEqual({ error: "TypeError", reads: ["tool"] });
});

test("serializes results as JSON and rejects callback or serialization failures", async ({
page,
}) => {
Expand Down
Loading