Files
waggle-os/packages/agent/tests/connectors/connectors-composio.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

209 lines
8.2 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { ComposioConnector } from '../../src/connectors/composio-connector.js';
import type { VaultStore } from '@waggle/core';
function createMockVault(cred?: { value: string; isExpired: boolean }): VaultStore {
return {
getConnectorCredential: vi.fn((id: string) => {
if (id === 'composio' && cred) return { ...cred, type: 'api_key' };
return null;
}),
get: vi.fn(() => null),
set: vi.fn(),
delete: vi.fn(),
list: vi.fn(() => []),
has: vi.fn(() => false),
setConnectorCredential: vi.fn(),
migrateFromConfig: vi.fn(() => 0),
} as unknown as VaultStore;
}
describe('ComposioConnector', () => {
let connector: ComposioConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new ComposioConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, and service', () => {
expect(connector.id).toBe('composio');
expect(connector.name).toBe('Composio (250+ services)');
expect(connector.service).toBe('composio.dev');
expect(connector.authType).toBe('api_key');
expect(connector.substrate).toBe('waggle');
});
it('has all 5 actions', () => {
expect(connector.actions).toHaveLength(5);
const names = connector.actions.map(a => a.name);
expect(names).toContain('list_integrations');
expect(names).toContain('list_actions');
expect(names).toContain('execute_action');
expect(names).toContain('list_connected_accounts');
expect(names).toContain('search_actions');
});
it('execute_action has high risk level', () => {
const executeAction = connector.actions.find(a => a.name === 'execute_action');
expect(executeAction).toBeDefined();
expect(executeAction!.riskLevel).toBe('high');
});
it('list/search actions have low risk level', () => {
const lowRiskActions = connector.actions.filter(a => a.name !== 'execute_action');
expect(lowRiskActions).toHaveLength(4);
for (const action of lowRiskActions) {
expect(action.riskLevel).toBe('low');
}
});
it('connect() retrieves API key from vault', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('composio');
});
it('execute returns error when not connected', async () => {
const vault = createMockVault(); // no credential
await connector.connect(vault);
const result = await connector.execute('list_integrations', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected without API key', async () => {
const vault = createMockVault(); // no credential
await connector.connect(vault);
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('composio');
});
it('healthCheck returns connected when API responds OK', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ items: [] }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
expect(health.id).toBe('composio');
});
it('healthCheck returns error when API fails', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: false, status: 401, text: async () => 'Unauthorized' }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('error');
expect(health.error).toContain('401');
});
it('execute(list_integrations) calls correct endpoint', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const mockData = { items: [{ id: 'int_1', name: 'GitHub' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('list_integrations', {});
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/integrations');
});
it('execute(list_actions) passes appName query param', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const mockActions = { items: [{ name: 'GITHUB_CREATE_ISSUE' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockActions }) as unknown as typeof fetch;
const result = await connector.execute('list_actions', { appName: 'github' });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockActions);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('appName=github');
});
it('execute(execute_action) POSTs to correct endpoint with params', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const mockResult = { execution_output: { status: 'success' } };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockResult }) as unknown as typeof fetch;
const result = await connector.execute('execute_action', {
actionId: 'GITHUB_CREATE_ISSUE',
params: { title: 'Test issue', body: 'Test body' },
connectedAccountId: 'acc_123',
});
expect(result.success).toBe(true);
expect((result.data as Record<string, unknown>).actionId).toBe('GITHUB_CREATE_ISSUE');
expect((result.data as Record<string, unknown>).service).toBe('composio');
expect((result.data as Record<string, unknown>).result).toEqual(mockResult);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('/actions/GITHUB_CREATE_ISSUE/execute');
expect(fetchCall[1].method).toBe('POST');
});
it('execute(execute_action) returns error without actionId', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('execute_action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('actionId is required');
});
it('execute(search_actions) passes searchQuery param', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const mockResults = { items: [{ name: 'GMAIL_SEND_EMAIL' }] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockResults }) as unknown as typeof fetch;
const result = await connector.execute('search_actions', { searchQuery: 'send email' });
expect(result.success).toBe(true);
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
expect(fetchCall[0]).toContain('searchQuery=send+email');
});
it('execute() returns error for unknown action', async () => {
const vault = createMockVault({ value: 'cmp_test_key', isExpired: false });
await connector.connect(vault);
const result = await connector.execute('unknown_action', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Unknown action');
});
it('toDefinition() maps correctly', () => {
const def = connector.toDefinition('connected');
expect(def.id).toBe('composio');
expect(def.tools).toContain('connector_composio_list_integrations');
expect(def.tools).toContain('connector_composio_execute_action');
expect(def.tools).toContain('connector_composio_search_actions');
expect(def.tools).toHaveLength(5);
expect(def.actions).toHaveLength(5);
expect(def.capabilities).toContain('read');
expect(def.capabilities).toContain('write');
expect(def.capabilities).toContain('search');
});
});