@checkstack/integration-backend 0.0.2
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.
- package/CHANGELOG.md +85 -0
- package/drizzle/0000_glossy_red_hulk.sql +28 -0
- package/drizzle/0001_rich_fixer.sql +2 -0
- package/drizzle/meta/0000_snapshot.json +191 -0
- package/drizzle/meta/0001_snapshot.json +190 -0
- package/drizzle/meta/_journal.json +20 -0
- package/drizzle.config.ts +8 -0
- package/package.json +36 -0
- package/src/connection-store.test.ts +468 -0
- package/src/connection-store.ts +463 -0
- package/src/delivery-coordinator.ts +390 -0
- package/src/event-registry.test.ts +396 -0
- package/src/event-registry.ts +99 -0
- package/src/hook-subscriber.ts +104 -0
- package/src/index.ts +306 -0
- package/src/provider-registry.test.ts +314 -0
- package/src/provider-registry.ts +107 -0
- package/src/provider-types.ts +257 -0
- package/src/router.ts +858 -0
- package/src/schema.ts +78 -0
- package/tsconfig.json +6 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createBackendPlugin,
|
|
3
|
+
coreServices,
|
|
4
|
+
createExtensionPoint,
|
|
5
|
+
createServiceRef,
|
|
6
|
+
} from "@checkstack/backend-api";
|
|
7
|
+
import {
|
|
8
|
+
permissionList,
|
|
9
|
+
pluginMetadata,
|
|
10
|
+
integrationContract,
|
|
11
|
+
integrationRoutes,
|
|
12
|
+
permissions,
|
|
13
|
+
} from "@checkstack/integration-common";
|
|
14
|
+
import { resolveRoute } from "@checkstack/common";
|
|
15
|
+
import type { PluginMetadata } from "@checkstack/common";
|
|
16
|
+
import type {
|
|
17
|
+
IntegrationEventDefinition,
|
|
18
|
+
IntegrationProvider,
|
|
19
|
+
} from "./provider-types";
|
|
20
|
+
|
|
21
|
+
import * as schema from "./schema";
|
|
22
|
+
import {
|
|
23
|
+
createIntegrationEventRegistry,
|
|
24
|
+
type IntegrationEventRegistry,
|
|
25
|
+
} from "./event-registry";
|
|
26
|
+
import {
|
|
27
|
+
createIntegrationProviderRegistry,
|
|
28
|
+
type IntegrationProviderRegistry,
|
|
29
|
+
} from "./provider-registry";
|
|
30
|
+
import { createDeliveryCoordinator } from "./delivery-coordinator";
|
|
31
|
+
import {
|
|
32
|
+
createConnectionStore,
|
|
33
|
+
type ConnectionStore,
|
|
34
|
+
} from "./connection-store";
|
|
35
|
+
import { subscribeToRegisteredEvents } from "./hook-subscriber";
|
|
36
|
+
import { createIntegrationRouter } from "./router";
|
|
37
|
+
import { registerSearchProvider } from "@checkstack/command-backend";
|
|
38
|
+
|
|
39
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
40
|
+
// Service References
|
|
41
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Service reference for the connection store.
|
|
45
|
+
* Provider plugins can inject this to access connection credentials.
|
|
46
|
+
*/
|
|
47
|
+
export const connectionStoreRef = createServiceRef<ConnectionStore>(
|
|
48
|
+
"integration.connectionStore"
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
52
|
+
// Extension Points
|
|
53
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extension point for registering integration events.
|
|
57
|
+
* Plugins use this to expose their hooks for external webhook subscriptions.
|
|
58
|
+
*/
|
|
59
|
+
export interface IntegrationEventExtensionPoint {
|
|
60
|
+
/**
|
|
61
|
+
* Register a hook as an integration event.
|
|
62
|
+
* The event will be namespaced by the plugin's ID automatically.
|
|
63
|
+
*/
|
|
64
|
+
registerEvent<T>(
|
|
65
|
+
definition: IntegrationEventDefinition<T>,
|
|
66
|
+
pluginMetadata: PluginMetadata
|
|
67
|
+
): void;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export const integrationEventExtensionPoint =
|
|
71
|
+
createExtensionPoint<IntegrationEventExtensionPoint>(
|
|
72
|
+
"integration.eventExtensionPoint"
|
|
73
|
+
);
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Extension point for registering integration providers.
|
|
77
|
+
* Plugins use this to register webhook delivery providers.
|
|
78
|
+
*/
|
|
79
|
+
export interface IntegrationProviderExtensionPoint {
|
|
80
|
+
/**
|
|
81
|
+
* Register an integration provider.
|
|
82
|
+
* The provider will be namespaced by the plugin's ID automatically.
|
|
83
|
+
* @template TConfig - Per-subscription configuration type
|
|
84
|
+
* @template TConnection - Site-wide connection configuration type (optional)
|
|
85
|
+
*/
|
|
86
|
+
addProvider<TConfig, TConnection = undefined>(
|
|
87
|
+
provider: IntegrationProvider<TConfig, TConnection>,
|
|
88
|
+
pluginMetadata: PluginMetadata
|
|
89
|
+
): void;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export const integrationProviderExtensionPoint =
|
|
93
|
+
createExtensionPoint<IntegrationProviderExtensionPoint>(
|
|
94
|
+
"integration.providerExtensionPoint"
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
98
|
+
// Plugin Definition
|
|
99
|
+
// ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
|
|
100
|
+
|
|
101
|
+
export default createBackendPlugin({
|
|
102
|
+
metadata: pluginMetadata,
|
|
103
|
+
|
|
104
|
+
register(env) {
|
|
105
|
+
// Create the registries
|
|
106
|
+
const eventRegistry = createIntegrationEventRegistry();
|
|
107
|
+
const providerRegistry = createIntegrationProviderRegistry();
|
|
108
|
+
|
|
109
|
+
// Register static permissions
|
|
110
|
+
env.registerPermissions(permissionList);
|
|
111
|
+
|
|
112
|
+
// Register the event extension point
|
|
113
|
+
env.registerExtensionPoint(integrationEventExtensionPoint, {
|
|
114
|
+
registerEvent: <T>(
|
|
115
|
+
definition: IntegrationEventDefinition<T>,
|
|
116
|
+
metadata: PluginMetadata
|
|
117
|
+
) => {
|
|
118
|
+
// Type erasure: cast to unknown for storage (the registry handles this internally)
|
|
119
|
+
eventRegistry.register(
|
|
120
|
+
definition as IntegrationEventDefinition<unknown>,
|
|
121
|
+
metadata
|
|
122
|
+
);
|
|
123
|
+
},
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
// Register the provider extension point
|
|
127
|
+
env.registerExtensionPoint(integrationProviderExtensionPoint, {
|
|
128
|
+
addProvider: (provider, metadata) => {
|
|
129
|
+
// Type erasure: cast to unknown for storage (the registry handles this internally)
|
|
130
|
+
providerRegistry.register(
|
|
131
|
+
provider as IntegrationProvider<unknown>,
|
|
132
|
+
metadata
|
|
133
|
+
);
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
env.registerInit({
|
|
138
|
+
schema,
|
|
139
|
+
deps: {
|
|
140
|
+
logger: coreServices.logger,
|
|
141
|
+
rpc: coreServices.rpc,
|
|
142
|
+
config: coreServices.config,
|
|
143
|
+
signalService: coreServices.signalService,
|
|
144
|
+
queueManager: coreServices.queueManager,
|
|
145
|
+
},
|
|
146
|
+
init: async ({
|
|
147
|
+
logger,
|
|
148
|
+
database,
|
|
149
|
+
rpc,
|
|
150
|
+
config,
|
|
151
|
+
signalService,
|
|
152
|
+
queueManager,
|
|
153
|
+
}) => {
|
|
154
|
+
logger.debug("🔌 Initializing Integration Backend...");
|
|
155
|
+
|
|
156
|
+
const db = database;
|
|
157
|
+
|
|
158
|
+
// Create connection store for generic connection management
|
|
159
|
+
const connectionStore = createConnectionStore({
|
|
160
|
+
configService: config,
|
|
161
|
+
providerRegistry,
|
|
162
|
+
logger,
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Publish connection store for provider plugins to inject
|
|
166
|
+
env.registerService(connectionStoreRef, connectionStore);
|
|
167
|
+
|
|
168
|
+
// Create delivery coordinator
|
|
169
|
+
const deliveryCoordinator = createDeliveryCoordinator({
|
|
170
|
+
db,
|
|
171
|
+
providerRegistry,
|
|
172
|
+
connectionStore,
|
|
173
|
+
queueManager,
|
|
174
|
+
signalService,
|
|
175
|
+
logger,
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
// Store for afterPluginsReady access
|
|
179
|
+
(
|
|
180
|
+
env as unknown as {
|
|
181
|
+
eventRegistry: IntegrationEventRegistry;
|
|
182
|
+
providerRegistry: IntegrationProviderRegistry;
|
|
183
|
+
deliveryCoordinator: typeof deliveryCoordinator;
|
|
184
|
+
}
|
|
185
|
+
).eventRegistry = eventRegistry;
|
|
186
|
+
(
|
|
187
|
+
env as unknown as {
|
|
188
|
+
eventRegistry: IntegrationEventRegistry;
|
|
189
|
+
providerRegistry: IntegrationProviderRegistry;
|
|
190
|
+
deliveryCoordinator: typeof deliveryCoordinator;
|
|
191
|
+
}
|
|
192
|
+
).providerRegistry = providerRegistry;
|
|
193
|
+
(
|
|
194
|
+
env as unknown as {
|
|
195
|
+
eventRegistry: IntegrationEventRegistry;
|
|
196
|
+
providerRegistry: IntegrationProviderRegistry;
|
|
197
|
+
deliveryCoordinator: typeof deliveryCoordinator;
|
|
198
|
+
}
|
|
199
|
+
).deliveryCoordinator = deliveryCoordinator;
|
|
200
|
+
|
|
201
|
+
// Create and register the router
|
|
202
|
+
const router = createIntegrationRouter({
|
|
203
|
+
db,
|
|
204
|
+
eventRegistry,
|
|
205
|
+
providerRegistry,
|
|
206
|
+
deliveryCoordinator,
|
|
207
|
+
connectionStore,
|
|
208
|
+
signalService,
|
|
209
|
+
logger,
|
|
210
|
+
});
|
|
211
|
+
rpc.registerRouter(router, integrationContract);
|
|
212
|
+
|
|
213
|
+
// Register command palette commands
|
|
214
|
+
registerSearchProvider({
|
|
215
|
+
pluginMetadata,
|
|
216
|
+
commands: [
|
|
217
|
+
{
|
|
218
|
+
id: "create",
|
|
219
|
+
title: "Create Integration Subscription",
|
|
220
|
+
subtitle: "Create a new subscription for integration events",
|
|
221
|
+
iconName: "Webhook",
|
|
222
|
+
route:
|
|
223
|
+
resolveRoute(integrationRoutes.routes.list) + "?action=create",
|
|
224
|
+
requiredPermissions: [permissions.integrationManage],
|
|
225
|
+
},
|
|
226
|
+
{
|
|
227
|
+
id: "manage",
|
|
228
|
+
title: "Manage Integrations",
|
|
229
|
+
subtitle: "Manage integration subscriptions and connections",
|
|
230
|
+
iconName: "Webhook",
|
|
231
|
+
shortcuts: ["meta+shift+g", "ctrl+shift+g"],
|
|
232
|
+
route: resolveRoute(integrationRoutes.routes.list),
|
|
233
|
+
requiredPermissions: [permissions.integrationManage],
|
|
234
|
+
},
|
|
235
|
+
{
|
|
236
|
+
id: "logs",
|
|
237
|
+
title: "View Integration Logs",
|
|
238
|
+
subtitle: "View integration delivery logs",
|
|
239
|
+
iconName: "FileText",
|
|
240
|
+
route: resolveRoute(integrationRoutes.routes.logs),
|
|
241
|
+
requiredPermissions: [permissions.integrationManage],
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
logger.debug("✅ Integration Backend initialized.");
|
|
247
|
+
},
|
|
248
|
+
afterPluginsReady: async ({ logger, onHook }) => {
|
|
249
|
+
// Get registries from env
|
|
250
|
+
const stored = env as unknown as {
|
|
251
|
+
eventRegistry: IntegrationEventRegistry;
|
|
252
|
+
providerRegistry: IntegrationProviderRegistry;
|
|
253
|
+
deliveryCoordinator: ReturnType<typeof createDeliveryCoordinator>;
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const events = stored.eventRegistry.getEvents();
|
|
257
|
+
const providers = stored.providerRegistry.getProviders();
|
|
258
|
+
|
|
259
|
+
logger.debug(
|
|
260
|
+
`🔌 Registered ${events.length} integration events: ${events
|
|
261
|
+
.map((e) => e.eventId)
|
|
262
|
+
.join(", ")}`
|
|
263
|
+
);
|
|
264
|
+
|
|
265
|
+
logger.debug(
|
|
266
|
+
`📡 Registered ${providers.length} integration providers: ${providers
|
|
267
|
+
.map((p) => p.qualifiedId)
|
|
268
|
+
.join(", ")}`
|
|
269
|
+
);
|
|
270
|
+
|
|
271
|
+
// Subscribe to all registered integration events
|
|
272
|
+
// Uses work-queue mode to ensure only ONE instance processes each event
|
|
273
|
+
subscribeToRegisteredEvents({
|
|
274
|
+
onHook,
|
|
275
|
+
eventRegistry: stored.eventRegistry,
|
|
276
|
+
deliveryCoordinator: stored.deliveryCoordinator,
|
|
277
|
+
logger,
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
// Start the delivery worker
|
|
281
|
+
await stored.deliveryCoordinator.startWorker();
|
|
282
|
+
|
|
283
|
+
logger.debug("✅ Integration Backend afterPluginsReady complete.");
|
|
284
|
+
},
|
|
285
|
+
});
|
|
286
|
+
},
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
// Re-export extension points for consumer plugins
|
|
290
|
+
export { integrationEventExtensionPoint as eventExtensionPoint };
|
|
291
|
+
export { integrationProviderExtensionPoint as providerExtensionPoint };
|
|
292
|
+
|
|
293
|
+
// Re-export provider types for consumer plugins
|
|
294
|
+
// All backend-only types are defined here (not in integration-common)
|
|
295
|
+
export type {
|
|
296
|
+
IntegrationEventDefinition,
|
|
297
|
+
IntegrationDeliveryContext,
|
|
298
|
+
IntegrationDeliveryResult,
|
|
299
|
+
TestConnectionResult,
|
|
300
|
+
ProviderDocumentation,
|
|
301
|
+
ConnectionOption,
|
|
302
|
+
GetConnectionOptionsParams,
|
|
303
|
+
IntegrationProvider,
|
|
304
|
+
RegisteredIntegrationProvider,
|
|
305
|
+
RegisteredIntegrationEvent,
|
|
306
|
+
} from "./provider-types";
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "bun:test";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
createIntegrationProviderRegistry,
|
|
5
|
+
type IntegrationProviderRegistry,
|
|
6
|
+
} from "./provider-registry";
|
|
7
|
+
import type { IntegrationProvider } from "./provider-types";
|
|
8
|
+
import { Versioned } from "@checkstack/backend-api";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Unit tests for IntegrationProviderRegistry.
|
|
12
|
+
*
|
|
13
|
+
* Tests cover:
|
|
14
|
+
* - Provider registration with proper namespacing
|
|
15
|
+
* - Provider retrieval by qualified ID
|
|
16
|
+
* - Config schema JSON conversion
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
// Test plugin metadata
|
|
20
|
+
const testPluginMetadata = {
|
|
21
|
+
pluginId: "test-plugin",
|
|
22
|
+
displayName: "Test Plugin",
|
|
23
|
+
} as const;
|
|
24
|
+
|
|
25
|
+
// Test config schemas
|
|
26
|
+
const webhookConfigSchema = z.object({
|
|
27
|
+
url: z.string().url(),
|
|
28
|
+
method: z.enum(["GET", "POST"]),
|
|
29
|
+
timeout: z.number().default(5000),
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const slackConfigSchema = z.object({
|
|
33
|
+
webhookUrl: z.string().url(),
|
|
34
|
+
channel: z.string(),
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
// Create test providers
|
|
38
|
+
function createTestProvider(
|
|
39
|
+
id: string,
|
|
40
|
+
schema: z.ZodType<unknown>
|
|
41
|
+
): IntegrationProvider<unknown> {
|
|
42
|
+
return {
|
|
43
|
+
id,
|
|
44
|
+
displayName: `${id.charAt(0).toUpperCase()}${id.slice(1)} Provider`,
|
|
45
|
+
description: `Deliver events via ${id}`,
|
|
46
|
+
icon: "Webhook",
|
|
47
|
+
config: new Versioned({
|
|
48
|
+
version: 1,
|
|
49
|
+
schema,
|
|
50
|
+
}),
|
|
51
|
+
deliver: async () => ({ success: true }),
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
describe("IntegrationProviderRegistry", () => {
|
|
56
|
+
let registry: IntegrationProviderRegistry;
|
|
57
|
+
|
|
58
|
+
beforeEach(() => {
|
|
59
|
+
registry = createIntegrationProviderRegistry();
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
63
|
+
// Provider Registration
|
|
64
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
65
|
+
|
|
66
|
+
describe("register", () => {
|
|
67
|
+
it("registers a provider with a fully qualified ID", () => {
|
|
68
|
+
const provider = createTestProvider("webhook", webhookConfigSchema);
|
|
69
|
+
|
|
70
|
+
registry.register(provider, testPluginMetadata);
|
|
71
|
+
|
|
72
|
+
expect(registry.hasProvider("test-plugin.webhook")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("generates correct qualified ID", () => {
|
|
76
|
+
const provider = createTestProvider("webhook", webhookConfigSchema);
|
|
77
|
+
|
|
78
|
+
registry.register(provider, testPluginMetadata);
|
|
79
|
+
|
|
80
|
+
const registered = registry.getProvider("test-plugin.webhook");
|
|
81
|
+
expect(registered?.qualifiedId).toBe("test-plugin.webhook");
|
|
82
|
+
expect(registered?.ownerPluginId).toBe("test-plugin");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it("preserves provider metadata", () => {
|
|
86
|
+
const provider: IntegrationProvider<unknown> = {
|
|
87
|
+
id: "custom",
|
|
88
|
+
displayName: "Custom Provider",
|
|
89
|
+
description: "A custom provider for testing",
|
|
90
|
+
icon: "Cog",
|
|
91
|
+
config: new Versioned({ version: 1, schema: webhookConfigSchema }),
|
|
92
|
+
deliver: async () => ({ success: true }),
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
registry.register(provider, testPluginMetadata);
|
|
96
|
+
|
|
97
|
+
const registered = registry.getProvider("test-plugin.custom");
|
|
98
|
+
expect(registered?.displayName).toBe("Custom Provider");
|
|
99
|
+
expect(registered?.description).toBe("A custom provider for testing");
|
|
100
|
+
expect(registered?.icon).toBe("Cog");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("preserves deliver function", () => {
|
|
104
|
+
const deliverFn = async () => ({ success: true, externalId: "ext-123" });
|
|
105
|
+
const provider: IntegrationProvider<unknown> = {
|
|
106
|
+
id: "webhook",
|
|
107
|
+
displayName: "Webhook",
|
|
108
|
+
config: new Versioned({ version: 1, schema: webhookConfigSchema }),
|
|
109
|
+
deliver: deliverFn,
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
registry.register(provider, testPluginMetadata);
|
|
113
|
+
|
|
114
|
+
const registered = registry.getProvider("test-plugin.webhook");
|
|
115
|
+
expect(registered?.deliver).toBeDefined();
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it("preserves testConnection function if provided", () => {
|
|
119
|
+
const provider: IntegrationProvider<unknown> = {
|
|
120
|
+
id: "webhook",
|
|
121
|
+
displayName: "Webhook",
|
|
122
|
+
config: new Versioned({ version: 1, schema: webhookConfigSchema }),
|
|
123
|
+
deliver: async () => ({ success: true }),
|
|
124
|
+
testConnection: async () => ({ success: true, message: "OK" }),
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
registry.register(provider, testPluginMetadata);
|
|
128
|
+
|
|
129
|
+
const registered = registry.getProvider("test-plugin.webhook");
|
|
130
|
+
expect(registered?.testConnection).toBeDefined();
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
135
|
+
// Provider Retrieval
|
|
136
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
describe("getProviders", () => {
|
|
139
|
+
it("returns empty array when no providers registered", () => {
|
|
140
|
+
expect(registry.getProviders()).toEqual([]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("returns all registered providers", () => {
|
|
144
|
+
registry.register(
|
|
145
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
146
|
+
testPluginMetadata
|
|
147
|
+
);
|
|
148
|
+
registry.register(
|
|
149
|
+
createTestProvider("slack", slackConfigSchema),
|
|
150
|
+
testPluginMetadata
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
const providers = registry.getProviders();
|
|
154
|
+
expect(providers.length).toBe(2);
|
|
155
|
+
expect(providers.map((p) => p.id).sort()).toEqual(["slack", "webhook"]);
|
|
156
|
+
});
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
describe("getProvider", () => {
|
|
160
|
+
it("returns undefined for non-existent provider", () => {
|
|
161
|
+
expect(registry.getProvider("non-existent.provider")).toBeUndefined();
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("returns provider by qualified ID", () => {
|
|
165
|
+
registry.register(
|
|
166
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
167
|
+
testPluginMetadata
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
const provider = registry.getProvider("test-plugin.webhook");
|
|
171
|
+
expect(provider?.displayName).toBe("Webhook Provider");
|
|
172
|
+
});
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
describe("hasProvider", () => {
|
|
176
|
+
it("returns false for non-existent provider", () => {
|
|
177
|
+
expect(registry.hasProvider("non-existent.provider")).toBe(false);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it("returns true for registered provider", () => {
|
|
181
|
+
registry.register(
|
|
182
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
183
|
+
testPluginMetadata
|
|
184
|
+
);
|
|
185
|
+
|
|
186
|
+
expect(registry.hasProvider("test-plugin.webhook")).toBe(true);
|
|
187
|
+
});
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
191
|
+
// Config Schema
|
|
192
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
193
|
+
|
|
194
|
+
describe("getProviderConfigSchema", () => {
|
|
195
|
+
it("returns undefined for non-existent provider", () => {
|
|
196
|
+
expect(
|
|
197
|
+
registry.getProviderConfigSchema("non-existent.provider")
|
|
198
|
+
).toBeUndefined();
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it("returns JSON Schema for provider config", () => {
|
|
202
|
+
registry.register(
|
|
203
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
204
|
+
testPluginMetadata
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
const schema = registry.getProviderConfigSchema("test-plugin.webhook");
|
|
208
|
+
expect(schema).toBeDefined();
|
|
209
|
+
expect(typeof schema).toBe("object");
|
|
210
|
+
expect(schema?.type).toBe("object");
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("JSON Schema includes property definitions", () => {
|
|
214
|
+
registry.register(
|
|
215
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
216
|
+
testPluginMetadata
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const schema = registry.getProviderConfigSchema("test-plugin.webhook");
|
|
220
|
+
const properties = schema?.properties as Record<string, unknown>;
|
|
221
|
+
|
|
222
|
+
expect(properties).toBeDefined();
|
|
223
|
+
expect(properties.url).toBeDefined();
|
|
224
|
+
expect(properties.method).toBeDefined();
|
|
225
|
+
expect(properties.timeout).toBeDefined();
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
230
|
+
// Multi-Plugin Registration
|
|
231
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
232
|
+
|
|
233
|
+
describe("multi-plugin registration", () => {
|
|
234
|
+
it("handles providers from multiple plugins", () => {
|
|
235
|
+
const plugin1 = { pluginId: "plugin-1" } as const;
|
|
236
|
+
const plugin2 = { pluginId: "plugin-2" } as const;
|
|
237
|
+
|
|
238
|
+
registry.register(
|
|
239
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
240
|
+
plugin1
|
|
241
|
+
);
|
|
242
|
+
registry.register(
|
|
243
|
+
createTestProvider("webhook", webhookConfigSchema),
|
|
244
|
+
plugin2
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
expect(registry.hasProvider("plugin-1.webhook")).toBe(true);
|
|
248
|
+
expect(registry.hasProvider("plugin-2.webhook")).toBe(true);
|
|
249
|
+
|
|
250
|
+
const providers = registry.getProviders();
|
|
251
|
+
expect(providers.length).toBe(2);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("correctly namespaces providers by plugin", () => {
|
|
255
|
+
const plugin1 = { pluginId: "integration-webhook" } as const;
|
|
256
|
+
const plugin2 = { pluginId: "integration-slack" } as const;
|
|
257
|
+
|
|
258
|
+
registry.register(
|
|
259
|
+
{
|
|
260
|
+
...createTestProvider("default", webhookConfigSchema),
|
|
261
|
+
displayName: "Webhook",
|
|
262
|
+
},
|
|
263
|
+
plugin1
|
|
264
|
+
);
|
|
265
|
+
registry.register(
|
|
266
|
+
{
|
|
267
|
+
...createTestProvider("default", slackConfigSchema),
|
|
268
|
+
displayName: "Slack",
|
|
269
|
+
},
|
|
270
|
+
plugin2
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
expect(
|
|
274
|
+
registry.getProvider("integration-webhook.default")?.displayName
|
|
275
|
+
).toBe("Webhook");
|
|
276
|
+
expect(
|
|
277
|
+
registry.getProvider("integration-slack.default")?.displayName
|
|
278
|
+
).toBe("Slack");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
283
|
+
// Supported Events
|
|
284
|
+
// ─────────────────────────────────────────────────────────────────────────
|
|
285
|
+
|
|
286
|
+
describe("supportedEvents", () => {
|
|
287
|
+
it("preserves supportedEvents array", () => {
|
|
288
|
+
const provider: IntegrationProvider<unknown> = {
|
|
289
|
+
id: "limited",
|
|
290
|
+
displayName: "Limited Provider",
|
|
291
|
+
config: new Versioned({ version: 1, schema: webhookConfigSchema }),
|
|
292
|
+
supportedEvents: ["incident.created", "incident.resolved"],
|
|
293
|
+
deliver: async () => ({ success: true }),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
registry.register(provider, testPluginMetadata);
|
|
297
|
+
|
|
298
|
+
const registered = registry.getProvider("test-plugin.limited");
|
|
299
|
+
expect(registered?.supportedEvents).toEqual([
|
|
300
|
+
"incident.created",
|
|
301
|
+
"incident.resolved",
|
|
302
|
+
]);
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
it("handles provider with no supportedEvents (accepts all)", () => {
|
|
306
|
+
const provider = createTestProvider("webhook", webhookConfigSchema);
|
|
307
|
+
|
|
308
|
+
registry.register(provider, testPluginMetadata);
|
|
309
|
+
|
|
310
|
+
const registered = registry.getProvider("test-plugin.webhook");
|
|
311
|
+
expect(registered?.supportedEvents).toBeUndefined();
|
|
312
|
+
});
|
|
313
|
+
});
|
|
314
|
+
});
|