@auxiora/connectors 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/LICENSE +191 -0
  2. package/dist/auth-manager.d.ts +27 -0
  3. package/dist/auth-manager.d.ts.map +1 -0
  4. package/dist/auth-manager.js +138 -0
  5. package/dist/auth-manager.js.map +1 -0
  6. package/dist/define-connector.d.ts +19 -0
  7. package/dist/define-connector.d.ts.map +1 -0
  8. package/dist/define-connector.js +27 -0
  9. package/dist/define-connector.js.map +1 -0
  10. package/dist/executor.d.ts +19 -0
  11. package/dist/executor.d.ts.map +1 -0
  12. package/dist/executor.js +72 -0
  13. package/dist/executor.js.map +1 -0
  14. package/dist/index.d.ts +7 -0
  15. package/dist/index.d.ts.map +1 -0
  16. package/dist/index.js +6 -0
  17. package/dist/index.js.map +1 -0
  18. package/dist/registry.d.ts +13 -0
  19. package/dist/registry.d.ts.map +1 -0
  20. package/dist/registry.js +33 -0
  21. package/dist/registry.js.map +1 -0
  22. package/dist/trigger-manager.d.ts +19 -0
  23. package/dist/trigger-manager.d.ts.map +1 -0
  24. package/dist/trigger-manager.js +62 -0
  25. package/dist/trigger-manager.js.map +1 -0
  26. package/dist/types.d.ts +101 -0
  27. package/dist/types.d.ts.map +1 -0
  28. package/dist/types.js +2 -0
  29. package/dist/types.js.map +1 -0
  30. package/package.json +26 -0
  31. package/src/auth-manager.ts +177 -0
  32. package/src/define-connector.ts +45 -0
  33. package/src/executor.ts +108 -0
  34. package/src/index.ts +18 -0
  35. package/src/registry.ts +42 -0
  36. package/src/trigger-manager.ts +95 -0
  37. package/src/types.ts +111 -0
  38. package/tests/auth-manager.test.ts +136 -0
  39. package/tests/define-connector.test.ts +61 -0
  40. package/tests/executor.test.ts +122 -0
  41. package/tests/registry.test.ts +101 -0
  42. package/tests/trigger-manager.test.ts +135 -0
  43. package/tsconfig.json +12 -0
  44. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,95 @@
1
+ import type { ConnectorRegistry } from './registry.js';
2
+ import type { AuthManager } from './auth-manager.js';
3
+ import type { TriggerEvent } from './types.js';
4
+
5
+ export type TriggerHandler = (events: TriggerEvent[]) => void | Promise<void>;
6
+
7
+ interface Subscription {
8
+ connectorId: string;
9
+ triggerId: string;
10
+ instanceId: string;
11
+ handler: TriggerHandler;
12
+ lastPollAt: number;
13
+ }
14
+
15
+ export class TriggerManager {
16
+ private registry: ConnectorRegistry;
17
+ private authManager: AuthManager;
18
+ private subscriptions = new Map<string, Subscription>();
19
+
20
+ constructor(registry: ConnectorRegistry, authManager: AuthManager) {
21
+ this.registry = registry;
22
+ this.authManager = authManager;
23
+ }
24
+
25
+ /** Subscribe to a trigger on a connector instance. Returns a subscription ID. */
26
+ subscribe(
27
+ connectorId: string,
28
+ triggerId: string,
29
+ instanceId: string,
30
+ handler: TriggerHandler,
31
+ ): string {
32
+ const connector = this.registry.get(connectorId);
33
+ if (!connector) {
34
+ throw new Error(`Connector "${connectorId}" not found`);
35
+ }
36
+
37
+ const trigger = connector.triggers.find((t) => t.id === triggerId);
38
+ if (!trigger) {
39
+ throw new Error(`Trigger "${triggerId}" not found in connector "${connectorId}"`);
40
+ }
41
+
42
+ const subId = `${connectorId}:${triggerId}:${instanceId}`;
43
+ this.subscriptions.set(subId, {
44
+ connectorId,
45
+ triggerId,
46
+ instanceId,
47
+ handler,
48
+ lastPollAt: Date.now(),
49
+ });
50
+
51
+ return subId;
52
+ }
53
+
54
+ /** Unsubscribe from a trigger. */
55
+ unsubscribe(subscriptionId: string): boolean {
56
+ return this.subscriptions.delete(subscriptionId);
57
+ }
58
+
59
+ /** Poll all subscriptions and invoke handlers for new events. */
60
+ async pollAll(): Promise<TriggerEvent[]> {
61
+ const allEvents: TriggerEvent[] = [];
62
+
63
+ for (const [subId, sub] of this.subscriptions) {
64
+ const connector = this.registry.get(sub.connectorId);
65
+ if (!connector?.pollTrigger) continue;
66
+
67
+ const token = this.authManager.getToken(sub.instanceId);
68
+ if (!token) continue;
69
+
70
+ try {
71
+ const events = await connector.pollTrigger(
72
+ sub.triggerId,
73
+ token.accessToken,
74
+ sub.lastPollAt,
75
+ );
76
+
77
+ if (events.length > 0) {
78
+ await sub.handler(events);
79
+ allEvents.push(...events);
80
+ }
81
+
82
+ sub.lastPollAt = Date.now();
83
+ } catch {
84
+ // Poll errors are silently ignored to avoid breaking other subscriptions
85
+ }
86
+ }
87
+
88
+ return allEvents;
89
+ }
90
+
91
+ /** Get all active subscription IDs. */
92
+ getSubscriptions(): string[] {
93
+ return [...this.subscriptions.keys()];
94
+ }
95
+ }
package/src/types.ts ADDED
@@ -0,0 +1,111 @@
1
+ import type { TrustLevel, TrustDomain } from '@auxiora/autonomy';
2
+
3
+ /** Supported authentication methods for connectors. */
4
+ export type AuthType = 'oauth2' | 'api_key' | 'token';
5
+
6
+ /** OAuth2 configuration for connectors that use OAuth2 auth. */
7
+ export interface OAuth2Config {
8
+ authUrl: string;
9
+ tokenUrl: string;
10
+ scopes: string[];
11
+ clientId?: string;
12
+ clientSecret?: string;
13
+ }
14
+
15
+ /** Authentication configuration for a connector. */
16
+ export interface AuthConfig {
17
+ type: AuthType;
18
+ oauth2?: OAuth2Config;
19
+ /** Human-readable instructions for obtaining credentials. */
20
+ instructions?: string;
21
+ }
22
+
23
+ /** Defines a single action that a connector can perform. */
24
+ export interface ActionDefinition {
25
+ id: string;
26
+ name: string;
27
+ description: string;
28
+ /** Minimum trust level required to execute this action. */
29
+ trustMinimum: TrustLevel;
30
+ /** Trust domain this action operates in. */
31
+ trustDomain: TrustDomain;
32
+ /** Whether this action can be reversed/undone. */
33
+ reversible: boolean;
34
+ /** Whether this action has external side effects. */
35
+ sideEffects: boolean;
36
+ /** Parameter schema (JSON Schema-like). */
37
+ params: Record<string, ParamDefinition>;
38
+ }
39
+
40
+ /** Parameter definition for an action. */
41
+ export interface ParamDefinition {
42
+ type: 'string' | 'number' | 'boolean' | 'object' | 'array';
43
+ description: string;
44
+ required?: boolean;
45
+ default?: unknown;
46
+ }
47
+
48
+ /** Defines a trigger that a connector can listen for. */
49
+ export interface TriggerDefinition {
50
+ id: string;
51
+ name: string;
52
+ description: string;
53
+ /** How this trigger is detected. */
54
+ type: 'poll' | 'webhook';
55
+ /** Polling interval in ms (for poll triggers). */
56
+ pollIntervalMs?: number;
57
+ }
58
+
59
+ /** Defines an entity type that a connector exposes. */
60
+ export interface EntityDefinition {
61
+ id: string;
62
+ name: string;
63
+ description: string;
64
+ /** Fields exposed by this entity. */
65
+ fields: Record<string, string>;
66
+ }
67
+
68
+ /** Full connector definition. */
69
+ export interface Connector {
70
+ id: string;
71
+ name: string;
72
+ description: string;
73
+ version: string;
74
+ category: string;
75
+ icon?: string;
76
+ auth: AuthConfig;
77
+ actions: ActionDefinition[];
78
+ triggers: TriggerDefinition[];
79
+ entities: EntityDefinition[];
80
+ /** Action handler called to execute actions. */
81
+ executeAction: (actionId: string, params: Record<string, unknown>, token: string) => Promise<unknown>;
82
+ /** Trigger poll handler. */
83
+ pollTrigger?: (triggerId: string, token: string, lastPollAt?: number) => Promise<TriggerEvent[]>;
84
+ }
85
+
86
+ /** Event emitted by a trigger. */
87
+ export interface TriggerEvent {
88
+ triggerId: string;
89
+ connectorId: string;
90
+ data: Record<string, unknown>;
91
+ timestamp: number;
92
+ }
93
+
94
+ /** Stored token data for a connector instance. */
95
+ export interface StoredToken {
96
+ accessToken: string;
97
+ refreshToken?: string;
98
+ expiresAt?: number;
99
+ tokenType?: string;
100
+ scopes?: string[];
101
+ }
102
+
103
+ /** A configured instance of a connector with auth credentials. */
104
+ export interface ConnectorInstance {
105
+ id: string;
106
+ connectorId: string;
107
+ label: string;
108
+ createdAt: number;
109
+ /** Whether auth is configured and valid. */
110
+ authenticated: boolean;
111
+ }
@@ -0,0 +1,136 @@
1
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
2
+ import { AuthManager } from '../src/auth-manager.js';
3
+ import type { AuthConfig } from '../src/types.js';
4
+ import type { AuthManagerVault } from '../src/auth-manager.js';
5
+
6
+ function createMockVault(data: Record<string, string> = {}): AuthManagerVault {
7
+ const store = new Map(Object.entries(data));
8
+ return {
9
+ get: (name: string) => store.get(name),
10
+ has: (name: string) => store.has(name),
11
+ add: async (name: string, value: string) => { store.set(name, value); },
12
+ };
13
+ }
14
+
15
+ describe('AuthManager', () => {
16
+ let authManager: AuthManager;
17
+
18
+ beforeEach(() => {
19
+ authManager = new AuthManager();
20
+ });
21
+
22
+ it('should authenticate with api_key', async () => {
23
+ const config: AuthConfig = { type: 'api_key' };
24
+ const token = await authManager.authenticate('inst-1', config, { apiKey: 'sk-123' });
25
+ expect(token.accessToken).toBe('sk-123');
26
+ expect(token.tokenType).toBe('api_key');
27
+ });
28
+
29
+ it('should authenticate with oauth2', async () => {
30
+ const config: AuthConfig = {
31
+ type: 'oauth2',
32
+ oauth2: { authUrl: 'https://auth.example.com', tokenUrl: 'https://token.example.com', scopes: ['read'] },
33
+ };
34
+ const token = await authManager.authenticate('inst-2', config, {
35
+ accessToken: 'at-abc',
36
+ refreshToken: 'rt-xyz',
37
+ });
38
+ expect(token.accessToken).toBe('at-abc');
39
+ expect(token.refreshToken).toBe('rt-xyz');
40
+ expect(token.scopes).toEqual(['read']);
41
+ });
42
+
43
+ it('should authenticate with token', async () => {
44
+ const config: AuthConfig = { type: 'token' };
45
+ const token = await authManager.authenticate('inst-3', config, { token: 'tok-456' });
46
+ expect(token.accessToken).toBe('tok-456');
47
+ expect(token.tokenType).toBe('Bearer');
48
+ });
49
+
50
+ it('should throw on missing api key', async () => {
51
+ const config: AuthConfig = { type: 'api_key' };
52
+ await expect(authManager.authenticate('inst-4', config, {})).rejects.toThrow('apiKey');
53
+ });
54
+
55
+ it('should throw on missing oauth2 access token', async () => {
56
+ const config: AuthConfig = { type: 'oauth2' };
57
+ await expect(authManager.authenticate('inst-5', config, {})).rejects.toThrow('accessToken');
58
+ });
59
+
60
+ it('should throw on missing token', async () => {
61
+ const config: AuthConfig = { type: 'token' };
62
+ await expect(authManager.authenticate('inst-6', config, {})).rejects.toThrow('token');
63
+ });
64
+
65
+ it('should get stored token', async () => {
66
+ const config: AuthConfig = { type: 'api_key' };
67
+ await authManager.authenticate('inst-1', config, { apiKey: 'sk-123' });
68
+ const token = authManager.getToken('inst-1');
69
+ expect(token?.accessToken).toBe('sk-123');
70
+ });
71
+
72
+ it('should return undefined for unknown instance', () => {
73
+ expect(authManager.getToken('unknown')).toBeUndefined();
74
+ });
75
+
76
+ it('should revoke token', async () => {
77
+ const config: AuthConfig = { type: 'api_key' };
78
+ await authManager.authenticate('inst-1', config, { apiKey: 'sk-123' });
79
+ expect(authManager.hasToken('inst-1')).toBe(true);
80
+ await authManager.revokeToken('inst-1');
81
+ expect(authManager.hasToken('inst-1')).toBe(false);
82
+ });
83
+
84
+ it('should detect expired tokens', async () => {
85
+ const config: AuthConfig = { type: 'oauth2' };
86
+ await authManager.authenticate('inst-exp', config, {
87
+ accessToken: 'at-exp',
88
+ expiresAt: String(Date.now() - 1000),
89
+ });
90
+ expect(authManager.isTokenExpired('inst-exp')).toBe(true);
91
+ });
92
+
93
+ it('should refresh oauth2 token', async () => {
94
+ // refreshToken now makes a real HTTP call, so we mock fetch and provide vault credentials
95
+ const vault = createMockVault({
96
+ 'connectors.inst-ref.credentials': JSON.stringify({ clientId: 'cid', clientSecret: 'csecret' }),
97
+ });
98
+ const mgr = new AuthManager(vault);
99
+ const config: AuthConfig = {
100
+ type: 'oauth2',
101
+ oauth2: { authUrl: 'https://auth.example.com', tokenUrl: 'https://token.example.com', scopes: ['read'] },
102
+ };
103
+ await mgr.authenticate('inst-ref', config, {
104
+ accessToken: 'at-old',
105
+ refreshToken: 'rt-xyz',
106
+ });
107
+
108
+ // Mock the global fetch to return a fake token response
109
+ const mockFetch = vi.fn().mockResolvedValue({
110
+ ok: true,
111
+ json: async () => ({ access_token: 'at-new', expires_in: 3600 }),
112
+ });
113
+ const originalFetch = globalThis.fetch;
114
+ globalThis.fetch = mockFetch as any;
115
+ try {
116
+ const refreshed = await mgr.refreshToken('inst-ref', config);
117
+ expect(refreshed.accessToken).toBe('at-new');
118
+ expect(refreshed.expiresAt).toBeGreaterThan(Date.now());
119
+ expect(refreshed.refreshToken).toBe('rt-xyz');
120
+ expect(mockFetch).toHaveBeenCalledOnce();
121
+ } finally {
122
+ globalThis.fetch = originalFetch;
123
+ }
124
+ });
125
+
126
+ it('should throw when refreshing non-oauth2 token', async () => {
127
+ const config: AuthConfig = { type: 'api_key' };
128
+ await authManager.authenticate('inst-api', config, { apiKey: 'key' });
129
+ await expect(authManager.refreshToken('inst-api', config)).rejects.toThrow('OAuth2');
130
+ });
131
+
132
+ it('should throw when refreshing unknown instance', async () => {
133
+ const config: AuthConfig = { type: 'oauth2' };
134
+ await expect(authManager.refreshToken('unknown', config)).rejects.toThrow('No token found');
135
+ });
136
+ });
@@ -0,0 +1,61 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { defineConnector } from '../src/define-connector.js';
3
+
4
+ describe('defineConnector', () => {
5
+ const baseOpts = {
6
+ id: 'my-connector',
7
+ name: 'My Connector',
8
+ description: 'A connector',
9
+ version: '1.0.0',
10
+ category: 'testing',
11
+ auth: { type: 'api_key' as const },
12
+ actions: [
13
+ {
14
+ id: 'action-1',
15
+ name: 'Action 1',
16
+ description: 'Does something',
17
+ trustMinimum: 1 as const,
18
+ trustDomain: 'integrations' as const,
19
+ reversible: false,
20
+ sideEffects: false,
21
+ params: {},
22
+ },
23
+ ],
24
+ executeAction: async () => ({}),
25
+ };
26
+
27
+ it('should create a valid connector', () => {
28
+ const connector = defineConnector(baseOpts);
29
+ expect(connector.id).toBe('my-connector');
30
+ expect(connector.name).toBe('My Connector');
31
+ expect(connector.actions).toHaveLength(1);
32
+ expect(connector.triggers).toEqual([]);
33
+ expect(connector.entities).toEqual([]);
34
+ });
35
+
36
+ it('should include triggers and entities when provided', () => {
37
+ const connector = defineConnector({
38
+ ...baseOpts,
39
+ triggers: [{ id: 't1', name: 'Trigger', description: 'A trigger', type: 'poll' }],
40
+ entities: [{ id: 'e1', name: 'Entity', description: 'An entity', fields: { name: 'string' } }],
41
+ });
42
+ expect(connector.triggers).toHaveLength(1);
43
+ expect(connector.entities).toHaveLength(1);
44
+ });
45
+
46
+ it('should throw if id is missing', () => {
47
+ expect(() => defineConnector({ ...baseOpts, id: '' })).toThrow('id and name are required');
48
+ });
49
+
50
+ it('should throw if name is missing', () => {
51
+ expect(() => defineConnector({ ...baseOpts, name: '' })).toThrow('id and name are required');
52
+ });
53
+
54
+ it('should throw if no actions are defined', () => {
55
+ expect(() => defineConnector({ ...baseOpts, actions: [] })).toThrow('at least one action');
56
+ });
57
+
58
+ it('should throw if auth is missing', () => {
59
+ expect(() => defineConnector({ ...baseOpts, auth: undefined as any })).toThrow('auth config is required');
60
+ });
61
+ });
@@ -0,0 +1,122 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs/promises';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { ConnectorRegistry } from '../src/registry.js';
6
+ import { AuthManager } from '../src/auth-manager.js';
7
+ import { ActionExecutor } from '../src/executor.js';
8
+ import { defineConnector } from '../src/define-connector.js';
9
+ import { TrustEngine, TrustGate, ActionAuditTrail } from '@auxiora/autonomy';
10
+
11
+ describe('ActionExecutor', () => {
12
+ let tmpDir: string;
13
+ let registry: ConnectorRegistry;
14
+ let authManager: AuthManager;
15
+ let trustGate: TrustGate;
16
+ let auditTrail: ActionAuditTrail;
17
+ let executor: ActionExecutor;
18
+
19
+ beforeEach(async () => {
20
+ tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'executor-'));
21
+ const engine = new TrustEngine({ defaultLevel: 2 }, path.join(tmpDir, 'state.json'));
22
+ await engine.load();
23
+
24
+ registry = new ConnectorRegistry();
25
+ authManager = new AuthManager();
26
+ trustGate = new TrustGate(engine);
27
+ auditTrail = new ActionAuditTrail(path.join(tmpDir, 'audit.json'));
28
+ executor = new ActionExecutor(registry, authManager, trustGate, auditTrail);
29
+
30
+ // Register a test connector
31
+ registry.register(
32
+ defineConnector({
33
+ id: 'test',
34
+ name: 'Test',
35
+ description: 'Test connector',
36
+ version: '1.0.0',
37
+ category: 'testing',
38
+ auth: { type: 'api_key' },
39
+ actions: [
40
+ {
41
+ id: 'do-thing',
42
+ name: 'Do Thing',
43
+ description: 'Does a thing',
44
+ trustMinimum: 1,
45
+ trustDomain: 'integrations',
46
+ reversible: true,
47
+ sideEffects: true,
48
+ params: {},
49
+ },
50
+ {
51
+ id: 'high-trust',
52
+ name: 'High Trust Action',
53
+ description: 'Needs high trust',
54
+ trustMinimum: 4,
55
+ trustDomain: 'integrations',
56
+ reversible: false,
57
+ sideEffects: true,
58
+ params: {},
59
+ },
60
+ ],
61
+ executeAction: async (actionId) => {
62
+ if (actionId === 'do-thing') return { result: 'done' };
63
+ throw new Error('Action failed');
64
+ },
65
+ }),
66
+ );
67
+
68
+ // Authenticate instance
69
+ await authManager.authenticate('inst-1', { type: 'api_key' }, { apiKey: 'test-key' });
70
+ });
71
+
72
+ afterEach(async () => {
73
+ await fs.rm(tmpDir, { recursive: true, force: true });
74
+ });
75
+
76
+ it('should execute an action successfully', async () => {
77
+ const result = await executor.execute('test', 'do-thing', {}, 'inst-1');
78
+ expect(result.success).toBe(true);
79
+ expect(result.data).toEqual({ result: 'done' });
80
+ expect(result.auditId).toBeDefined();
81
+ });
82
+
83
+ it('should deny action when trust level is insufficient', async () => {
84
+ const result = await executor.execute('test', 'high-trust', {}, 'inst-1');
85
+ expect(result.success).toBe(false);
86
+ expect(result.error).toContain('denied');
87
+ });
88
+
89
+ it('should fail for unknown connector', async () => {
90
+ const result = await executor.execute('unknown', 'action', {}, 'inst-1');
91
+ expect(result.success).toBe(false);
92
+ expect(result.error).toContain('not found');
93
+ });
94
+
95
+ it('should fail for unknown action', async () => {
96
+ const result = await executor.execute('test', 'unknown', {}, 'inst-1');
97
+ expect(result.success).toBe(false);
98
+ expect(result.error).toContain('not found');
99
+ });
100
+
101
+ it('should fail when no auth token exists', async () => {
102
+ const result = await executor.execute('test', 'do-thing', {}, 'no-token');
103
+ expect(result.success).toBe(false);
104
+ expect(result.error).toContain('authentication token');
105
+ });
106
+
107
+ it('should record audit entry on success', async () => {
108
+ const result = await executor.execute('test', 'do-thing', {}, 'inst-1');
109
+ const entry = auditTrail.getById(result.auditId!);
110
+ expect(entry).toBeDefined();
111
+ expect(entry!.outcome).toBe('success');
112
+ expect(entry!.executed).toBe(true);
113
+ });
114
+
115
+ it('should record audit entry on trust denial', async () => {
116
+ const result = await executor.execute('test', 'high-trust', {}, 'inst-1');
117
+ const entry = auditTrail.getById(result.auditId!);
118
+ expect(entry).toBeDefined();
119
+ expect(entry!.outcome).toBe('failure');
120
+ expect(entry!.executed).toBe(false);
121
+ });
122
+ });
@@ -0,0 +1,101 @@
1
+ import { describe, it, expect, beforeEach } from 'vitest';
2
+ import { ConnectorRegistry } from '../src/registry.js';
3
+ import { defineConnector } from '../src/define-connector.js';
4
+ import type { Connector } from '../src/types.js';
5
+
6
+ function makeConnector(overrides: Partial<Connector> = {}): Connector {
7
+ return defineConnector({
8
+ id: overrides.id ?? 'test-connector',
9
+ name: overrides.name ?? 'Test Connector',
10
+ description: 'A test connector',
11
+ version: '1.0.0',
12
+ category: overrides.category ?? 'testing',
13
+ auth: { type: 'api_key' },
14
+ actions: [
15
+ {
16
+ id: 'test-action',
17
+ name: 'Test Action',
18
+ description: 'Does a test thing',
19
+ trustMinimum: 1,
20
+ trustDomain: 'integrations',
21
+ reversible: false,
22
+ sideEffects: false,
23
+ params: {},
24
+ },
25
+ ],
26
+ triggers: [
27
+ {
28
+ id: 'test-trigger',
29
+ name: 'Test Trigger',
30
+ description: 'A test trigger',
31
+ type: 'poll',
32
+ pollIntervalMs: 60000,
33
+ },
34
+ ],
35
+ executeAction: async () => ({ ok: true }),
36
+ });
37
+ }
38
+
39
+ describe('ConnectorRegistry', () => {
40
+ let registry: ConnectorRegistry;
41
+
42
+ beforeEach(() => {
43
+ registry = new ConnectorRegistry();
44
+ });
45
+
46
+ it('should register and retrieve a connector', () => {
47
+ const connector = makeConnector();
48
+ registry.register(connector);
49
+ expect(registry.get('test-connector')).toBe(connector);
50
+ });
51
+
52
+ it('should throw when registering a duplicate', () => {
53
+ const connector = makeConnector();
54
+ registry.register(connector);
55
+ expect(() => registry.register(connector)).toThrow('already registered');
56
+ });
57
+
58
+ it('should list all connectors', () => {
59
+ registry.register(makeConnector({ id: 'a', name: 'A' }));
60
+ registry.register(makeConnector({ id: 'b', name: 'B' }));
61
+ expect(registry.list()).toHaveLength(2);
62
+ });
63
+
64
+ it('should list connectors by category', () => {
65
+ registry.register(makeConnector({ id: 'a', category: 'productivity' }));
66
+ registry.register(makeConnector({ id: 'b', category: 'devtools' }));
67
+ registry.register(makeConnector({ id: 'c', category: 'productivity' }));
68
+ expect(registry.listByCategory('productivity')).toHaveLength(2);
69
+ expect(registry.listByCategory('devtools')).toHaveLength(1);
70
+ expect(registry.listByCategory('unknown')).toHaveLength(0);
71
+ });
72
+
73
+ it('should get actions for a connector', () => {
74
+ registry.register(makeConnector());
75
+ const actions = registry.getActions('test-connector');
76
+ expect(actions).toHaveLength(1);
77
+ expect(actions[0].id).toBe('test-action');
78
+ });
79
+
80
+ it('should return empty array for unknown connector actions', () => {
81
+ expect(registry.getActions('unknown')).toEqual([]);
82
+ });
83
+
84
+ it('should get triggers for a connector', () => {
85
+ registry.register(makeConnector());
86
+ const triggers = registry.getTriggers('test-connector');
87
+ expect(triggers).toHaveLength(1);
88
+ expect(triggers[0].id).toBe('test-trigger');
89
+ });
90
+
91
+ it('should unregister a connector', () => {
92
+ registry.register(makeConnector());
93
+ expect(registry.has('test-connector')).toBe(true);
94
+ registry.unregister('test-connector');
95
+ expect(registry.has('test-connector')).toBe(false);
96
+ });
97
+
98
+ it('should return undefined for unknown connector', () => {
99
+ expect(registry.get('nonexistent')).toBeUndefined();
100
+ });
101
+ });