Path: blob/main/extensions/copilot/src/extension/byok/common/test/geminiFunctionDeclarationConverter.spec.ts
13405 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Type } from '@google/genai';6import { describe, expect, it } from 'vitest';7import { toGeminiFunction, ToolJsonSchema } from '../geminiFunctionDeclarationConverter';89describe('GeminiFunctionDeclarationConverter', () => {10describe('toGeminiFunction', () => {11it('should convert basic function with simple parameters', () => {12const schema: ToolJsonSchema = {13type: 'object',14properties: {15name: {16type: 'string',17description: 'The name parameter'18},19age: {20type: 'number',21description: 'The age parameter'22},23isActive: {24type: 'boolean',25description: 'Whether the user is active'26}27},28required: ['name', 'age']29};3031const result = toGeminiFunction('testFunction', 'A test function', schema);3233expect(result.name).toBe('testFunction');34expect(result.description).toBe('A test function');35expect(result.parameters).toBeDefined();36expect(result.parameters!.type).toBe(Type.OBJECT);37expect(result.parameters!.required).toEqual(['name', 'age']);38expect(result.parameters!.properties).toBeDefined();39expect(result.parameters!.properties!['name']).toEqual({40type: Type.STRING,41description: 'The name parameter'42});43expect(result.parameters!.properties!['age']).toEqual({44type: Type.NUMBER,45description: 'The age parameter'46});47expect(result.parameters!.properties!['isActive']).toEqual({48type: Type.BOOLEAN,49description: 'Whether the user is active'50});51});5253it('should handle function with no description', () => {54const schema: ToolJsonSchema = {55type: 'object',56properties: {57value: { type: 'string' }58}59};6061const result = toGeminiFunction('noDescFunction', '', schema);6263expect(result.description).toBe('No description provided.');64});6566it('should handle integer type by mapping to INTEGER', () => {67const schema: ToolJsonSchema = {68type: 'object',69properties: {70count: {71type: 'integer',72description: 'An integer count'73},74groupIndex: {75type: 'integer',76description: 'Group index'77}78},79required: ['count']80};8182const result = toGeminiFunction('integerFunction', 'Function with integer parameters', schema);8384expect(result.parameters).toBeDefined();85expect(result.parameters!.type).toBe(Type.OBJECT);86expect(result.parameters!.required).toEqual(['count']);87expect(result.parameters!.properties).toBeDefined();88expect(result.parameters!.properties!['count']).toEqual({89type: Type.INTEGER,90description: 'An integer count'91});92expect(result.parameters!.properties!['groupIndex']).toEqual({93type: Type.INTEGER,94description: 'Group index'95});96});9798it('should handle null type by mapping to NULL', () => {99const schema: ToolJsonSchema = {100type: 'object',101properties: {102nullableField: {103type: 'null',104description: 'A nullable field'105}106}107};108109const result = toGeminiFunction('nullFunction', 'Function with null parameter', schema);110111expect(result.parameters).toBeDefined();112expect(result.parameters!.properties).toBeDefined();113expect(result.parameters!.properties!['nullableField']).toEqual({114type: Type.NULL,115description: 'A nullable field'116});117});118119it('should handle array schema by using items as parameters', () => {120const schema: ToolJsonSchema = {121type: 'array',122items: {123type: 'object',124properties: {125id: { type: 'string' },126count: { type: 'number' }127},128required: ['id']129}130};131132const result = toGeminiFunction('arrayFunction', 'Array function', schema);133134expect(result.parameters).toBeDefined();135expect(result.parameters!.type).toBe(Type.OBJECT);136expect(result.parameters!.required).toEqual(['id']);137expect(result.parameters!.properties).toBeDefined();138expect(result.parameters!.properties!['id']).toEqual({139type: Type.STRING140});141expect(result.parameters!.properties!['count']).toEqual({142type: Type.NUMBER143});144});145146it('should handle nested object properties', () => {147const schema: ToolJsonSchema = {148type: 'object',149properties: {150user: {151type: 'object',152description: 'User information',153properties: {154profile: {155type: 'object',156properties: {157firstName: { type: 'string' },158lastName: { type: 'string' }159},160required: ['firstName']161},162settings: {163type: 'object',164properties: {165theme: { type: 'string' },166notifications: { type: 'boolean' }167}168}169},170required: ['profile']171}172}173};174175const result = toGeminiFunction('nestedFunction', 'Function with nested objects', schema);176177expect(result.parameters).toBeDefined();178expect(result.parameters!.properties).toBeDefined();179const userProperty = result.parameters!.properties!['user'];180expect(userProperty.type).toBe(Type.OBJECT);181expect(userProperty.description).toBe('User information');182expect(userProperty.required).toEqual(['profile']);183expect(userProperty.properties).toBeDefined();184185const profileProperty = userProperty.properties!['profile'];186expect(profileProperty.type).toBe(Type.OBJECT);187expect(profileProperty.required).toEqual(['firstName']);188expect(profileProperty.properties).toBeDefined();189expect(profileProperty.properties!['firstName']).toEqual({190type: Type.STRING191});192expect(profileProperty.properties!['lastName']).toEqual({193type: Type.STRING194});195196const settingsProperty = userProperty.properties!['settings'];197expect(settingsProperty.type).toBe(Type.OBJECT);198expect(settingsProperty.properties).toBeDefined();199expect(settingsProperty.properties!['theme']).toEqual({200type: Type.STRING201});202expect(settingsProperty.properties!['notifications']).toEqual({203type: Type.BOOLEAN204});205});206207it('should handle array properties with primitive items', () => {208const schema: ToolJsonSchema = {209type: 'object',210properties: {211tags: {212type: 'array',213description: 'List of tags',214items: {215type: 'string',216description: 'Individual tag'217}218},219scores: {220type: 'array',221items: {222type: 'number'223}224}225}226};227228const result = toGeminiFunction('arrayPropsFunction', 'Function with arrays', schema);229230expect(result.parameters).toBeDefined();231expect(result.parameters!.properties).toBeDefined();232const tagsProperty = result.parameters!.properties!['tags'];233expect(tagsProperty.type).toBe(Type.ARRAY);234expect(tagsProperty.description).toBe('List of tags');235expect(tagsProperty.items).toEqual({236type: Type.STRING,237description: 'Individual tag'238});239240const scoresProperty = result.parameters!.properties!['scores'];241expect(scoresProperty.type).toBe(Type.ARRAY);242expect(scoresProperty.items).toEqual({243type: Type.NUMBER244});245});246247it('should handle array properties with object items', () => {248const schema: ToolJsonSchema = {249type: 'object',250properties: {251items: {252type: 'array',253description: 'List of items',254items: {255type: 'object',256description: 'Individual item',257properties: {258id: { type: 'string' },259name: { type: 'string' },260metadata: {261type: 'object',262properties: {263created: { type: 'string' },264version: { type: 'number' }265}266}267},268required: ['id', 'name']269}270}271}272};273274const result = toGeminiFunction('complexArrayFunction', 'Function with complex arrays', schema);275276expect(result.parameters).toBeDefined();277expect(result.parameters!.properties).toBeDefined();278const itemsProperty = result.parameters!.properties!['items'];279expect(itemsProperty.type).toBe(Type.ARRAY);280expect(itemsProperty.description).toBe('List of items');281expect(itemsProperty.items).toBeDefined();282expect(itemsProperty.items!.type).toBe(Type.OBJECT);283expect(itemsProperty.items!.description).toBe('Individual item');284expect(itemsProperty.items!.required).toEqual(['id', 'name']);285expect(itemsProperty.items!.properties).toBeDefined();286expect(itemsProperty.items!.properties!['id']).toEqual({287type: Type.STRING288});289expect(itemsProperty.items!.properties!['name']).toEqual({290type: Type.STRING291});292expect(itemsProperty.items!.properties!['metadata'].type).toBe(Type.OBJECT);293expect(itemsProperty.items!.properties!['metadata'].properties).toBeDefined();294expect(itemsProperty.items!.properties!['metadata'].properties!['created']).toEqual({295type: Type.STRING296});297expect(itemsProperty.items!.properties!['metadata'].properties!['version']).toEqual({298type: Type.NUMBER299});300});301302it('should handle enum properties', () => {303const schema: ToolJsonSchema = {304type: 'object',305properties: {306status: {307type: 'string',308description: 'Status value',309enum: ['active', 'inactive', 'pending']310},311priority: {312type: 'string',313enum: ['1', '2', '3', '4', '5']314}315}316};317318const result = toGeminiFunction('enumFunction', 'Function with enums', schema);319320expect(result.parameters).toBeDefined();321expect(result.parameters!.properties).toBeDefined();322const statusProperty = result.parameters!.properties!['status'];323expect(statusProperty.type).toBe(Type.STRING);324expect(statusProperty.description).toBe('Status value');325expect(statusProperty.enum).toEqual(['active', 'inactive', 'pending']);326327const priorityProperty = result.parameters!.properties!['priority'];328expect(priorityProperty.type).toBe(Type.STRING);329expect(priorityProperty.enum).toEqual(['1', '2', '3', '4', '5']);330});331332it('should handle anyOf composition by using first option', () => {333const schema: ToolJsonSchema = {334type: 'object',335properties: {336value: {337anyOf: [338{ type: 'string', description: 'String value' },339{ type: 'number', description: 'Number value' }340]341}342}343};344345const result = toGeminiFunction('anyOfFunction', 'Function with anyOf', schema);346347expect(result.parameters).toBeDefined();348expect(result.parameters!.properties).toBeDefined();349const valueProperty = result.parameters!.properties!['value'];350expect(valueProperty.type).toBe(Type.STRING);351expect(valueProperty.description).toBe('String value');352});353354it('should handle oneOf composition by using first option', () => {355const schema: ToolJsonSchema = {356type: 'object',357properties: {358data: {359oneOf: [360{ type: 'boolean', description: 'Boolean data' },361{ type: 'string', description: 'String data' }362]363}364}365};366367const result = toGeminiFunction('oneOfFunction', 'Function with oneOf', schema);368369expect(result.parameters).toBeDefined();370expect(result.parameters!.properties).toBeDefined();371const dataProperty = result.parameters!.properties!['data'];372expect(dataProperty.type).toBe(Type.BOOLEAN);373expect(dataProperty.description).toBe('Boolean data');374});375376it('should handle allOf composition by using first option', () => {377const schema: ToolJsonSchema = {378type: 'object',379properties: {380config: {381allOf: [382{ type: 'object', description: 'Config object' },383{ type: 'string', description: 'Config string' }384]385}386}387};388389const result = toGeminiFunction('allOfFunction', 'Function with allOf', schema);390391expect(result.parameters).toBeDefined();392expect(result.parameters!.properties).toBeDefined();393const configProperty = result.parameters!.properties!['config'];394expect(configProperty.type).toBe(Type.OBJECT);395expect(configProperty.description).toBe('Config object');396});397398it('should handle schema with no properties', () => {399const schema: ToolJsonSchema = {400type: 'object'401};402403const result = toGeminiFunction('emptyFunction', 'Function with no properties', schema);404405expect(result.parameters).toBeDefined();406expect(result.parameters!.type).toBe(Type.OBJECT);407expect(result.parameters!.properties).toEqual({});408expect(result.parameters!.required).toEqual([]);409});410411it('should handle schema with no required fields', () => {412const schema: ToolJsonSchema = {413type: 'object',414properties: {415optional1: { type: 'string' },416optional2: { type: 'number' }417}418};419420const result = toGeminiFunction('optionalFunction', 'Function with optional params', schema);421422expect(result.parameters).toBeDefined();423expect(result.parameters!.required).toEqual([]);424expect(result.parameters!.properties).toBeDefined();425expect(result.parameters!.properties!['optional1']).toEqual({426type: Type.STRING427});428expect(result.parameters!.properties!['optional2']).toEqual({429type: Type.NUMBER430});431});432433it('should default to object type when type is missing', () => {434const schema: ToolJsonSchema = {435properties: {436field: {437description: 'Field without type'438}439}440};441442const result = toGeminiFunction('defaultTypeFunction', 'Function with missing types', schema);443444expect(result.parameters).toBeDefined();445expect(result.parameters!.properties).toBeDefined();446const fieldProperty = result.parameters!.properties!['field'];447expect(fieldProperty.type).toBe(Type.OBJECT);448expect(fieldProperty.description).toBe('Field without type');449});450});451});452453