@legible-sync/example-eda 1.2.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 (118) hide show
  1. package/.eslintrc.js +16 -0
  2. package/README.md +103 -0
  3. package/__tests__/integration/ecommerce-flow.test.ts +247 -0
  4. package/__tests__/unit/EventBus.test.ts +154 -0
  5. package/__tests__/unit/PluginManager.test.ts +111 -0
  6. package/__tests__/unit/concepts/User.test.ts +130 -0
  7. package/dist/core/EventBus.d.ts +16 -0
  8. package/dist/core/EventBus.d.ts.map +1 -0
  9. package/dist/core/EventBus.js +44 -0
  10. package/dist/core/EventBus.js.map +1 -0
  11. package/dist/core/PluginManager.d.ts +16 -0
  12. package/dist/core/PluginManager.d.ts.map +1 -0
  13. package/dist/core/PluginManager.js +37 -0
  14. package/dist/core/PluginManager.js.map +1 -0
  15. package/dist/index.d.ts +2 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +145 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/plugins/analytics/concepts/Analytics.d.ts +3 -0
  20. package/dist/plugins/analytics/concepts/Analytics.d.ts.map +1 -0
  21. package/dist/plugins/analytics/concepts/Analytics.js +43 -0
  22. package/dist/plugins/analytics/concepts/Analytics.js.map +1 -0
  23. package/dist/plugins/analytics/index.d.ts +3 -0
  24. package/dist/plugins/analytics/index.d.ts.map +1 -0
  25. package/dist/plugins/analytics/index.js +16 -0
  26. package/dist/plugins/analytics/index.js.map +1 -0
  27. package/dist/plugins/analytics/syncs/analytics-events.sync.d.ts +3 -0
  28. package/dist/plugins/analytics/syncs/analytics-events.sync.d.ts.map +1 -0
  29. package/dist/plugins/analytics/syncs/analytics-events.sync.js +101 -0
  30. package/dist/plugins/analytics/syncs/analytics-events.sync.js.map +1 -0
  31. package/dist/plugins/inventory/concepts/Inventory.d.ts +3 -0
  32. package/dist/plugins/inventory/concepts/Inventory.d.ts.map +1 -0
  33. package/dist/plugins/inventory/concepts/Inventory.js +60 -0
  34. package/dist/plugins/inventory/concepts/Inventory.js.map +1 -0
  35. package/dist/plugins/inventory/index.d.ts +3 -0
  36. package/dist/plugins/inventory/index.d.ts.map +1 -0
  37. package/dist/plugins/inventory/index.js +15 -0
  38. package/dist/plugins/inventory/index.js.map +1 -0
  39. package/dist/plugins/notifications/concepts/Notification.d.ts +3 -0
  40. package/dist/plugins/notifications/concepts/Notification.d.ts.map +1 -0
  41. package/dist/plugins/notifications/concepts/Notification.js +42 -0
  42. package/dist/plugins/notifications/concepts/Notification.js.map +1 -0
  43. package/dist/plugins/notifications/index.d.ts +3 -0
  44. package/dist/plugins/notifications/index.d.ts.map +1 -0
  45. package/dist/plugins/notifications/index.js +15 -0
  46. package/dist/plugins/notifications/index.js.map +1 -0
  47. package/dist/plugins/orders/concepts/Order.d.ts +3 -0
  48. package/dist/plugins/orders/concepts/Order.d.ts.map +1 -0
  49. package/dist/plugins/orders/concepts/Order.js +98 -0
  50. package/dist/plugins/orders/concepts/Order.js.map +1 -0
  51. package/dist/plugins/orders/index.d.ts +3 -0
  52. package/dist/plugins/orders/index.d.ts.map +1 -0
  53. package/dist/plugins/orders/index.js +16 -0
  54. package/dist/plugins/orders/index.js.map +1 -0
  55. package/dist/plugins/orders/syncs/order-workflow.sync.d.ts +3 -0
  56. package/dist/plugins/orders/syncs/order-workflow.sync.d.ts.map +1 -0
  57. package/dist/plugins/orders/syncs/order-workflow.sync.js +168 -0
  58. package/dist/plugins/orders/syncs/order-workflow.sync.js.map +1 -0
  59. package/dist/plugins/payments/concepts/Payment.d.ts +3 -0
  60. package/dist/plugins/payments/concepts/Payment.d.ts.map +1 -0
  61. package/dist/plugins/payments/concepts/Payment.js +156 -0
  62. package/dist/plugins/payments/concepts/Payment.js.map +1 -0
  63. package/dist/plugins/payments/index.d.ts +3 -0
  64. package/dist/plugins/payments/index.d.ts.map +1 -0
  65. package/dist/plugins/payments/index.js +16 -0
  66. package/dist/plugins/payments/index.js.map +1 -0
  67. package/dist/plugins/payments/syncs/payment-workflow.sync.d.ts +3 -0
  68. package/dist/plugins/payments/syncs/payment-workflow.sync.d.ts.map +1 -0
  69. package/dist/plugins/payments/syncs/payment-workflow.sync.js +264 -0
  70. package/dist/plugins/payments/syncs/payment-workflow.sync.js.map +1 -0
  71. package/dist/plugins/products/concepts/Product.d.ts +3 -0
  72. package/dist/plugins/products/concepts/Product.d.ts.map +1 -0
  73. package/dist/plugins/products/concepts/Product.js +85 -0
  74. package/dist/plugins/products/concepts/Product.js.map +1 -0
  75. package/dist/plugins/products/index.d.ts +3 -0
  76. package/dist/plugins/products/index.d.ts.map +1 -0
  77. package/dist/plugins/products/index.js +16 -0
  78. package/dist/plugins/products/index.js.map +1 -0
  79. package/dist/plugins/products/syncs/product-events.sync.d.ts +3 -0
  80. package/dist/plugins/products/syncs/product-events.sync.d.ts.map +1 -0
  81. package/dist/plugins/products/syncs/product-events.sync.js +77 -0
  82. package/dist/plugins/products/syncs/product-events.sync.js.map +1 -0
  83. package/dist/plugins/users/concepts/User.d.ts +3 -0
  84. package/dist/plugins/users/concepts/User.d.ts.map +1 -0
  85. package/dist/plugins/users/concepts/User.js +81 -0
  86. package/dist/plugins/users/concepts/User.js.map +1 -0
  87. package/dist/plugins/users/index.d.ts +3 -0
  88. package/dist/plugins/users/index.d.ts.map +1 -0
  89. package/dist/plugins/users/index.js +16 -0
  90. package/dist/plugins/users/index.js.map +1 -0
  91. package/dist/plugins/users/syncs/user-events.sync.d.ts +3 -0
  92. package/dist/plugins/users/syncs/user-events.sync.d.ts.map +1 -0
  93. package/dist/plugins/users/syncs/user-events.sync.js +75 -0
  94. package/dist/plugins/users/syncs/user-events.sync.js.map +1 -0
  95. package/package.json +40 -0
  96. package/src/core/EventBus.ts +55 -0
  97. package/src/core/PluginManager.ts +51 -0
  98. package/src/index.ts +169 -0
  99. package/src/plugins/analytics/concepts/Analytics.ts +53 -0
  100. package/src/plugins/analytics/index.ts +15 -0
  101. package/src/plugins/analytics/syncs/analytics-events.sync.ts +103 -0
  102. package/src/plugins/inventory/concepts/Inventory.ts +73 -0
  103. package/src/plugins/inventory/index.ts +14 -0
  104. package/src/plugins/notifications/concepts/Notification.ts +49 -0
  105. package/src/plugins/notifications/index.ts +14 -0
  106. package/src/plugins/orders/concepts/Order.ts +118 -0
  107. package/src/plugins/orders/index.ts +15 -0
  108. package/src/plugins/orders/syncs/order-workflow.sync.ts +173 -0
  109. package/src/plugins/payments/concepts/Payment.ts +186 -0
  110. package/src/plugins/payments/index.ts +15 -0
  111. package/src/plugins/payments/syncs/payment-workflow.sync.ts +274 -0
  112. package/src/plugins/products/concepts/Product.ts +102 -0
  113. package/src/plugins/products/index.ts +15 -0
  114. package/src/plugins/products/syncs/product-events.sync.ts +78 -0
  115. package/src/plugins/users/concepts/User.ts +97 -0
  116. package/src/plugins/users/index.ts +15 -0
  117. package/src/plugins/users/syncs/user-events.sync.ts +76 -0
  118. package/tsconfig.json +9 -0
package/.eslintrc.js ADDED
@@ -0,0 +1,16 @@
1
+ module.exports = {
2
+ parser: '@typescript-eslint/parser',
3
+ extends: [
4
+ 'eslint:recommended',
5
+ 'plugin:@typescript-eslint/recommended'
6
+ ],
7
+ plugins: ['@typescript-eslint'],
8
+ env: {
9
+ node: true,
10
+ es6: true
11
+ },
12
+ rules: {
13
+ '@typescript-eslint/no-explicit-any': 'off',
14
+ '@typescript-eslint/no-unused-vars': 'off'
15
+ }
16
+ };
package/README.md ADDED
@@ -0,0 +1,103 @@
1
+ # Event-Driven Architecture Example
2
+
3
+ This example demonstrates how to build an Event-Driven Architecture (EDA) system using LegibleSync with a plugin-based modular architecture.
4
+
5
+ ## Architecture Overview
6
+
7
+ The system is composed of independent plugins that communicate through events:
8
+
9
+ - **Users Plugin**: User management and authentication
10
+ - **Products Plugin**: Product catalog management
11
+ - **Orders Plugin**: Order processing and lifecycle
12
+ - **Inventory Plugin**: Stock management and alerts
13
+ - **Notifications Plugin**: Multi-channel notifications
14
+ - **Analytics Plugin**: Event tracking and reporting
15
+
16
+ ## Event Flow
17
+
18
+ ```
19
+ User Registration → Welcome Email → Analytics Track
20
+
21
+ Product Added → Inventory Update → Low Stock Alert
22
+
23
+ Order Created → Inventory Deduct → Payment Process → Order Confirmed
24
+
25
+ Notification Sent → Analytics Update
26
+ ```
27
+
28
+ ## Plugin Structure
29
+
30
+ Each plugin follows the same structure:
31
+
32
+ ```
33
+ plugin-name/
34
+ ├── concepts/ # Business logic components
35
+ ├── syncs/ # Event-driven rules
36
+ └── index.ts # Plugin registration
37
+ ```
38
+
39
+ ## Key EDA Patterns Demonstrated
40
+
41
+ ### 1. Event Sourcing
42
+ - All business events are captured as ActionRecords
43
+ - Complete audit trail of system activity
44
+
45
+ ### 2. CQRS (Command Query Responsibility Segregation)
46
+ - Commands: Write operations (create, update, delete)
47
+ - Queries: Read operations with filtering
48
+
49
+ ### 3. Event-Driven Orchestration
50
+ - Declarative sync rules define event flows
51
+ - Automatic execution of business processes
52
+
53
+ ### 4. Plugin Isolation
54
+ - Each plugin is independently deployable
55
+ - Loose coupling through event contracts
56
+
57
+ ## Running the Example
58
+
59
+ ```bash
60
+ cd packages/example-eda
61
+ npm run dev
62
+ ```
63
+
64
+ This will start the EDA system and demonstrate various event flows.
65
+
66
+ ## Testing
67
+
68
+ The example includes comprehensive test suites:
69
+
70
+ ### Unit Tests
71
+ ```bash
72
+ # Run all unit tests
73
+ npm test
74
+
75
+ # Run specific test file
76
+ npm test -- EventBus.test.ts
77
+
78
+ # Run with coverage
79
+ npm run test:coverage
80
+
81
+ # Run tests in watch mode
82
+ npm run test:watch
83
+ ```
84
+
85
+ ### Test Structure
86
+
87
+ - **Unit Tests** (`__tests__/unit/`): Test individual components
88
+ - `PluginManager.test.ts`: Plugin loading and management
89
+ - `EventBus.test.ts`: Event publishing and subscription
90
+ - `concepts/User.test.ts`: User concept business logic
91
+
92
+ - **Integration Tests** (`__tests__/integration/`): Test complete workflows
93
+ - `ecommerce-flow.test.ts`: End-to-end e-commerce scenarios
94
+
95
+ ### Test Coverage
96
+
97
+ The test suite covers:
98
+ - ✅ Plugin loading and initialization
99
+ - ✅ Event publishing and subscription
100
+ - ✅ Business logic validation
101
+ - ✅ Cross-plugin communication
102
+ - ✅ Error handling
103
+ - ✅ Async operation handling
@@ -0,0 +1,247 @@
1
+ import { LegibleEngine } from '@legible-sync/core';
2
+ import { PluginManager } from '../../src/core/PluginManager';
3
+ import { EventBus } from '../../src/core/EventBus';
4
+ import { usersPlugin } from '../../src/plugins/users';
5
+ import { productsPlugin } from '../../src/plugins/products';
6
+ import { ordersPlugin } from '../../src/plugins/orders';
7
+ import { inventoryPlugin } from '../../src/plugins/inventory';
8
+ import { notificationsPlugin } from '../../src/plugins/notifications';
9
+ import { analyticsPlugin } from '../../src/plugins/analytics';
10
+ import { paymentsPlugin } from '../../src/plugins/payments';
11
+
12
+ describe('E-commerce Flow Integration', () => {
13
+ let engine: LegibleEngine;
14
+ let pluginManager: PluginManager;
15
+ let eventBus: EventBus;
16
+
17
+ beforeEach(async () => {
18
+ engine = new LegibleEngine();
19
+ pluginManager = new PluginManager(engine);
20
+ eventBus = new EventBus();
21
+
22
+ // Register EventBus as a concept
23
+ engine.registerConcept('EventBus', {
24
+ state: {},
25
+ async execute(action: string, input: any) {
26
+ if (action === 'publish') {
27
+ const { event, data } = input;
28
+ await eventBus.publish({
29
+ type: event,
30
+ payload: data,
31
+ source: 'system',
32
+ flowId: input.flowId || 'system'
33
+ });
34
+ return { published: true };
35
+ }
36
+ throw new Error(`Unknown EventBus action: ${action}`);
37
+ }
38
+ });
39
+
40
+ // Load all plugins
41
+ await pluginManager.loadPlugin(usersPlugin);
42
+ await pluginManager.loadPlugin(productsPlugin);
43
+ await pluginManager.loadPlugin(inventoryPlugin);
44
+ await pluginManager.loadPlugin(ordersPlugin);
45
+ await pluginManager.loadPlugin(paymentsPlugin);
46
+ await pluginManager.loadPlugin(notificationsPlugin);
47
+ await pluginManager.loadPlugin(analyticsPlugin);
48
+
49
+ // Reset concept states for test isolation
50
+ await engine.invoke('User', 'reset', {}, 'reset');
51
+ await engine.invoke('Product', 'reset', {}, 'reset');
52
+ });
53
+
54
+ describe('Complete User Registration Flow', () => {
55
+ it('should handle user registration with analytics tracking', async () => {
56
+ const flowId = 'user-reg-flow';
57
+
58
+ const result = await engine.invoke('User', 'register', {
59
+ username: 'johndoe',
60
+ email: 'john@example.com',
61
+ password: 'password123'
62
+ }, flowId);
63
+
64
+ expect(result.userId).toBeDefined();
65
+ expect(result.user.username).toBe('johndoe');
66
+ expect(result.user.email).toBe('john@example.com');
67
+
68
+ // Check analytics tracking
69
+ const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
70
+ expect(analytics.metrics['user_registered']).toBe(1);
71
+ });
72
+ });
73
+
74
+ describe('Complete Product Management Flow', () => {
75
+ it('should handle product creation with analytics tracking', async () => {
76
+ const flowId = 'product-create-flow';
77
+
78
+ const result = await engine.invoke('Product', 'create', {
79
+ name: 'Test Product',
80
+ sku: 'TEST-001',
81
+ price: 29.99,
82
+ description: 'A test product',
83
+ category: 'test'
84
+ }, flowId);
85
+
86
+ expect(result.productId).toBeDefined();
87
+ expect(result.product.name).toBe('Test Product');
88
+ expect(result.product.sku).toBe('TEST-001');
89
+
90
+ // Check analytics tracking
91
+ const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
92
+ expect(analytics.metrics['product_created']).toBe(1);
93
+ });
94
+ });
95
+
96
+ describe('Order Processing Flow', () => {
97
+ let userId: string;
98
+ let productId: string;
99
+
100
+ beforeEach(async () => {
101
+ // Create user
102
+ const userResult = await engine.invoke('User', 'register', {
103
+ username: 'testuser',
104
+ email: 'test@example.com',
105
+ password: 'password123'
106
+ }, 'setup-user');
107
+
108
+ userId = userResult.userId;
109
+
110
+ // Create product
111
+ const productResult = await engine.invoke('Product', 'create', {
112
+ name: 'Test Product',
113
+ sku: 'TEST-001',
114
+ price: 29.99,
115
+ category: 'test'
116
+ }, 'setup-product');
117
+
118
+ productId = productResult.productId;
119
+
120
+ // Set inventory
121
+ await engine.invoke('Inventory', 'setStock', {
122
+ productId,
123
+ quantity: 10
124
+ }, 'setup-inventory');
125
+ });
126
+
127
+ it('should process complete order workflow', async () => {
128
+ const orderFlow = 'order-flow';
129
+
130
+ // Create order
131
+ const orderResult = await engine.invoke('Order', 'create', {
132
+ userId,
133
+ items: [
134
+ {
135
+ productId,
136
+ quantity: 2,
137
+ price: 29.99
138
+ }
139
+ ]
140
+ }, orderFlow);
141
+
142
+ expect(orderResult.orderId).toBeDefined();
143
+
144
+ // Wait for async operations
145
+ await new Promise(resolve => setTimeout(resolve, 100));
146
+
147
+ // Check order status (should be auto-confirmed)
148
+ const orderStatus = await engine.invoke('Order', 'get', {
149
+ orderId: orderResult.orderId
150
+ }, 'status-check');
151
+
152
+ expect(orderStatus.order.status).toBe('confirmed');
153
+ expect(orderStatus.order.total).toBe(59.98); // 2 * 29.99
154
+
155
+ // Check inventory deduction
156
+ const inventoryStatus = await engine.invoke('Inventory', 'getStock', {
157
+ productId
158
+ }, 'inventory-check');
159
+
160
+ expect(inventoryStatus.quantity).toBe(8); // 10 - 2
161
+
162
+ // Check analytics
163
+ const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'analytics-check');
164
+ expect(analytics.metrics['order_created']).toBe(1);
165
+ expect(analytics.metrics['order_confirmed']).toBe(1);
166
+ });
167
+
168
+ it('should handle order confirmation notifications', async () => {
169
+ // Create and confirm order
170
+ const orderResult = await engine.invoke('Order', 'create', {
171
+ userId,
172
+ items: [{ productId, quantity: 1, price: 29.99 }]
173
+ }, 'notification-test');
174
+
175
+ // Wait for processing
176
+ await new Promise(resolve => setTimeout(resolve, 50));
177
+
178
+ // Check notification history
179
+ const notifications = await engine.invoke('Notification', 'getHistory', {
180
+ recipient: 'test@example.com'
181
+ }, 'notification-check');
182
+
183
+ expect(notifications.notifications.length).toBeGreaterThan(0);
184
+ const orderNotification = notifications.notifications.find((n: any) =>
185
+ n.template === 'order-confirmation'
186
+ );
187
+ expect(orderNotification).toBeDefined();
188
+ });
189
+ });
190
+
191
+ describe('Plugin Integration', () => {
192
+ it('should have all plugins loaded', () => {
193
+ const loadedPlugins = pluginManager.getLoadedPlugins();
194
+ expect(loadedPlugins).toHaveLength(7);
195
+ expect(loadedPlugins).toContain('users');
196
+ expect(loadedPlugins).toContain('products');
197
+ expect(loadedPlugins).toContain('orders');
198
+ expect(loadedPlugins).toContain('inventory');
199
+ expect(loadedPlugins).toContain('payments');
200
+ expect(loadedPlugins).toContain('notifications');
201
+ expect(loadedPlugins).toContain('analytics');
202
+ });
203
+
204
+ it('should handle cross-plugin communication', async () => {
205
+ // Create user
206
+ const userResult = await engine.invoke('User', 'register', {
207
+ username: 'integration-test',
208
+ email: 'integration@example.com',
209
+ password: 'password123'
210
+ }, 'integration-flow');
211
+
212
+ // Create product
213
+ const productResult = await engine.invoke('Product', 'create', {
214
+ name: 'Integration Product',
215
+ sku: 'INT-001',
216
+ price: 19.99,
217
+ category: 'integration'
218
+ }, 'integration-flow');
219
+
220
+ // Set inventory
221
+ await engine.invoke('Inventory', 'setStock', {
222
+ productId: productResult.productId,
223
+ quantity: 5
224
+ }, 'integration-flow');
225
+
226
+ // Create order (this triggers multiple plugins)
227
+ await engine.invoke('Order', 'create', {
228
+ userId: userResult.userId,
229
+ items: [{
230
+ productId: productResult.productId,
231
+ quantity: 1,
232
+ price: 19.99
233
+ }]
234
+ }, 'integration-flow');
235
+
236
+ // Wait for all async operations
237
+ await new Promise(resolve => setTimeout(resolve, 100));
238
+
239
+ // Verify cross-plugin effects
240
+ const analytics = await engine.invoke('Analytics', 'getMetrics', {}, 'final-check');
241
+ expect(analytics.metrics['user_registered']).toBeGreaterThanOrEqual(1);
242
+ expect(analytics.metrics['product_created']).toBeGreaterThanOrEqual(1);
243
+ expect(analytics.metrics['order_created']).toBeGreaterThanOrEqual(1);
244
+ expect(analytics.metrics['order_confirmed']).toBeGreaterThanOrEqual(1);
245
+ });
246
+ });
247
+ });
@@ -0,0 +1,154 @@
1
+ import { EventBus, Event } from '../../src/core/EventBus';
2
+
3
+ describe('EventBus', () => {
4
+ let eventBus: EventBus;
5
+
6
+ beforeEach(() => {
7
+ eventBus = new EventBus();
8
+ });
9
+
10
+ describe('Event Subscription', () => {
11
+ it('should subscribe to events', () => {
12
+ const handler = jest.fn();
13
+ eventBus.subscribe('user.created', handler);
14
+
15
+ // Should not throw, subscription is successful
16
+ expect(() => eventBus.subscribe('user.created', handler)).not.toThrow();
17
+ });
18
+
19
+ it('should handle multiple handlers for the same event', async () => {
20
+ const handler1 = jest.fn();
21
+ const handler2 = jest.fn();
22
+
23
+ eventBus.subscribe('order.placed', handler1);
24
+ eventBus.subscribe('order.placed', handler2);
25
+
26
+ const eventData = {
27
+ type: 'order.placed' as const,
28
+ payload: { orderId: '123' },
29
+ source: 'orders',
30
+ flowId: 'flow1'
31
+ };
32
+
33
+ await eventBus.publish(eventData);
34
+
35
+ expect(handler1).toHaveBeenCalledTimes(1);
36
+ expect(handler2).toHaveBeenCalledTimes(1);
37
+
38
+ const calledEvent1 = handler1.mock.calls[0][0];
39
+ const calledEvent2 = handler2.mock.calls[0][0];
40
+
41
+ expect(calledEvent1.type).toBe('order.placed');
42
+ expect(calledEvent1.payload).toEqual({ orderId: '123' });
43
+ expect(calledEvent1.source).toBe('orders');
44
+ expect(calledEvent1.flowId).toBe('flow1');
45
+ expect(calledEvent1.timestamp).toBeInstanceOf(Date);
46
+
47
+ expect(calledEvent2.type).toBe('order.placed');
48
+ expect(calledEvent2.payload).toEqual({ orderId: '123' });
49
+ expect(calledEvent2.source).toBe('orders');
50
+ expect(calledEvent2.flowId).toBe('flow1');
51
+ expect(calledEvent2.timestamp).toBeInstanceOf(Date);
52
+ });
53
+ });
54
+
55
+ describe('Event Publishing', () => {
56
+ it('should publish events to subscribed handlers', async () => {
57
+ const handler = jest.fn();
58
+ eventBus.subscribe('product.updated', handler);
59
+
60
+ const eventData = {
61
+ type: 'product.updated' as const,
62
+ payload: { productId: '456', changes: { price: 29.99 } },
63
+ source: 'products',
64
+ flowId: 'flow2'
65
+ };
66
+
67
+ await eventBus.publish(eventData);
68
+
69
+ expect(handler).toHaveBeenCalledTimes(1);
70
+ const calledEvent = handler.mock.calls[0][0];
71
+ expect(calledEvent.type).toBe('product.updated');
72
+ expect(calledEvent.payload).toEqual(eventData.payload);
73
+ expect(calledEvent.source).toBe('products');
74
+ expect(calledEvent.flowId).toBe('flow2');
75
+ expect(calledEvent.timestamp).toBeInstanceOf(Date);
76
+ });
77
+
78
+ it('should handle events with no subscribers', async () => {
79
+ const eventData = {
80
+ type: 'unknown.event' as const,
81
+ payload: { data: 'test' },
82
+ source: 'test',
83
+ flowId: 'flow3'
84
+ };
85
+
86
+ // Should not throw when publishing to non-existent subscribers
87
+ await expect(eventBus.publish(eventData)).resolves.not.toThrow();
88
+ });
89
+
90
+ it('should handle handler errors gracefully', async () => {
91
+ const goodHandler = jest.fn();
92
+ const badHandler = jest.fn().mockRejectedValue(new Error('Handler failed'));
93
+
94
+ eventBus.subscribe('test.event', goodHandler);
95
+ eventBus.subscribe('test.event', badHandler);
96
+
97
+ const eventData = {
98
+ type: 'test.event' as const,
99
+ payload: { test: true },
100
+ source: 'test',
101
+ flowId: 'flow4'
102
+ };
103
+
104
+ // Should not throw even if one handler fails
105
+ await expect(eventBus.publish(eventData)).resolves.not.toThrow();
106
+
107
+ // Good handler should still be called
108
+ expect(goodHandler).toHaveBeenCalledTimes(1);
109
+ expect(badHandler).toHaveBeenCalledTimes(1);
110
+ });
111
+
112
+ it('should call handlers asynchronously', async () => {
113
+ const handler = jest.fn().mockResolvedValue(undefined);
114
+ eventBus.subscribe('async.event', handler);
115
+
116
+ const eventData = {
117
+ type: 'async.event' as const,
118
+ payload: { async: true },
119
+ source: 'async',
120
+ flowId: 'flow5'
121
+ };
122
+
123
+ const publishPromise = eventBus.publish(eventData);
124
+
125
+ // Should return a promise
126
+ expect(publishPromise).toBeInstanceOf(Promise);
127
+
128
+ await publishPromise;
129
+
130
+ expect(handler).toHaveBeenCalledTimes(1);
131
+ });
132
+ });
133
+
134
+ describe('Event Creation Helper', () => {
135
+ it('should create events from engine actions', () => {
136
+ const event = eventBus.createEventFromAction(
137
+ 'User',
138
+ 'register',
139
+ { username: 'testuser' },
140
+ { userId: '123', user: { id: '123' } },
141
+ 'flow6'
142
+ );
143
+
144
+ expect(event.type).toBe('User.register');
145
+ expect(event.payload).toEqual({
146
+ input: { username: 'testuser' },
147
+ output: { userId: '123', user: { id: '123' } }
148
+ });
149
+ expect(event.source).toBe('User');
150
+ expect(event.flowId).toBe('flow6');
151
+ expect(event.timestamp).toBeInstanceOf(Date);
152
+ });
153
+ });
154
+ });
@@ -0,0 +1,111 @@
1
+ import { LegibleEngine } from '@legible-sync/core';
2
+ import { PluginManager, Plugin } from '../../src/core/PluginManager';
3
+
4
+ describe('PluginManager', () => {
5
+ let engine: LegibleEngine;
6
+ let pluginManager: PluginManager;
7
+
8
+ beforeEach(() => {
9
+ engine = new LegibleEngine();
10
+ pluginManager = new PluginManager(engine);
11
+ });
12
+
13
+ describe('Plugin Loading', () => {
14
+ it('should load a plugin successfully', async () => {
15
+ const mockPlugin: Plugin = {
16
+ name: 'test-plugin',
17
+ concepts: {
18
+ TestConcept: {
19
+ state: { counter: 0 },
20
+ execute: jest.fn().mockResolvedValue({ success: true })
21
+ }
22
+ },
23
+ syncs: [],
24
+ initialize: jest.fn().mockResolvedValue(undefined)
25
+ };
26
+
27
+ await pluginManager.loadPlugin(mockPlugin);
28
+
29
+ expect(mockPlugin.initialize).toHaveBeenCalledWith(engine);
30
+ expect(pluginManager.getLoadedPlugins()).toContain('test-plugin');
31
+ expect(pluginManager.getPlugin('test-plugin')).toBe(mockPlugin);
32
+ });
33
+
34
+ it('should register concepts and syncs with the engine', async () => {
35
+ const mockConcept = {
36
+ state: {},
37
+ execute: jest.fn().mockResolvedValue({ result: 'ok' })
38
+ };
39
+
40
+ const mockSync = {
41
+ name: 'test-sync',
42
+ when: [{ concept: 'TestConcept', action: 'test' }],
43
+ then: [{ concept: 'OtherConcept', action: 'handle', input: {} }]
44
+ };
45
+
46
+ const mockPlugin: Plugin = {
47
+ name: 'test-plugin',
48
+ concepts: { TestConcept: mockConcept },
49
+ syncs: [mockSync]
50
+ };
51
+
52
+ // Mock engine methods
53
+ const registerConceptSpy = jest.spyOn(engine as any, 'registerConcept');
54
+ const registerSyncSpy = jest.spyOn(engine as any, 'registerSync');
55
+
56
+ await pluginManager.loadPlugin(mockPlugin);
57
+
58
+ expect(registerConceptSpy).toHaveBeenCalledWith('TestConcept', mockConcept);
59
+ expect(registerSyncSpy).toHaveBeenCalledWith(mockSync);
60
+ });
61
+
62
+ it('should handle plugin initialization errors', async () => {
63
+ const mockPlugin: Plugin = {
64
+ name: 'failing-plugin',
65
+ concepts: {},
66
+ syncs: [],
67
+ initialize: jest.fn().mockRejectedValue(new Error('Init failed'))
68
+ };
69
+
70
+ await expect(pluginManager.loadPlugin(mockPlugin)).rejects.toThrow('Init failed');
71
+ expect(pluginManager.getLoadedPlugins()).not.toContain('failing-plugin');
72
+ });
73
+ });
74
+
75
+ describe('Plugin Management', () => {
76
+ it('should return loaded plugin names', async () => {
77
+ const plugin1: Plugin = {
78
+ name: 'plugin1',
79
+ concepts: {},
80
+ syncs: []
81
+ };
82
+
83
+ const plugin2: Plugin = {
84
+ name: 'plugin2',
85
+ concepts: {},
86
+ syncs: []
87
+ };
88
+
89
+ await pluginManager.loadPlugin(plugin1);
90
+ await pluginManager.loadPlugin(plugin2);
91
+
92
+ const loadedPlugins = pluginManager.getLoadedPlugins();
93
+ expect(loadedPlugins).toHaveLength(2);
94
+ expect(loadedPlugins).toContain('plugin1');
95
+ expect(loadedPlugins).toContain('plugin2');
96
+ });
97
+
98
+ it('should return specific plugin by name', async () => {
99
+ const mockPlugin: Plugin = {
100
+ name: 'specific-plugin',
101
+ concepts: {},
102
+ syncs: []
103
+ };
104
+
105
+ await pluginManager.loadPlugin(mockPlugin);
106
+
107
+ expect(pluginManager.getPlugin('specific-plugin')).toBe(mockPlugin);
108
+ expect(pluginManager.getPlugin('non-existent')).toBeUndefined();
109
+ });
110
+ });
111
+ });