import { describe, it, expect, vi } from 'vitest'; import { ConnectorRegistry } from '@waggle/agent'; import { BaseConnector, type ConnectorAction, type ConnectorResult } from '@waggle/agent'; import type { VaultStore } from '@waggle/core'; import type { ConnectorHealth } from '@waggle/shared'; import { needsConfirmation, getApprovalClass } from '@waggle/agent'; // ─── Mock Connector for integration tests ───────────────────────────── class TestConnector extends BaseConnector { readonly id = 'test'; readonly name = 'Test Service'; readonly description = 'Integration test connector'; readonly service = 'test.example.com'; readonly authType = 'bearer' as const; readonly substrate = 'waggle' as const; readonly actions: ConnectorAction[] = [ { name: 'read_data', description: 'Read test data', inputSchema: { properties: { query: { type: 'string' } } }, riskLevel: 'low', }, { name: 'create_item', description: 'Create a test item', inputSchema: { properties: { name: { type: 'string' } }, required: ['name'] }, riskLevel: 'medium', }, ]; async connect(): Promise { /* no-op */ } async healthCheck(): Promise { return { id: this.id, name: this.name, status: 'connected', lastChecked: new Date().toISOString() }; } async execute(action: string, params: Record): Promise { return { success: true, data: { action, ...params } }; } } function createMockVault(credentials: Record = {}): VaultStore { return { getConnectorCredential: vi.fn((id: string) => { const cred = credentials[id]; if (!cred) return null; return { value: cred.value, type: 'bearer', isExpired: cred.isExpired }; }), setConnectorCredential: vi.fn(), set: vi.fn(), get: vi.fn(), delete: vi.fn(), list: vi.fn(() => []), has: vi.fn(() => false), migrateFromConfig: vi.fn(() => 0), } as unknown as VaultStore; } // ─── Integration Tests ─────────────────────────────────────────────── describe('ConnectorRegistry integration', () => { it('registry getDefinitions() returns correct status from vault', async () => { const vault = createMockVault({ test: { value: 'tok', isExpired: false } }); const registry = new ConnectorRegistry(vault); registry.register(new TestConnector()); expect(await registry.hydrate('test')).toBe(true); const defs = registry.getDefinitions(); expect(defs).toHaveLength(1); expect(defs[0].id).toBe('test'); expect(defs[0].status).toBe('connected'); expect(defs[0].tools).toEqual(['connector_test_read_data', 'connector_test_create_item']); }); it('connector tools are included in generated tools when connected', async () => { const vault = createMockVault({ test: { value: 'tok', isExpired: false } }); const registry = new ConnectorRegistry(vault); registry.register(new TestConnector()); expect(await registry.hydrate('test')).toBe(true); const tools = registry.generateTools(); expect(tools).toHaveLength(2); expect(tools.map(t => t.name)).toEqual(['connector_test_read_data', 'connector_test_create_item']); }); it('no connector tools generated when disconnected', async () => { const vault = createMockVault(); // no credentials const registry = new ConnectorRegistry(vault); registry.register(new TestConnector()); expect(await registry.hydrate('test')).toBe(true); expect(registry.generateTools()).toEqual([]); }); it('health check delegates to connector', async () => { const vault = createMockVault(); const registry = new ConnectorRegistry(vault); registry.register(new TestConnector()); const health = await registry.healthCheck('test'); expect(health).not.toBeNull(); expect(health!.status).toBe('connected'); }); }); describe('Connector confirmation gates', () => { it('connector read tools do not need confirmation', () => { expect(needsConfirmation('connector_test_read_data')).toBe(false); }); it('connector write tools need confirmation by action name pattern', () => { expect(needsConfirmation('connector_github_create_issue')).toBe(true); expect(needsConfirmation('connector_slack_send_message')).toBe(true); expect(needsConfirmation('connector_jira_update_issue')).toBe(true); expect(needsConfirmation('connector_jira_delete_issue')).toBe(true); expect(needsConfirmation('connector_jira_transition_issue')).toBe(true); }); it('email send tools get critical approval class (by tool name, not args)', () => { // Security: approval class is determined by tool name, not LLM-provided metadata expect(getApprovalClass('connector_email_send_email')).toBe('critical'); expect(getApprovalClass('connector_email_send_template')).toBe('critical'); }); it('connector write tools get elevated approval class (by tool name)', () => { expect(getApprovalClass('connector_github_create_issue')).toBe('elevated'); expect(getApprovalClass('connector_jira_update_issue')).toBe('elevated'); }); it('connector read tools get standard approval class', () => { expect(getApprovalClass('connector_github_list_repos')).toBe('standard'); }); });