Files
waggle-os/packages/agent/tests/connectors/connectors-google.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

489 lines
19 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { GmailConnector } from '../../src/connectors/gmail-connector.js';
import { GoogleDocsConnector } from '../../src/connectors/gdocs-connector.js';
import { GoogleDriveConnector } from '../../src/connectors/gdrive-connector.js';
import { GoogleSheetsConnector } from '../../src/connectors/gsheets-connector.js';
import type { VaultStore } from '@waggle/core';
function createMockVault(connectorId: string, cred?: { value: string; isExpired: boolean }): VaultStore {
return {
getConnectorCredential: vi.fn((id: string) => {
if (id === connectorId && cred) return { ...cred, type: 'bearer' };
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;
}
// ─── Gmail Connector ───
describe('GmailConnector', () => {
let connector: GmailConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GmailConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, and service', () => {
expect(connector.id).toBe('gmail');
expect(connector.name).toBe('Gmail');
expect(connector.service).toBe('gmail.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected actions', () => {
const actionNames = connector.actions.map(a => a.name);
expect(actionNames).toContain('list_messages');
expect(actionNames).toContain('get_message');
expect(actionNames).toContain('send_message');
expect(actionNames).toContain('search_messages');
expect(actionNames).toContain('list_labels');
});
it('has at least 5 actions with required fields', () => {
expect(connector.actions.length).toBeGreaterThanOrEqual(5);
for (const action of connector.actions) {
expect(action.name).toBeTruthy();
expect(action.description).toBeTruthy();
expect(action.inputSchema).toBeDefined();
expect(['low', 'medium', 'high']).toContain(action.riskLevel);
}
});
it('has correct risk levels', () => {
const listMessages = connector.actions.find(a => a.name === 'list_messages');
const getMessage = connector.actions.find(a => a.name === 'get_message');
const sendMessage = connector.actions.find(a => a.name === 'send_message');
const searchMessages = connector.actions.find(a => a.name === 'search_messages');
const listLabels = connector.actions.find(a => a.name === 'list_labels');
expect(listMessages?.riskLevel).toBe('low');
expect(getMessage?.riskLevel).toBe('low');
expect(sendMessage?.riskLevel).toBe('medium');
expect(searchMessages?.riskLevel).toBe('low');
expect(listLabels?.riskLevel).toBe('low');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_messages', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected when no token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('gmail');
});
it('connect retrieves token from vault', async () => {
const vault = createMockVault('gmail', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('gmail');
});
it('healthCheck returns connected when API responds OK', async () => {
const vault = createMockVault('gmail', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ emailAddress: 'user@gmail.com' }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('execute(list_messages) calls Gmail API', async () => {
const vault = createMockVault('gmail', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
const mockData = { messages: [{ id: '123', threadId: 'abc' }], resultSizeEstimate: 1 };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('list_messages', { maxResults: 5 });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
});
it('execute returns error for unknown action', async () => {
const vault = createMockVault('gmail', { value: 'ya29.test_token', 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('gmail');
expect(def.tools).toContain('connector_gmail_list_messages');
expect(def.tools).toContain('connector_gmail_send_message');
expect(def.actions.length).toBe(connector.actions.length);
});
});
// ─── Google Docs Connector ───
describe('GoogleDocsConnector', () => {
let connector: GoogleDocsConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GoogleDocsConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, and service', () => {
expect(connector.id).toBe('gdocs');
expect(connector.name).toBe('Google Docs');
expect(connector.service).toBe('docs.google.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected actions', () => {
const actionNames = connector.actions.map(a => a.name);
expect(actionNames).toContain('get_document');
expect(actionNames).toContain('create_document');
expect(actionNames).toContain('update_document');
expect(actionNames).toContain('list_comments');
});
it('has at least 4 actions with required fields', () => {
expect(connector.actions.length).toBeGreaterThanOrEqual(4);
for (const action of connector.actions) {
expect(action.name).toBeTruthy();
expect(action.description).toBeTruthy();
expect(action.inputSchema).toBeDefined();
expect(['low', 'medium', 'high']).toContain(action.riskLevel);
}
});
it('has correct risk levels', () => {
const getDoc = connector.actions.find(a => a.name === 'get_document');
const createDoc = connector.actions.find(a => a.name === 'create_document');
const updateDoc = connector.actions.find(a => a.name === 'update_document');
const listComments = connector.actions.find(a => a.name === 'list_comments');
expect(getDoc?.riskLevel).toBe('low');
expect(createDoc?.riskLevel).toBe('medium');
expect(updateDoc?.riskLevel).toBe('medium');
expect(listComments?.riskLevel).toBe('low');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('get_document', { documentId: 'abc' });
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected when no token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('gdocs');
});
it('connect retrieves token from vault', async () => {
const vault = createMockVault('gdocs', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('gdocs');
});
it('healthCheck returns connected when API responds OK', async () => {
const vault = createMockVault('gdocs', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ user: { displayName: 'Test' } }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('execute(get_document) calls Docs API', async () => {
const vault = createMockVault('gdocs', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
const mockData = { documentId: 'abc', title: 'Test Doc', body: {} };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('get_document', { documentId: 'abc' });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
});
it('execute returns error for unknown action', async () => {
const vault = createMockVault('gdocs', { value: 'ya29.test_token', 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('gdocs');
expect(def.tools).toContain('connector_gdocs_get_document');
expect(def.tools).toContain('connector_gdocs_create_document');
expect(def.actions.length).toBe(connector.actions.length);
});
});
// ─── Google Drive Connector ───
describe('GoogleDriveConnector', () => {
let connector: GoogleDriveConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GoogleDriveConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, and service', () => {
expect(connector.id).toBe('gdrive');
expect(connector.name).toBe('Google Drive');
expect(connector.service).toBe('drive.google.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected actions', () => {
const actionNames = connector.actions.map(a => a.name);
expect(actionNames).toContain('list_files');
expect(actionNames).toContain('search_files');
expect(actionNames).toContain('get_file_metadata');
expect(actionNames).toContain('download_file');
expect(actionNames).toContain('upload_file');
expect(actionNames).toContain('create_folder');
});
it('has at least 6 actions with required fields', () => {
expect(connector.actions.length).toBeGreaterThanOrEqual(6);
for (const action of connector.actions) {
expect(action.name).toBeTruthy();
expect(action.description).toBeTruthy();
expect(action.inputSchema).toBeDefined();
expect(['low', 'medium', 'high']).toContain(action.riskLevel);
}
});
it('has correct risk levels', () => {
const listFiles = connector.actions.find(a => a.name === 'list_files');
const searchFiles = connector.actions.find(a => a.name === 'search_files');
const getMetadata = connector.actions.find(a => a.name === 'get_file_metadata');
const downloadFile = connector.actions.find(a => a.name === 'download_file');
const uploadFile = connector.actions.find(a => a.name === 'upload_file');
const createFolder = connector.actions.find(a => a.name === 'create_folder');
expect(listFiles?.riskLevel).toBe('low');
expect(searchFiles?.riskLevel).toBe('low');
expect(getMetadata?.riskLevel).toBe('low');
expect(downloadFile?.riskLevel).toBe('low');
expect(uploadFile?.riskLevel).toBe('medium');
expect(createFolder?.riskLevel).toBe('medium');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('list_files', {});
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected when no token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('gdrive');
});
it('connect retrieves token from vault', async () => {
const vault = createMockVault('gdrive', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('gdrive');
});
it('healthCheck returns connected when API responds OK', async () => {
const vault = createMockVault('gdrive', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ user: { displayName: 'Test' } }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('execute(list_files) calls Drive API', async () => {
const vault = createMockVault('gdrive', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
const mockData = { files: [{ id: 'f1', name: 'report.pdf' }], nextPageToken: null };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('list_files', { pageSize: 10 });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
});
it('execute returns error for unknown action', async () => {
const vault = createMockVault('gdrive', { value: 'ya29.test_token', 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('gdrive');
expect(def.tools).toContain('connector_gdrive_list_files');
expect(def.tools).toContain('connector_gdrive_upload_file');
expect(def.tools).toContain('connector_gdrive_create_folder');
expect(def.actions.length).toBe(connector.actions.length);
});
});
// ─── Google Sheets Connector ───
describe('GoogleSheetsConnector', () => {
let connector: GoogleSheetsConnector;
let originalFetch: typeof globalThis.fetch;
beforeEach(() => {
connector = new GoogleSheetsConnector();
originalFetch = globalThis.fetch;
});
afterEach(() => {
globalThis.fetch = originalFetch;
});
it('has correct id, name, and service', () => {
expect(connector.id).toBe('gsheets');
expect(connector.name).toBe('Google Sheets');
expect(connector.service).toBe('sheets.google.com');
expect(connector.authType).toBe('bearer');
expect(connector.substrate).toBe('waggle');
});
it('has expected actions', () => {
const actionNames = connector.actions.map(a => a.name);
expect(actionNames).toContain('get_spreadsheet');
expect(actionNames).toContain('get_values');
expect(actionNames).toContain('update_values');
expect(actionNames).toContain('append_values');
expect(actionNames).toContain('create_spreadsheet');
});
it('has at least 5 actions with required fields', () => {
expect(connector.actions.length).toBeGreaterThanOrEqual(5);
for (const action of connector.actions) {
expect(action.name).toBeTruthy();
expect(action.description).toBeTruthy();
expect(action.inputSchema).toBeDefined();
expect(['low', 'medium', 'high']).toContain(action.riskLevel);
}
});
it('has correct risk levels', () => {
const getSpreadsheet = connector.actions.find(a => a.name === 'get_spreadsheet');
const getValues = connector.actions.find(a => a.name === 'get_values');
const updateValues = connector.actions.find(a => a.name === 'update_values');
const appendValues = connector.actions.find(a => a.name === 'append_values');
const createSpreadsheet = connector.actions.find(a => a.name === 'create_spreadsheet');
expect(getSpreadsheet?.riskLevel).toBe('low');
expect(getValues?.riskLevel).toBe('low');
expect(updateValues?.riskLevel).toBe('medium');
expect(appendValues?.riskLevel).toBe('medium');
expect(createSpreadsheet?.riskLevel).toBe('medium');
});
it('execute returns error when not connected', async () => {
const result = await connector.execute('get_spreadsheet', { spreadsheetId: 'abc' });
expect(result.success).toBe(false);
expect(result.error).toContain('Not connected');
});
it('healthCheck returns disconnected when no token', async () => {
const health = await connector.healthCheck();
expect(health.status).toBe('disconnected');
expect(health.id).toBe('gsheets');
});
it('connect retrieves token from vault', async () => {
const vault = createMockVault('gsheets', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
expect(vault.getConnectorCredential).toHaveBeenCalledWith('gsheets');
});
it('healthCheck returns connected when API responds OK', async () => {
const vault = createMockVault('gsheets', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ user: { displayName: 'Test' } }) }) as unknown as typeof fetch;
const health = await connector.healthCheck();
expect(health.status).toBe('connected');
});
it('execute(get_spreadsheet) calls Sheets API', async () => {
const vault = createMockVault('gsheets', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
const mockData = { spreadsheetId: 'abc', properties: { title: 'Budget' }, sheets: [] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('get_spreadsheet', { spreadsheetId: 'abc' });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
});
it('execute(get_values) calls Sheets values API', async () => {
const vault = createMockVault('gsheets', { value: 'ya29.test_token', isExpired: false });
await connector.connect(vault);
const mockData = { range: 'Sheet1!A1:D10', majorDimension: 'ROWS', values: [['a', 'b'], ['c', 'd']] };
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
const result = await connector.execute('get_values', { spreadsheetId: 'abc', range: 'Sheet1!A1:D10' });
expect(result.success).toBe(true);
expect(result.data).toEqual(mockData);
});
it('execute returns error for unknown action', async () => {
const vault = createMockVault('gsheets', { value: 'ya29.test_token', 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('gsheets');
expect(def.tools).toContain('connector_gsheets_get_spreadsheet');
expect(def.tools).toContain('connector_gsheets_update_values');
expect(def.tools).toContain('connector_gsheets_create_spreadsheet');
expect(def.actions.length).toBe(connector.actions.length);
});
});