From 910dcba90965f7db484a207551c9be342d8bc1f0 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 12 Jan 2025 15:14:51 +0100 Subject: [PATCH 1/2] Complete the function.ts implementation with improved error handling and validation --- src/function.ts | 120 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 80 insertions(+), 40 deletions(-) diff --git a/src/function.ts b/src/function.ts index f773bb46..c400793a 100644 --- a/src/function.ts +++ b/src/function.ts @@ -1,11 +1,14 @@ export enum ExecutableGameFunctionStatus { - Done = "done", - Failed = "failed", + Done = 'done', + Failed = 'failed', + InProgress = 'in_progress' } -export type ExecutableGameFunctionResponseJSON = ReturnType< - ExecutableGameFunctionResponse["toJSON"] ->; +export interface ExecutableGameFunctionResponseJSON { + action_id: string; + action_status: ExecutableGameFunctionStatus; + feedback_message: string; +} export class ExecutableGameFunctionResponse { constructor( @@ -13,7 +16,7 @@ export class ExecutableGameFunctionResponse { public feedback: string ) {} - toJSON(id: string) { + toJSON(id: string): ExecutableGameFunctionResponseJSON { return { action_id: id, action_status: this.status, @@ -22,53 +25,39 @@ export class ExecutableGameFunctionResponse { } } -interface IGameFunction { - name: string; - description: string; - args: T; - executable: ( - args: Partial>, - logger: (msg: string) => void - ) => Promise; - hint?: string; -} - export interface GameFunctionArg { name: string; description: string; type?: string; optional?: boolean; + defaultValue?: string; + validator?: (value: string) => boolean; } -export type GameFunctionBase = { +export interface IGameFunction { name: string; description: string; - args: GameFunctionArg[]; + args: T; executable: ( - args: Record, + args: Partial>, logger: (msg: string) => void ) => Promise; hint?: string; - execute: ( - args: Record, - logger: (msg: string) => void - ) => Promise; - toJSON(): Object; -}; +} -type ExecutableArgs = { - [K in T[number]["name"]]: string; +export type ExecutableArgs = { + [K in T[number]['name']]: string; }; class GameFunction implements IGameFunction { - public name: string; - public description: string; - public args: T; - public executable: ( + public readonly name: string; + public readonly description: string; + public readonly args: T; + public readonly executable: ( args: Partial>, logger: (msg: string) => void ) => Promise; - public hint?: string; + public readonly hint?: string; constructor(options: IGameFunction) { this.name = options.name; @@ -76,9 +65,36 @@ class GameFunction implements IGameFunction { this.args = options.args; this.executable = options.executable; this.hint = options.hint; + + // Validate the function configuration + this.validateConfiguration(); } - toJSON() { + private validateConfiguration(): void { + if (!this.name || this.name.trim() === '') { + throw new Error('Function name is required'); + } + + if (!this.description || this.description.trim() === '') { + throw new Error('Function description is required'); + } + + if (!Array.isArray(this.args)) { + throw new Error('Function args must be an array'); + } + + this.args.forEach((arg, index) => { + if (!arg.name || arg.name.trim() === '') { + throw new Error(`Argument at index ${index} must have a name`); + } + + if (!arg.description || arg.description.trim() === '') { + throw new Error(`Argument ${arg.name} must have a description`); + } + }); + } + + toJSON(): Record { return { fn_name: this.name, fn_description: this.description, @@ -87,13 +103,29 @@ class GameFunction implements IGameFunction { }; } + private validateArgs(args: Record): void { + this.args.forEach((arg) => { + const value = args[arg.name]?.value; + + if (!arg.optional && (value === undefined || value === null || value === '')) { + throw new Error(`Required argument ${arg.name} is missing`); + } + + if (value && arg.validator && !arg.validator(value)) { + throw new Error(`Invalid value for argument ${arg.name}`); + } + }); + } + async execute( - args: { - [key in GameFunctionArg["name"]]: { value: string }; - }, + args: Record, logger: (msg: string) => void - ) { - const argValues: ExecutableArgs = Object.keys(args).reduce( + ): Promise { + // Validate input arguments + this.validateArgs(args); + + // Convert args to expected format + const argValues = Object.keys(args).reduce( (acc, key) => { acc[key as keyof ExecutableArgs] = args[key]?.value; return acc; @@ -101,7 +133,15 @@ class GameFunction implements IGameFunction { {} as ExecutableArgs ); - return await this.executable(argValues, logger); + try { + return await this.executable(argValues, logger); + } catch (error) { + logger(`Error executing function ${this.name}: ${error.message}`); + return new ExecutableGameFunctionResponse( + ExecutableGameFunctionStatus.Failed, + `Function execution failed: ${error.message}` + ); + } } } From ec8f1268a4e73a9b2f74f3891413503ca5c63460 Mon Sep 17 00:00:00 2001 From: spacexbt Date: Sun, 12 Jan 2025 15:15:14 +0100 Subject: [PATCH 2/2] Add comprehensive tests for agent and function components --- tests/agent.test.ts | 64 ++++++++++++++++++++++++++++++ tests/function.test.ts | 88 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 152 insertions(+) create mode 100644 tests/agent.test.ts create mode 100644 tests/function.test.ts diff --git a/tests/agent.test.ts b/tests/agent.test.ts new file mode 100644 index 00000000..91bb4ce6 --- /dev/null +++ b/tests/agent.test.ts @@ -0,0 +1,64 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import GameAgent from '../src/agent'; +import GameWorker from '../src/worker'; +import { ExecutableGameFunctionResponse, ExecutableGameFunctionStatus } from '../src/function'; +import { ActionType } from '../src/api'; +import { AgentNotInitializedError, WorkerNotFoundError } from '../src/errors'; + +describe('GameAgent', () => { + const mockApiKey = 'test-api-key'; + const mockWorker = new GameWorker({ + id: 'worker1', + name: 'Test Worker', + description: 'Test worker description', + functions: [] + }); + + const mockAgentConfig = { + name: 'Test Agent', + goal: 'Test Goal', + description: 'Test Description', + workers: [mockWorker] + }; + + it('should create agent instance correctly', () => { + const agent = new GameAgent(mockApiKey, mockAgentConfig); + expect(agent).toBeInstanceOf(GameAgent); + }); + + it('should throw error when not initialized', async () => { + const agent = new GameAgent(mockApiKey, mockAgentConfig); + await expect(agent.step()).rejects.toThrow(AgentNotInitializedError); + }); + + it('should throw error for invalid worker', () => { + const agent = new GameAgent(mockApiKey, mockAgentConfig); + // @ts-ignore - Accessing private method for testing + expect(() => agent.getWorkerById('invalid-id')).toThrow(WorkerNotFoundError); + }); + + it('should handle initialization correctly', async () => { + const agent = new GameAgent(mockApiKey, mockAgentConfig); + const mockMap = { id: 'map1' }; + const mockAgentResponse = { id: 'agent1' }; + + // Mock the GameClient methods + agent['gameClient'].createMap = jest.fn().mockResolvedValue(mockMap); + agent['gameClient'].createAgent = jest.fn().mockResolvedValue(mockAgentResponse); + + await agent.init(); + + expect(agent['state'].mapId).toBe(mockMap.id); + expect(agent['state'].agentId).toBe(mockAgentResponse.id); + }); + + it('should handle cleanup correctly', async () => { + const agent = new GameAgent(mockApiKey, mockAgentConfig); + await agent.cleanup(); + + expect(agent['state'].agentId).toBeNull(); + expect(agent['state'].mapId).toBeNull(); + expect(agent['state'].currentWorkerId).toBe(mockWorker.id); + expect(agent['state'].lastActionResult).toBeNull(); + }); +}); diff --git a/tests/function.test.ts b/tests/function.test.ts new file mode 100644 index 00000000..5ccecd16 --- /dev/null +++ b/tests/function.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it, jest } from '@jest/globals'; +import GameFunction, { + ExecutableGameFunctionResponse, + ExecutableGameFunctionStatus, + GameFunctionArg +} from '../src/function'; + +describe('GameFunction', () => { + const mockArgs: GameFunctionArg[] = [ + { + name: 'arg1', + description: 'Test argument 1', + optional: false + } + ]; + + const mockExecutable = async (args: any, logger: any) => { + return new ExecutableGameFunctionResponse(ExecutableGameFunctionStatus.Done, 'Success'); + }; + + const createMockFunction = () => { + return new GameFunction({ + name: 'testFunction', + description: 'Test function description', + args: mockArgs, + executable: mockExecutable + }); + }; + + it('should create function instance correctly', () => { + const fn = createMockFunction(); + expect(fn).toBeInstanceOf(GameFunction); + }); + + it('should validate configuration on creation', () => { + expect(() => { + new GameFunction({ + name: '', + description: 'Test', + args: mockArgs, + executable: mockExecutable + }); + }).toThrow('Function name is required'); + }); + + it('should validate required arguments', async () => { + const fn = createMockFunction(); + const mockLogger = jest.fn(); + + await expect( + fn.execute({}, mockLogger) + ).rejects.toThrow('Required argument arg1 is missing'); + }); + + it('should execute successfully with valid arguments', async () => { + const fn = createMockFunction(); + const mockLogger = jest.fn(); + + const result = await fn.execute( + { arg1: { value: 'test' } }, + mockLogger + ); + + expect(result).toBeInstanceOf(ExecutableGameFunctionResponse); + expect(result.status).toBe(ExecutableGameFunctionStatus.Done); + }); + + it('should handle execution errors gracefully', async () => { + const errorFn = new GameFunction({ + name: 'errorFunction', + description: 'Test error function', + args: mockArgs, + executable: async () => { + throw new Error('Test error'); + } + }); + + const mockLogger = jest.fn(); + const result = await errorFn.execute( + { arg1: { value: 'test' } }, + mockLogger + ); + + expect(result.status).toBe(ExecutableGameFunctionStatus.Failed); + expect(result.feedback).toContain('Test error'); + expect(mockLogger).toHaveBeenCalled(); + }); +});