Skip to main content
Glama
firebase
by firebase
tool_test.ts10.6 kB
/** * Copyright 2025 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { action, z } from '@genkit-ai/core'; import { initNodeFeatures } from '@genkit-ai/core/node'; import { Registry } from '@genkit-ai/core/registry'; import * as assert from 'assert'; import { afterEach, describe, it } from 'node:test'; import { defineInterrupt, defineTool, isDynamicTool, tool, } from '../src/tool.js'; initNodeFeatures(); describe('defineInterrupt', () => { let registry = new Registry(); registry.apiStability = 'beta'; afterEach(() => { registry = new Registry(); registry.apiStability = 'beta'; }); function expectInterrupt(fn: () => any, metadata?: Record<string, any>) { return assert.rejects(fn, { name: 'ToolInterruptError', metadata }); } it('should throw a simple interrupt with no metadata', async () => { const simple = defineInterrupt(registry, { name: 'simple', description: 'simple interrupt', }); await expectInterrupt(async () => { await simple({}); }); }); it('should throw a simple interrupt with fixed metadata', async () => { const simple = defineInterrupt(registry, { name: 'simple', description: 'simple interrupt', requestMetadata: { foo: 'bar' }, }); await expectInterrupt( async () => { await simple({}); }, { foo: 'bar' } ); }); it('should throw a simple interrupt with function-returned metadata', async () => { const simple = defineInterrupt(registry, { name: 'simple', description: 'simple interrupt', inputSchema: z.string(), requestMetadata: (foo) => ({ foo }), }); await expectInterrupt( async () => { await simple('bar'); }, { foo: 'bar' } ); }); it('should throw a simple interrupt with async function-returned metadata', async () => { const simple = defineInterrupt(registry, { name: 'simple', description: 'simple interrupt', inputSchema: z.string(), requestMetadata: async (foo) => ({ foo }), }); await expectInterrupt( async () => { await simple('bar'); }, { foo: 'bar' } ); }); it('should register the reply schema / json schema as the output schema of the tool', () => { const ResponseSchema = z.object({ foo: z.string() }); const simple = defineInterrupt(registry, { name: 'simple', description: 'simple', outputSchema: ResponseSchema, }); assert.equal(simple.__action.outputSchema, ResponseSchema); const simple2 = defineInterrupt(registry, { name: 'simple2', description: 'simple2', outputJsonSchema: { type: 'string' }, }); assert.deepStrictEqual(simple2.__action.outputJsonSchema, { type: 'string', }); }); describe('multipart tools', () => { it('should define a multipart tool', async () => { const t = defineTool( registry, { name: 'test', description: 'test', multipart: true }, async () => { return { output: 'main output', content: [{ text: 'part 1' }], }; } ); assert.equal(t.__action.metadata.type, 'tool.v2'); assert.equal(t.__action.actionType, 'tool.v2'); const result = await t({}); assert.deepStrictEqual(result, { output: 'main output', content: [{ text: 'part 1' }], }); }); it('should handle fallback output', async () => { const t = defineTool( registry, { name: 'test', description: 'test', multipart: true }, async () => { return { content: [{ text: 'part 1' }], }; } ); const result = await t({}); assert.deepStrictEqual(result, { content: [{ text: 'part 1' }], }); }); }); }); describe('isDynamicTool', () => { let registry = new Registry(); registry.apiStability = 'beta'; afterEach(() => { registry = new Registry(); registry.apiStability = 'beta'; }); it('should return true for a dynamic tool', () => { const dynamic = tool({ name: 'dynamic', description: 'test' }); assert.strictEqual(isDynamicTool(dynamic), true); }); it('should remain dynamic after registration', () => { const dynamic = tool({ name: 'dynamic', description: 'test' }); assert.strictEqual(isDynamicTool(dynamic), true); registry.registerAction('tool', dynamic); assert.strictEqual(isDynamicTool(dynamic), true); }); it('should return false for a registered tool', () => { const regular = defineTool( registry, { name: 'regular', description: 'test' }, async () => {} ); assert.strictEqual(isDynamicTool(regular), false); }); it('should return false for a non-tool action', () => { const regularAction = action( { actionType: 'util', name: 'regularAction', description: 'test' }, async () => {} ); assert.strictEqual(isDynamicTool(regularAction), false); }); it('should return false for a non-action', () => { assert.strictEqual(isDynamicTool({}), false); assert.strictEqual(isDynamicTool('tool'), false); assert.strictEqual(isDynamicTool(123), false); }); }); describe('defineTool', () => { let registry = new Registry(); registry.apiStability = 'beta'; afterEach(() => { registry = new Registry(); registry.apiStability = 'beta'; }); describe('.respond()', () => { it('constructs a ToolResponsePart', () => { const t = defineTool( registry, { name: 'test', description: 'test' }, async () => {} ); assert.deepStrictEqual( t.respond({ toolRequest: { name: 'test', input: {} } }, 'output'), { toolResponse: { name: 'test', output: 'output', }, metadata: { interruptResponse: true, }, } ); }); it('includes metadata', () => { const t = defineTool( registry, { name: 'test', description: 'test' }, async () => {} ); assert.deepStrictEqual( t.respond({ toolRequest: { name: 'test', input: {} } }, 'output', { metadata: { extra: 'data' }, }), { toolResponse: { name: 'test', output: 'output', }, metadata: { interruptResponse: { extra: 'data' }, }, } ); }); it('validates schema', () => { const t = defineTool( registry, { name: 'test', description: 'test', outputSchema: z.number() }, async (input, { interrupt }) => interrupt() ); assert.throws( () => { t.respond( { toolRequest: { name: 'test', input: {} } }, 'not_a_number' as any ); }, { name: 'GenkitError', status: 'INVALID_ARGUMENT' } ); assert.deepStrictEqual( t.respond({ toolRequest: { name: 'test', input: {} } }, 55), { toolResponse: { name: 'test', output: 55, }, metadata: { interruptResponse: true, }, } ); }); }); describe('.restart()', () => { it('constructs a ToolRequestPart', () => { const t = defineTool( registry, { name: 'test', description: 'test' }, async () => {} ); assert.deepStrictEqual( t.restart({ toolRequest: { name: 'test', input: {} } }), { toolRequest: { name: 'test', input: {}, }, metadata: { resumed: true, }, } ); }); it('includes metadata', () => { const t = defineTool( registry, { name: 'test', description: 'test' }, async () => {} ); assert.deepStrictEqual( t.restart( { toolRequest: { name: 'test', input: {} } }, { extra: 'data' } ), { toolRequest: { name: 'test', input: {}, }, metadata: { resumed: { extra: 'data' }, }, } ); }); it('validates schema', () => { const t = defineTool( registry, { name: 'test', description: 'test', inputSchema: z.number() }, async (input, { interrupt }) => interrupt() ); assert.throws( () => { t.restart({ toolRequest: { name: 'test', input: {} } }, undefined, { replaceInput: 'not_a_number' as any, }); }, { name: 'GenkitError', status: 'INVALID_ARGUMENT' } ); assert.deepStrictEqual( t.restart({ toolRequest: { name: 'test', input: {} } }, undefined, { replaceInput: 55, }), { toolRequest: { name: 'test', input: 55, }, metadata: { resumed: true, replacedInput: {}, }, } ); }); }); it('should register a v1 tool as v2 as well', async () => { defineTool(registry, { name: 'test', description: 'test' }, async () => {}); assert.ok(await registry.lookupAction('/tool/test')); assert.ok(await registry.lookupAction('/tool.v2/test')); }); it('should only register a multipart tool as v2', async () => { defineTool( registry, { name: 'test', description: 'test', multipart: true }, async () => {} ); assert.ok(await registry.lookupAction('/tool.v2/test')); assert.equal(await registry.lookupAction('/tool/test'), undefined); }); it('should wrap v1 tool output when called as v2', async () => { defineTool( registry, { name: 'test', description: 'test' }, async () => 'foo' ); const action = await registry.lookupAction('/tool.v2/test'); assert.ok(action); const result = await action!({}); assert.deepStrictEqual(result, { output: 'foo' }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/firebase/genkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server