664 lines
25 KiB
TypeScript
664 lines
25 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { LinearConnector } from '../../src/connectors/linear-connector.js';
|
|
import { AsanaConnector } from '../../src/connectors/asana-connector.js';
|
|
import { TrelloConnector } from '../../src/connectors/trello-connector.js';
|
|
import { MondayConnector } from '../../src/connectors/monday-connector.js';
|
|
import type { VaultStore } from '@waggle/core';
|
|
|
|
function createMockVault(connectorId: string, cred?: { value: string; isExpired: boolean }, extras?: Record<string, string>): VaultStore {
|
|
return {
|
|
getConnectorCredential: vi.fn((id: string) => {
|
|
if (id === connectorId && cred) return { ...cred, type: 'bearer' };
|
|
return null;
|
|
}),
|
|
get: vi.fn((key: string) => {
|
|
if (extras && extras[key]) return { value: extras[key] };
|
|
return 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;
|
|
}
|
|
|
|
// ─── Linear Connector ───
|
|
|
|
describe('LinearConnector', () => {
|
|
let connector: LinearConnector;
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
connector = new LinearConnector();
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('has correct id, name, and service', () => {
|
|
expect(connector.id).toBe('linear');
|
|
expect(connector.name).toBe('Linear');
|
|
expect(connector.service).toBe('linear.app');
|
|
expect(connector.authType).toBe('bearer');
|
|
expect(connector.substrate).toBe('waggle');
|
|
});
|
|
|
|
it('has at least 3 actions with required fields', () => {
|
|
expect(connector.actions.length).toBeGreaterThanOrEqual(3);
|
|
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 for actions', () => {
|
|
const listIssues = connector.actions.find(a => a.name === 'list_issues');
|
|
const createIssue = connector.actions.find(a => a.name === 'create_issue');
|
|
const searchIssues = connector.actions.find(a => a.name === 'search_issues');
|
|
expect(listIssues?.riskLevel).toBe('low');
|
|
expect(createIssue?.riskLevel).toBe('medium');
|
|
expect(searchIssues?.riskLevel).toBe('low');
|
|
});
|
|
|
|
it('execute returns error when not connected', async () => {
|
|
const result = await connector.execute('list_issues', {});
|
|
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('linear');
|
|
});
|
|
|
|
it('connect retrieves token from vault', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
expect(vault.getConnectorCredential).toHaveBeenCalledWith('linear');
|
|
});
|
|
|
|
it('healthCheck returns connected when API responds OK', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: { viewer: { id: '1', name: 'User' } } }) }) as unknown as typeof fetch;
|
|
|
|
const health = await connector.healthCheck();
|
|
expect(health.status).toBe('connected');
|
|
});
|
|
|
|
it('execute(list_issues) calls GraphQL API', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
const mockData = { data: { issues: { nodes: [{ id: '1', title: 'Test' }] } } };
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
|
|
|
const result = await connector.execute('list_issues', {});
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual(mockData.data);
|
|
});
|
|
|
|
it('binds list_issues filters as GraphQL variables', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
const teamInjection = '__WAGGLE_TEAM__" } }) { viewer { id } } #';
|
|
const stateInjection = '__WAGGLE_STATE__" } }) { viewer { name } } #';
|
|
const firstInjection = '__WAGGLE_FIRST__) { viewer { id } } #';
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ data: { issues: { nodes: [] } } }),
|
|
});
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
|
|
const result = await connector.execute('list_issues', {
|
|
teamId: teamInjection,
|
|
state: stateInjection,
|
|
first: firstInjection,
|
|
});
|
|
|
|
expect(result.success).toBe(true);
|
|
const body = JSON.parse((fetchMock.mock.calls[0][1] as RequestInit).body as string);
|
|
expect(body.query).not.toContain('__WAGGLE_');
|
|
expect(body.variables).toEqual({
|
|
first: firstInjection,
|
|
filter: {
|
|
team: { id: { eq: teamInjection } },
|
|
state: { name: { eq: stateInjection } },
|
|
},
|
|
});
|
|
});
|
|
|
|
it('binds list limits and preserves default issue filters', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
const fetchMock = vi.fn().mockResolvedValue({
|
|
ok: true,
|
|
json: async () => ({ data: {} }),
|
|
});
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
|
|
const calls: Array<[string, Record<string, unknown>, Record<string, unknown>]> = [
|
|
['list_issues', {}, { first: 50 }],
|
|
['list_projects', {}, { first: 50 }],
|
|
['list_teams', {}, { first: 50 }],
|
|
['list_projects', { first: '__WAGGLE_PROJECT_FIRST__' }, { first: '__WAGGLE_PROJECT_FIRST__' }],
|
|
['list_teams', { first: '__WAGGLE_TEAM_FIRST__' }, { first: '__WAGGLE_TEAM_FIRST__' }],
|
|
];
|
|
|
|
for (const [action, params, expectedVariables] of calls) {
|
|
const callIndex = fetchMock.mock.calls.length;
|
|
const result = await connector.execute(action, params);
|
|
expect(result.success).toBe(true);
|
|
const body = JSON.parse((fetchMock.mock.calls[callIndex][1] as RequestInit).body as string);
|
|
expect(body.query).not.toContain('__WAGGLE_');
|
|
expect(body.variables).toEqual(expectedVariables);
|
|
if (action === 'list_issues') expect(body.query).not.toContain('$filter');
|
|
}
|
|
});
|
|
|
|
it('execute returns error for unknown action', async () => {
|
|
const vault = createMockVault('linear', { value: 'lin_api_test123', 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('linear');
|
|
expect(def.tools).toContain('connector_linear_list_issues');
|
|
expect(def.tools).toContain('connector_linear_create_issue');
|
|
expect(def.actions.length).toBe(connector.actions.length);
|
|
});
|
|
});
|
|
|
|
// ─── Asana Connector ───
|
|
|
|
describe('AsanaConnector', () => {
|
|
let connector: AsanaConnector;
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
connector = new AsanaConnector();
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('has correct id, name, and service', () => {
|
|
expect(connector.id).toBe('asana');
|
|
expect(connector.name).toBe('Asana');
|
|
expect(connector.service).toBe('asana.com');
|
|
expect(connector.authType).toBe('bearer');
|
|
expect(connector.substrate).toBe('waggle');
|
|
});
|
|
|
|
it('has at least 3 actions with required fields', () => {
|
|
expect(connector.actions.length).toBeGreaterThanOrEqual(3);
|
|
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 for actions', () => {
|
|
const listTasks = connector.actions.find(a => a.name === 'list_tasks');
|
|
const createTask = connector.actions.find(a => a.name === 'create_task');
|
|
const searchTasks = connector.actions.find(a => a.name === 'search_tasks');
|
|
expect(listTasks?.riskLevel).toBe('low');
|
|
expect(createTask?.riskLevel).toBe('medium');
|
|
expect(searchTasks?.riskLevel).toBe('low');
|
|
});
|
|
|
|
it('execute returns error when not connected', async () => {
|
|
const result = await connector.execute('list_tasks', {});
|
|
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('asana');
|
|
});
|
|
|
|
it('connect retrieves token from vault', async () => {
|
|
const vault = createMockVault('asana', { value: 'asana_pat_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
expect(vault.getConnectorCredential).toHaveBeenCalledWith('asana');
|
|
});
|
|
|
|
it('healthCheck returns connected when API responds OK', async () => {
|
|
const vault = createMockVault('asana', { value: 'asana_pat_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: { gid: '1', name: 'User' } }) }) as unknown as typeof fetch;
|
|
|
|
const health = await connector.healthCheck();
|
|
expect(health.status).toBe('connected');
|
|
});
|
|
|
|
it('execute(list_tasks) calls REST API', async () => {
|
|
const vault = createMockVault('asana', { value: 'asana_pat_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
const mockData = { data: [{ gid: '1', name: 'Task 1' }] };
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
|
|
|
const result = await connector.execute('list_tasks', { project: 'proj123' });
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual(mockData);
|
|
});
|
|
|
|
it('execute returns error for unknown action', async () => {
|
|
const vault = createMockVault('asana', { value: 'asana_pat_test123', 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('asana');
|
|
expect(def.tools).toContain('connector_asana_list_tasks');
|
|
expect(def.tools).toContain('connector_asana_create_task');
|
|
expect(def.actions.length).toBe(connector.actions.length);
|
|
});
|
|
});
|
|
|
|
// ─── Trello Connector ───
|
|
|
|
describe('TrelloConnector', () => {
|
|
let connector: TrelloConnector;
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
connector = new TrelloConnector();
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('has correct id, name, and service', () => {
|
|
expect(connector.id).toBe('trello');
|
|
expect(connector.name).toBe('Trello');
|
|
expect(connector.service).toBe('trello.com');
|
|
expect(connector.authType).toBe('api_key');
|
|
expect(connector.substrate).toBe('waggle');
|
|
});
|
|
|
|
it('has at least 3 actions with required fields', () => {
|
|
expect(connector.actions.length).toBeGreaterThanOrEqual(3);
|
|
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 for actions', () => {
|
|
const listBoards = connector.actions.find(a => a.name === 'list_boards');
|
|
const createCard = connector.actions.find(a => a.name === 'create_card');
|
|
const searchCards = connector.actions.find(a => a.name === 'search_cards');
|
|
expect(listBoards?.riskLevel).toBe('low');
|
|
expect(createCard?.riskLevel).toBe('medium');
|
|
expect(searchCards?.riskLevel).toBe('low');
|
|
});
|
|
|
|
it('execute returns error when not connected', async () => {
|
|
const result = await connector.execute('list_boards', {});
|
|
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('trello');
|
|
});
|
|
|
|
it('connect retrieves credentials from vault', async () => {
|
|
const vault = createMockVault('trello', { value: 'trello_token_test', isExpired: false }, {
|
|
'connector:trello:api_key': 'trello_key_test',
|
|
});
|
|
await connector.connect(vault);
|
|
expect(vault.getConnectorCredential).toHaveBeenCalledWith('trello');
|
|
expect(vault.get).toHaveBeenCalledWith('connector:trello:api_key');
|
|
});
|
|
|
|
it('healthCheck returns connected when API responds OK', async () => {
|
|
const vault = createMockVault('trello', { value: 'trello_token_test', isExpired: false }, {
|
|
'connector:trello:api_key': 'trello_key_test',
|
|
});
|
|
await connector.connect(vault);
|
|
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ id: '1', username: 'user' }) }) as unknown as typeof fetch;
|
|
|
|
const health = await connector.healthCheck();
|
|
expect(health.status).toBe('connected');
|
|
});
|
|
|
|
it('execute(list_boards) calls REST API with auth params', async () => {
|
|
const vault = createMockVault('trello', { value: 'trello_token_test', isExpired: false }, {
|
|
'connector:trello:api_key': 'trello_key_test',
|
|
});
|
|
await connector.connect(vault);
|
|
|
|
const mockBoards = [{ id: '1', name: 'My Board' }];
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockBoards }) as unknown as typeof fetch;
|
|
|
|
const result = await connector.execute('list_boards', {});
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual(mockBoards);
|
|
|
|
// Verify auth params are in the URL
|
|
const fetchCall = vi.mocked(globalThis.fetch).mock.calls[0];
|
|
expect(fetchCall[0]).toContain('key=trello_key_test');
|
|
expect(fetchCall[0]).toContain('token=trello_token_test');
|
|
});
|
|
|
|
it('execute returns error for unknown action', async () => {
|
|
const vault = createMockVault('trello', { value: 'trello_token_test', isExpired: false }, {
|
|
'connector:trello:api_key': 'trello_key_test',
|
|
});
|
|
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('trello');
|
|
expect(def.tools).toContain('connector_trello_list_boards');
|
|
expect(def.tools).toContain('connector_trello_create_card');
|
|
expect(def.actions.length).toBe(connector.actions.length);
|
|
});
|
|
});
|
|
|
|
// ─── Monday.com Connector ───
|
|
|
|
describe('MondayConnector', () => {
|
|
let connector: MondayConnector;
|
|
let originalFetch: typeof globalThis.fetch;
|
|
|
|
beforeEach(() => {
|
|
connector = new MondayConnector();
|
|
originalFetch = globalThis.fetch;
|
|
});
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch;
|
|
});
|
|
|
|
it('has correct id, name, and service', () => {
|
|
expect(connector.id).toBe('monday');
|
|
expect(connector.name).toBe('Monday.com');
|
|
expect(connector.service).toBe('monday.com');
|
|
expect(connector.authType).toBe('bearer');
|
|
expect(connector.substrate).toBe('waggle');
|
|
});
|
|
|
|
it('has at least 3 actions with required fields', () => {
|
|
expect(connector.actions.length).toBeGreaterThanOrEqual(3);
|
|
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 for actions', () => {
|
|
const listBoards = connector.actions.find(a => a.name === 'list_boards');
|
|
const createItem = connector.actions.find(a => a.name === 'create_item');
|
|
const searchItems = connector.actions.find(a => a.name === 'search_items');
|
|
expect(listBoards?.riskLevel).toBe('low');
|
|
expect(createItem?.riskLevel).toBe('medium');
|
|
expect(searchItems?.riskLevel).toBe('low');
|
|
});
|
|
|
|
it('execute returns error when not connected', async () => {
|
|
const result = await connector.execute('list_boards', {});
|
|
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('monday');
|
|
});
|
|
|
|
it('connect retrieves token from vault', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
expect(vault.getConnectorCredential).toHaveBeenCalledWith('monday');
|
|
});
|
|
|
|
it('healthCheck returns connected when API responds OK', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: { me: { id: '1', name: 'User' } } }) }) as unknown as typeof fetch;
|
|
|
|
const health = await connector.healthCheck();
|
|
expect(health.status).toBe('connected');
|
|
});
|
|
|
|
it('execute(list_boards) calls GraphQL API', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
|
|
const mockData = { data: { boards: [{ id: '1', name: 'Sprint Board' }] } };
|
|
globalThis.fetch = vi.fn().mockResolvedValue({ ok: true, json: async () => mockData }) as unknown as typeof fetch;
|
|
|
|
const result = await connector.execute('list_boards', {});
|
|
expect(result.success).toBe(true);
|
|
expect(result.data).toEqual(mockData.data);
|
|
});
|
|
|
|
it('binds every action value as an exact GraphQL variable', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: {} }) });
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
const cases: Array<[
|
|
string,
|
|
Record<string, unknown>,
|
|
Record<string, unknown>,
|
|
]> = [
|
|
[
|
|
'list_boards',
|
|
{
|
|
limit: '__WAGGLE_BOARD_LIMIT__) { users { id } } #',
|
|
page: '__WAGGLE_BOARD_PAGE__) { users { email } } #',
|
|
board_kind: 'private',
|
|
},
|
|
{
|
|
limit: '__WAGGLE_BOARD_LIMIT__) { users { id } } #',
|
|
page: '__WAGGLE_BOARD_PAGE__) { users { email } } #',
|
|
boardKind: 'private',
|
|
},
|
|
],
|
|
[
|
|
'list_items',
|
|
{
|
|
boardId: '__WAGGLE_BOARD_ID__]) { users { id } } #',
|
|
groupId: '__WAGGLE_GROUP_ID__"]) { users { email } } #',
|
|
limit: '__WAGGLE_ITEM_LIMIT__) { users { id } } #',
|
|
},
|
|
{
|
|
boardId: '__WAGGLE_BOARD_ID__]) { users { id } } #',
|
|
groupId: '__WAGGLE_GROUP_ID__"]) { users { email } } #',
|
|
limit: '__WAGGLE_ITEM_LIMIT__) { users { id } } #',
|
|
},
|
|
],
|
|
[
|
|
'create_item',
|
|
{
|
|
boardId: '__WAGGLE_CREATE_BOARD__',
|
|
itemName: '__WAGGLE_ITEM_NAME__") { users { id } } #',
|
|
groupId: '__WAGGLE_CREATE_GROUP__") { users { email } } #',
|
|
columnValues: '__WAGGLE_CREATE_COLUMNS__") { users { id } } #',
|
|
},
|
|
{
|
|
boardId: '__WAGGLE_CREATE_BOARD__',
|
|
itemName: '__WAGGLE_ITEM_NAME__") { users { id } } #',
|
|
groupId: '__WAGGLE_CREATE_GROUP__") { users { email } } #',
|
|
columnValues: '__WAGGLE_CREATE_COLUMNS__") { users { id } } #',
|
|
},
|
|
],
|
|
[
|
|
'update_item',
|
|
{
|
|
boardId: '__WAGGLE_UPDATE_BOARD__',
|
|
itemId: '__WAGGLE_UPDATE_ITEM__',
|
|
columnValues: '__WAGGLE_UPDATE_COLUMNS__") { users { id } } #',
|
|
},
|
|
{
|
|
boardId: '__WAGGLE_UPDATE_BOARD__',
|
|
itemId: '__WAGGLE_UPDATE_ITEM__',
|
|
columnValues: '__WAGGLE_UPDATE_COLUMNS__") { users { id } } #',
|
|
},
|
|
],
|
|
[
|
|
'search_items',
|
|
{
|
|
query: '__WAGGLE_SEARCH_QUERY__"]) { users { email } } #',
|
|
limit: '__WAGGLE_SEARCH_LIMIT__) { users { id } } #',
|
|
},
|
|
{
|
|
query: '__WAGGLE_SEARCH_QUERY__"]) { users { email } } #',
|
|
limit: '__WAGGLE_SEARCH_LIMIT__) { users { id } } #',
|
|
},
|
|
],
|
|
];
|
|
|
|
for (const [action, params, expectedVariables] of cases) {
|
|
const callIndex = fetchMock.mock.calls.length;
|
|
const result = await connector.execute(action, params);
|
|
expect(result.success).toBe(true);
|
|
const body = JSON.parse((fetchMock.mock.calls[callIndex][1] as RequestInit).body as string);
|
|
expect(body.query).not.toContain('__WAGGLE_');
|
|
expect(body.query).not.toContain('private');
|
|
expect(body.variables).toEqual(expectedVariables);
|
|
}
|
|
});
|
|
|
|
it('preserves defaults and optional Monday action branches', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
const fetchMock = vi.fn().mockResolvedValue({ ok: true, json: async () => ({ data: {} }) });
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
const cases: Array<[
|
|
string,
|
|
Record<string, unknown>,
|
|
Record<string, unknown>,
|
|
string[],
|
|
]> = [
|
|
['list_boards', {}, { limit: 25, page: 1 }, ['$boardKind']],
|
|
[
|
|
'list_items',
|
|
{ boardId: '__WAGGLE_NO_GROUP_BOARD__' },
|
|
{ boardId: '__WAGGLE_NO_GROUP_BOARD__', limit: 50 },
|
|
['$groupId'],
|
|
],
|
|
[
|
|
'create_item',
|
|
{ boardId: '__WAGGLE_REQUIRED_BOARD__', itemName: '__WAGGLE_REQUIRED_NAME__' },
|
|
{ boardId: '__WAGGLE_REQUIRED_BOARD__', itemName: '__WAGGLE_REQUIRED_NAME__' },
|
|
['$groupId', '$columnValues'],
|
|
],
|
|
[
|
|
'search_items',
|
|
{ query: '__WAGGLE_DEFAULT_SEARCH__' },
|
|
{ limit: 25, query: '__WAGGLE_DEFAULT_SEARCH__' },
|
|
[],
|
|
],
|
|
];
|
|
|
|
for (const [action, params, expectedVariables, omittedDefinitions] of cases) {
|
|
const callIndex = fetchMock.mock.calls.length;
|
|
const result = await connector.execute(action, params);
|
|
expect(result.success).toBe(true);
|
|
const body = JSON.parse((fetchMock.mock.calls[callIndex][1] as RequestInit).body as string);
|
|
expect(body.query).not.toContain('__WAGGLE_');
|
|
expect(body.variables).toEqual(expectedVariables);
|
|
for (const omitted of omittedDefinitions) expect(body.query).not.toContain(omitted);
|
|
if (action === 'list_boards') {
|
|
expect(body.query).toContain('limit: $limit, page: $page');
|
|
expect(body.query).not.toContain('limit: 25');
|
|
expect(body.query).not.toContain('page: 1');
|
|
}
|
|
}
|
|
});
|
|
|
|
it('binds valid board kinds and rejects all other values before issuing GraphQL', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', isExpired: false });
|
|
await connector.connect(vault);
|
|
const fetchMock = vi.fn();
|
|
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
|
|
for (const boardKind of ['public', 'private', 'share']) {
|
|
fetchMock.mockResolvedValueOnce({ ok: true, json: async () => ({ data: {} }) });
|
|
const callIndex = fetchMock.mock.calls.length;
|
|
const result = await connector.execute('list_boards', { board_kind: boardKind });
|
|
expect(result.success).toBe(true);
|
|
const body = JSON.parse((fetchMock.mock.calls[callIndex][1] as RequestInit).body as string);
|
|
expect(body.query).not.toContain(boardKind);
|
|
expect(body.variables).toEqual({ limit: 25, page: 1, boardKind });
|
|
}
|
|
|
|
for (const invalidKind of [
|
|
'private) { users { id email } } #',
|
|
'workspace',
|
|
42,
|
|
null,
|
|
{ toString: 1 },
|
|
]) {
|
|
const result = await connector.execute('list_boards', { board_kind: invalidKind });
|
|
expect(result.success).toBe(false);
|
|
expect(result.error).toContain('Invalid board_kind');
|
|
}
|
|
expect(fetchMock).toHaveBeenCalledTimes(3);
|
|
});
|
|
|
|
it('execute returns error for unknown action', async () => {
|
|
const vault = createMockVault('monday', { value: 'monday_api_test123', 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('monday');
|
|
expect(def.tools).toContain('connector_monday_list_boards');
|
|
expect(def.tools).toContain('connector_monday_create_item');
|
|
expect(def.actions.length).toBe(connector.actions.length);
|
|
});
|
|
});
|