/* Any copyright is dedicated to the Public Domain. http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; add_task(async function test_getToolsForWindow_and_invoke() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "reverse", description: "Reverses a string", inputSchema: { type: "object", properties: { text: { type: "string" } }, }, execute: (input, _client) => Promise.resolve(input.text.split("").reverse().join("")), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal(tools.length, 1, "Should have one tool registered"); let tool = tools[0]; Assert.equal(tool.name, "reverse", "Tool name should be 'reverse'"); Assert.equal( tool.description, "Reverses a string", "Tool description should match" ); Assert.equal( typeof tool.inputSchema, "object", "inputSchema should be an object" ); Assert.equal( tool.inputSchema.type, "object", "inputSchema.type should be 'object'" ); let result = await Services.modelContext.invokeTool( innerWindowId, "reverse", { text: "hello" } ); Assert.equal( result, "olleh", "XPCOM invokeTool with args should return 'olleh'" ); } ); }); add_task(async function test_invoke_nonexistent_tool_xpcom() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "existing", description: "A tool", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.resolve("ok"), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; await Assert.rejects( Services.modelContext.invokeTool(innerWindowId, "nonexistent"), e => e.name === "NotFoundError", "XPCOM invokeTool with nonexistent tool should reject" ); } ); }); add_task(async function test_invalid_window_id_getTools() { await Assert.rejects( Services.modelContext.getToolsForWindow(0xdeadbeef), e => e.name === "NotFoundError", "getToolsForWindow with invalid window ID should reject" ); }); add_task(async function test_non_string_return_number() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "getNum", description: "Returns a number", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.resolve(42), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let xpcomResult = await Services.modelContext.invokeTool( innerWindowId, "getNum" ); Assert.equal( xpcomResult, 42, "XPCOM invokeTool should return raw numeric value" ); Assert.equal( typeof xpcomResult, "number", "XPCOM result should be typeof number" ); } ); }); add_task(async function test_non_string_argument_types_xpcom() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "echo", description: "Echoes argument as JSON", inputSchema: { type: "object" }, execute: (input, _client) => Promise.resolve(JSON.stringify(input)), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let resultNum = await Services.modelContext.invokeTool( innerWindowId, "echo", { value: 42 } ); Assert.equal( resultNum, '{"value":42}', "XPCOM should handle object with number value" ); let resultBool = await Services.modelContext.invokeTool( innerWindowId, "echo", { value: true } ); Assert.equal( resultBool, '{"value":true}', "XPCOM should handle object with boolean value" ); let resultNested = await Services.modelContext.invokeTool( innerWindowId, "echo", { a: { b: [1, 2, 3] } } ); Assert.equal( resultNested, '{"a":{"b":[1,2,3]}}', "XPCOM should handle nested object argument" ); } ); }); add_task(async function test_tool_that_rejects_xpcom() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "fail", description: "Always fails", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.reject(new Error("boom")), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; await Assert.rejects( Services.modelContext.invokeTool(innerWindowId, "fail"), e => e.message === "boom", "XPCOM invokeTool should reject with the tool's rejection value" ); } ); }); add_task(async function test_tool_that_throws_xpcom() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "thrower", description: "Throws synchronously", inputSchema: { type: "object" }, execute: (_input, _client) => { throw new Error("sync boom"); }, }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; await Assert.rejects( Services.modelContext.invokeTool(innerWindowId, "thrower"), e => e.message === "sync boom", "XPCOM invokeTool should reject with the tool's thrown error" ); } ); }); add_task(async function test_unregister_tool() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "temp", description: "Temporary tool", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.resolve("hi"), }); content.navigator.modelContext.unregisterTool("temp"); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal( tools.length, 0, "XPCOM getToolsForWindow should return zero tools after unregister" ); await Assert.rejects( Services.modelContext.invokeTool(innerWindowId, "temp"), e => e.name === "NotFoundError", "XPCOM invokeTool on unregistered tool should reject" ); } ); }); add_task(async function test_multiple_tools() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "upper", description: "Uppercases a string", inputSchema: { type: "object" }, execute: (input, _client) => Promise.resolve(input.text.toUpperCase()), }); content.navigator.modelContext.registerTool({ name: "lower", description: "Lowercases a string", inputSchema: { type: "object" }, execute: (input, _client) => Promise.resolve(input.text.toLowerCase()), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal(tools.length, 2, "Should have two tools registered"); let names = tools.map(t => t.name).sort(); Assert.equal(names[0], "lower", "Should have 'lower' tool"); Assert.equal(names[1], "upper", "Should have 'upper' tool"); let upperResult = await Services.modelContext.invokeTool( innerWindowId, "upper", { text: "hello" } ); Assert.equal( upperResult, "HELLO", "XPCOM upper tool should return 'HELLO'" ); let lowerResult = await Services.modelContext.invokeTool( innerWindowId, "lower", { text: "HELLO" } ); Assert.equal( lowerResult, "hello", "XPCOM lower tool should return 'hello'" ); } ); }); add_task(async function test_undefined_arguments_xpcom() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "noArgs", description: "Takes no arguments", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.resolve("done"), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let result = await Services.modelContext.invokeTool( innerWindowId, "noArgs" ); Assert.equal(result, "done", "Tool invoked with no input should work"); } ); }); add_task(async function test_tool_without_inputSchema() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "noSchema", description: "Tool without inputSchema", execute: (_input, _client) => Promise.resolve("ok"), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal(tools.length, 1, "XPCOM should have one tool"); Assert.equal( tools[0].inputSchema, undefined, "inputSchema should be undefined when not provided" ); } ); }); add_task(async function test_tool_with_annotations() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "readOnly", description: "A read-only tool", inputSchema: { type: "object" }, annotations: { readOnlyHint: true }, execute: (_input, _client) => Promise.resolve("ok"), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal(tools.length, 1, "XPCOM should have one tool"); Assert.ok(tools[0].annotations, "XPCOM annotations should be present"); Assert.equal( tools[0].annotations.readOnlyHint, true, "XPCOM readOnlyHint should be true" ); } ); }); add_task(async function test_registerTool_rejects_non_object_inputSchema() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { const cases = [ /* eslint-disable no-new-wrappers */ { value: new Number(0x41414141), label: "Number wrapper" }, { value: new String("hello"), label: "String wrapper" }, { value: new Boolean(true), label: "Boolean wrapper" }, { value: [1, 2, 3], label: "array" }, /* eslint-enable no-new-wrappers */ ]; for (const { value, label } of cases) { Assert.throws( () => { content.navigator.modelContext.registerTool({ name: "poc_" + label, description: "poc", inputSchema: value, execute: async () => {}, }); }, /inputSchema must serialize to a JSON object/, `registerTool with ${label} inputSchema should throw TypeError` ); } let tools = content.navigator.modelContext.getTools(); Assert.equal( tools.length, 0, "content getTools should return empty after all registrations were rejected" ); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let xpcomTools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal( xpcomTools.length, 0, "XPCOM getToolsForWindow should return empty after all registrations were rejected" ); } ); }); add_task(async function test_tool_without_annotations() { await BrowserTestUtils.withNewTab( { gBrowser, url: "https://example.com" }, async browser => { await SpecialPowers.spawn(browser, [], async () => { content.navigator.modelContext.registerTool({ name: "noAnnotations", description: "A tool without annotations", inputSchema: { type: "object" }, execute: (_input, _client) => Promise.resolve("ok"), }); }); let innerWindowId = browser.browsingContext.currentWindowGlobal.innerWindowId; let tools = await Services.modelContext.getToolsForWindow(innerWindowId); Assert.equal(tools.length, 1, "XPCOM should have one tool"); Assert.equal( tools[0].annotations, undefined, "annotations should be undefined when not provided" ); } ); });