View on GitHub

Matimo - AI Tools Ecosystem

Define tools once in YAML, use them everywhere

Download this project as a .zip file Download this project as a tar.gz file

Testing Tools

Write and validate tests for Matimo tools.

Tool Validation

Validate All Tools

# Validate tool definitions (YAML syntax and schema)
pnpm validate-tools

# Output:
# ✅ tools/calculator/definition.yaml - Valid
# ✅ tools/gmail/send-email/definition.yaml - Valid
# ✅ tools/gmail/list-messages/definition.yaml - Valid

This validates:


Unit Tests

Test Tool Definition

import { describe, it, expect, beforeAll } from 'vitest';
import { MatimoInstance } from 'matimo';

describe('Tool Definition', () => {
  let matimo: Awaited<ReturnType<typeof MatimoInstance.init>>;

  beforeAll(async () => {
    matimo = await MatimoInstance.init('./tools');
  });

  it('should load calculator tool', () => {
    const tool = matimo.getTool('calculator');

    expect(tool).toBeDefined();
    expect(tool!.name).toBe('calculator');
    expect(tool!.description).toBe('Perform basic math operations');
    expect(tool!.version).toBe('1.0.0');
  });

  it('should have required parameters', () => {
    const tool = matimo.getTool('calculator');

    expect(tool!.parameters).toHaveProperty('operation');
    expect(tool!.parameters).toHaveProperty('a');
    expect(tool!.parameters).toHaveProperty('b');
  });

  it('should validate parameter types', () => {
    const tool = matimo.getTool('calculator');

    expect(tool!.parameters!.operation.type).toBe('string');
    expect(tool!.parameters!.a.type).toBe('number');
    expect(tool!.parameters!.b.type).toBe('number');
  });

  it('should have execution config', () => {
    const tool = matimo.getTool('calculator');

    expect(tool!.execution).toBeDefined();
    expect(tool!.execution.type).toMatch(/command|http/);
  });
});

Integration Tests

Test Tool Execution

import { describe, it, expect, beforeAll } from 'vitest';
import { MatimoInstance } from 'matimo';

describe('Calculator Tool Execution', () => {
  let m: Awaited<ReturnType<typeof MatimoInstance.init>>;

  beforeAll(async () => {
    m = await MatimoInstance.init('./tools');
  });

  it('should execute add operation', async () => {
    const result = await m.execute('calculator', {
      operation: 'add',
      a: 5,
      b: 3,
    });

    expect(result.result).toBe(8);
  });

  it('should execute subtract operation', async () => {
    const result = await m.execute('calculator', {
      operation: 'subtract',
      a: 10,
      b: 4,
    });

    expect(result.result).toBe(6);
  });

  it('should execute multiply operation', async () => {
    const result = await m.execute('calculator', {
      operation: 'multiply',
      a: 5,
      b: 3,
    });

    expect(result.result).toBe(15);
  });

  it('should execute divide operation', async () => {
    const result = await m.execute('calculator', {
      operation: 'divide',
      a: 10,
      b: 2,
    });

    expect(result.result).toBe(5);
  });
});

Parameter Validation Tests

Test Parameter Constraints

import { describe, it, expect, beforeAll } from 'vitest';
import { MatimoInstance } from 'matimo';

describe('Calculator Parameter Validation', () => {
  let m: Awaited<ReturnType<typeof MatimoInstance.init>>;

  beforeAll(async () => {
    m = await MatimoInstance.init('./tools');
  });

  it('should reject invalid operation', async () => {
    expect(async () => {
      await m.execute('calculator', {
        operation: 'invalid', // Not in enum
        a: 5,
        b: 3,
      });
    }).rejects.toThrow('INVALID_PARAMETERS');
  });

  it('should require all parameters', async () => {
    expect(async () => {
      await m.execute('calculator', {
        operation: 'add',
        // Missing: a, b
      });
    }).rejects.toThrow('INVALID_PARAMETERS');
  });

  it('should validate parameter types', async () => {
    expect(async () => {
      await m.execute('calculator', {
        operation: 'add',
        a: 'five', // Should be number
        b: 3,
      });
    }).rejects.toThrow('INVALID_PARAMETERS');
  });
});

OAuth2 Tool Tests

Test OAuth2 Tools

import { describe, it, expect, beforeAll } from 'vitest';
import { MatimoInstance } from 'matimo';

describe('Gmail Tool Execution', () => {
  let m: Awaited<ReturnType<typeof MatimoInstance.init>>;

  beforeAll(async () => {
    // Ensure GMAIL_ACCESS_TOKEN is set in environment
    if (!process.env.GMAIL_ACCESS_TOKEN) {
      console.warn('⚠️  Skipping Gmail tests: GMAIL_ACCESS_TOKEN not set');
      return;
    }

    m = await MatimoInstance.init('./tools');
  });

  it('should send email', async () => {
    if (!m) {
      console.warn('Skipping: no Matimo instance');
      return;
    }

    const result = await m.execute('gmail-send-email', {
      to: 'test@example.com',
      subject: 'Test Email',
      body: 'This is a test email',
    });

    expect(result.messageId).toBeDefined();
  });

  it('should list messages', async () => {
    if (!m) return;

    const result = await m.execute('gmail-list-messages', {
      maxResults: 10,
    });

    expect(result.messages).toBeDefined();
    expect(Array.isArray(result.messages)).toBe(true);
  });

  it('should handle missing auth token', async () => {
    // Clear token temporarily
    const saved = process.env.GMAIL_ACCESS_TOKEN;
    delete process.env.GMAIL_ACCESS_TOKEN;

    const m2 = await MatimoInstance.init('./tools');

    expect(async () => {
      await m2.execute('gmail-send-email', {
        to: 'test@example.com',
        subject: 'Test',
        body: 'Test',
      });
    }).rejects.toThrow('AUTH_FAILED');

    // Restore token
    if (saved) {
      process.env.GMAIL_ACCESS_TOKEN = saved;
    }
  });
});

Error Handling Tests

Test Error Conditions

import { describe, it, expect, beforeAll } from 'vitest';
import { MatimoInstance } from 'matimo';

describe('Error Handling', () => {
  let m: Awaited<ReturnType<typeof MatimoInstance.init>>;

  beforeAll(async () => {
    m = await MatimoInstance.init('./tools');
  });

  it('should throw TOOL_NOT_FOUND', async () => {
    expect(async () => {
      await m.execute('unknown-tool', {});
    }).rejects.toThrow('TOOL_NOT_FOUND');
  });

  it('should throw INVALID_PARAMETERS', async () => {
    expect(async () => {
      await m.execute('calculator', {
        operation: 'add',
        // Missing: a, b
      });
    }).rejects.toThrow('INVALID_PARAMETERS');
  });

  it('should include error details', async () => {
    try {
      await m.execute('calculator', {
        operation: 'invalid',
        a: 5,
        b: 3,
      });
      expect.fail('Should throw error');
    } catch (error) {
      expect(error.code).toBe('INVALID_PARAMETERS');
      expect(error.details).toBeDefined();
    }
  });
});

Running Tests

# Run all tests
pnpm test

# Run with coverage
pnpm test:coverage

# Run in watch mode (during development)
pnpm test:watch

# Run specific test file
pnpm test -- tools.test.ts

# Run tests matching pattern
pnpm test -- --grep "Calculator"

Test Coverage

View Coverage Report

pnpm test:coverage

# Output:
# ✅ 100% Statements
# ✅ 100% Branches
# ✅ 100% Functions
# ✅ 100% Lines

Best Practices

1. Test YAML Syntax First

Always validate YAML before testing execution:

pnpm validate-tools

2. Test Parameters Before Execution

// First: validate tool definition
const tool = m.getTool('calculator');
expect(tool.parameters).toBeDefined();

// Then: test execution
const result = await m.execute('calculator', params);

3. Mock External Calls (for unit tests)

import { describe, it, expect, vi } from 'vitest';

describe('Gmail Tool (Mocked)', () => {
  it('should call Gmail API', async () => {
    const mockExecute = vi.fn().mockResolvedValue({
      messageId: 'msg_123',
    });

    const result = await mockExecute({
      to: 'test@example.com',
      subject: 'Test',
      body: 'Test',
    });

    expect(result.messageId).toBe('msg_123');
  });
});

4. Test Error Cases

it('should handle execution errors', async () => {
  expect(async () => {
    await m.execute('calculator', {
      operation: 'divide',
      a: 10,
      b: 0, // Division by zero
    });
  }).rejects.toThrow();
});

Next Steps