@renseiai/agentfactory 0.8.12 → 0.8.13

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 (101) hide show
  1. package/dist/src/config/repository-config.d.ts +24 -0
  2. package/dist/src/config/repository-config.d.ts.map +1 -1
  3. package/dist/src/config/repository-config.js +21 -0
  4. package/dist/src/config/repository-config.test.js +202 -0
  5. package/dist/src/governor/decision-engine.d.ts +2 -0
  6. package/dist/src/governor/decision-engine.d.ts.map +1 -1
  7. package/dist/src/governor/decision-engine.js +7 -0
  8. package/dist/src/governor/decision-engine.test.js +63 -0
  9. package/dist/src/governor/governor-types.d.ts +2 -1
  10. package/dist/src/governor/governor-types.d.ts.map +1 -1
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -0
  14. package/dist/src/merge-queue/conflict-resolver.d.ts +62 -0
  15. package/dist/src/merge-queue/conflict-resolver.d.ts.map +1 -0
  16. package/dist/src/merge-queue/conflict-resolver.js +168 -0
  17. package/dist/src/merge-queue/conflict-resolver.test.d.ts +2 -0
  18. package/dist/src/merge-queue/conflict-resolver.test.d.ts.map +1 -0
  19. package/dist/src/merge-queue/conflict-resolver.test.js +405 -0
  20. package/dist/src/merge-queue/lock-file-regeneration.d.ts +14 -0
  21. package/dist/src/merge-queue/lock-file-regeneration.d.ts.map +1 -0
  22. package/dist/src/merge-queue/lock-file-regeneration.js +82 -0
  23. package/dist/src/merge-queue/lock-file-regeneration.test.d.ts +2 -0
  24. package/dist/src/merge-queue/lock-file-regeneration.test.d.ts.map +1 -0
  25. package/dist/src/merge-queue/lock-file-regeneration.test.js +236 -0
  26. package/dist/src/merge-queue/merge-worker.d.ts +79 -0
  27. package/dist/src/merge-queue/merge-worker.d.ts.map +1 -0
  28. package/dist/src/merge-queue/merge-worker.js +221 -0
  29. package/dist/src/merge-queue/merge-worker.test.d.ts +2 -0
  30. package/dist/src/merge-queue/merge-worker.test.d.ts.map +1 -0
  31. package/dist/src/merge-queue/merge-worker.test.js +883 -0
  32. package/dist/src/merge-queue/strategies/index.d.ts +19 -0
  33. package/dist/src/merge-queue/strategies/index.d.ts.map +1 -0
  34. package/dist/src/merge-queue/strategies/index.js +30 -0
  35. package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts +14 -0
  36. package/dist/src/merge-queue/strategies/merge-commit-strategy.d.ts.map +1 -0
  37. package/dist/src/merge-queue/strategies/merge-commit-strategy.js +58 -0
  38. package/dist/src/merge-queue/strategies/rebase-strategy.d.ts +14 -0
  39. package/dist/src/merge-queue/strategies/rebase-strategy.d.ts.map +1 -0
  40. package/dist/src/merge-queue/strategies/rebase-strategy.js +62 -0
  41. package/dist/src/merge-queue/strategies/squash-strategy.d.ts +14 -0
  42. package/dist/src/merge-queue/strategies/squash-strategy.d.ts.map +1 -0
  43. package/dist/src/merge-queue/strategies/squash-strategy.js +59 -0
  44. package/dist/src/merge-queue/strategies/strategies.test.d.ts +2 -0
  45. package/dist/src/merge-queue/strategies/strategies.test.d.ts.map +1 -0
  46. package/dist/src/merge-queue/strategies/strategies.test.js +354 -0
  47. package/dist/src/merge-queue/strategies/types.d.ts +62 -0
  48. package/dist/src/merge-queue/strategies/types.d.ts.map +1 -0
  49. package/dist/src/merge-queue/strategies/types.js +7 -0
  50. package/dist/src/orchestrator/parse-work-result.d.ts.map +1 -1
  51. package/dist/src/orchestrator/parse-work-result.js +22 -0
  52. package/dist/src/orchestrator/parse-work-result.test.js +49 -0
  53. package/dist/src/providers/index.d.ts +1 -0
  54. package/dist/src/providers/index.d.ts.map +1 -1
  55. package/dist/src/providers/plugin-types.d.ts +177 -0
  56. package/dist/src/providers/plugin-types.d.ts.map +1 -0
  57. package/dist/src/providers/plugin-types.js +10 -0
  58. package/dist/src/providers/plugin-types.test.d.ts +2 -0
  59. package/dist/src/providers/plugin-types.test.d.ts.map +1 -0
  60. package/dist/src/providers/plugin-types.test.js +810 -0
  61. package/dist/src/registry/index.d.ts +4 -0
  62. package/dist/src/registry/index.d.ts.map +1 -0
  63. package/dist/src/registry/index.js +2 -0
  64. package/dist/src/registry/loader.d.ts +25 -0
  65. package/dist/src/registry/loader.d.ts.map +1 -0
  66. package/dist/src/registry/loader.js +88 -0
  67. package/dist/src/registry/node-type-registry.d.ts +52 -0
  68. package/dist/src/registry/node-type-registry.d.ts.map +1 -0
  69. package/dist/src/registry/node-type-registry.js +130 -0
  70. package/dist/src/registry/types.d.ts +65 -0
  71. package/dist/src/registry/types.d.ts.map +1 -0
  72. package/dist/src/registry/types.js +10 -0
  73. package/dist/src/workflow/expression/ast.d.ts +1 -1
  74. package/dist/src/workflow/expression/ast.d.ts.map +1 -1
  75. package/dist/src/workflow/expression/context.d.ts +4 -0
  76. package/dist/src/workflow/expression/context.d.ts.map +1 -1
  77. package/dist/src/workflow/expression/context.js +5 -1
  78. package/dist/src/workflow/expression/evaluator.d.ts.map +1 -1
  79. package/dist/src/workflow/expression/evaluator.js +24 -1
  80. package/dist/src/workflow/expression/evaluator.test.js +174 -0
  81. package/dist/src/workflow/expression/expression.test.js +140 -1
  82. package/dist/src/workflow/expression/helpers.d.ts +4 -0
  83. package/dist/src/workflow/expression/helpers.d.ts.map +1 -1
  84. package/dist/src/workflow/expression/helpers.js +51 -0
  85. package/dist/src/workflow/expression/index.d.ts +14 -0
  86. package/dist/src/workflow/expression/index.d.ts.map +1 -1
  87. package/dist/src/workflow/expression/index.js +28 -1
  88. package/dist/src/workflow/expression/lexer.d.ts.map +1 -1
  89. package/dist/src/workflow/expression/lexer.js +43 -0
  90. package/dist/src/workflow/expression/parser.js +1 -1
  91. package/dist/src/workflow/index.d.ts +3 -3
  92. package/dist/src/workflow/index.d.ts.map +1 -1
  93. package/dist/src/workflow/index.js +4 -2
  94. package/dist/src/workflow/workflow-loader.d.ts +8 -2
  95. package/dist/src/workflow/workflow-loader.d.ts.map +1 -1
  96. package/dist/src/workflow/workflow-loader.js +21 -2
  97. package/dist/src/workflow/workflow-types.d.ts +781 -12
  98. package/dist/src/workflow/workflow-types.d.ts.map +1 -1
  99. package/dist/src/workflow/workflow-types.js +248 -3
  100. package/dist/src/workflow/workflow-types.test.js +621 -1
  101. package/package.json +3 -2
@@ -0,0 +1,810 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ // ---------------------------------------------------------------------------
3
+ // Helpers — reusable valid implementations of the core interfaces
4
+ // ---------------------------------------------------------------------------
5
+ function createValidAction(overrides) {
6
+ return {
7
+ id: 'create-issue',
8
+ displayName: 'Create Issue',
9
+ inputSchema: {
10
+ type: 'object',
11
+ properties: { title: { type: 'string' } },
12
+ required: ['title'],
13
+ },
14
+ outputSchema: {
15
+ type: 'object',
16
+ properties: { id: { type: 'string' } },
17
+ },
18
+ execute: async (_input, _context) => ({ success: true, data: { id: '123' } }),
19
+ ...overrides,
20
+ };
21
+ }
22
+ function createValidTrigger(overrides) {
23
+ return {
24
+ id: 'issue-created',
25
+ displayName: 'Issue Created',
26
+ eventType: 'issue.created',
27
+ filterSchema: {
28
+ type: 'object',
29
+ properties: { project: { type: 'string' } },
30
+ },
31
+ normalizeEvent: (rawEvent) => ({
32
+ id: 'evt-001',
33
+ type: 'issue.created',
34
+ timestamp: new Date('2025-01-15T12:00:00Z'),
35
+ data: { project: 'backend' },
36
+ raw: rawEvent,
37
+ }),
38
+ registerWebhook: async (_config) => ({
39
+ id: 'wh-001',
40
+ url: 'https://hooks.example.com/abc',
41
+ }),
42
+ deregisterWebhook: async (_registration) => { },
43
+ ...overrides,
44
+ };
45
+ }
46
+ function createValidCondition(overrides) {
47
+ return {
48
+ id: 'is-high-priority',
49
+ displayName: 'Is High Priority',
50
+ evaluationSchema: {
51
+ type: 'object',
52
+ properties: { priority: { type: 'number' } },
53
+ },
54
+ evaluate: async (params, _context) => params.priority >= 3,
55
+ ...overrides,
56
+ };
57
+ }
58
+ function createValidCredential(overrides) {
59
+ return {
60
+ id: 'oauth2-creds',
61
+ authType: 'oauth2',
62
+ requiredFields: [
63
+ { name: 'clientId', label: 'Client ID', type: 'string' },
64
+ { name: 'clientSecret', label: 'Client Secret', type: 'password', secret: true },
65
+ ],
66
+ authenticate: async (credentials) => {
67
+ if (credentials.clientId && credentials.clientSecret) {
68
+ return { valid: true, metadata: { scope: 'read write' } };
69
+ }
70
+ return { valid: false, error: 'Missing credentials' };
71
+ },
72
+ ...overrides,
73
+ };
74
+ }
75
+ function createValidPlugin(overrides) {
76
+ return {
77
+ id: 'github',
78
+ displayName: 'GitHub',
79
+ description: 'GitHub integration for issues, PRs, and repos',
80
+ version: '1.0.0',
81
+ icon: 'github-icon',
82
+ actions: [createValidAction()],
83
+ triggers: [createValidTrigger()],
84
+ conditions: [createValidCondition()],
85
+ credentials: [createValidCredential()],
86
+ ...overrides,
87
+ };
88
+ }
89
+ // ===========================================================================
90
+ // Tests
91
+ // ===========================================================================
92
+ describe('Provider Plugin Type System', () => {
93
+ // -------------------------------------------------------------------------
94
+ // 1. Interface Shape Tests
95
+ // -------------------------------------------------------------------------
96
+ describe('Interface Shape Tests', () => {
97
+ describe('ActionDefinition', () => {
98
+ it('accepts a valid action with all required fields', () => {
99
+ const action = createValidAction();
100
+ expect(action).toBeDefined();
101
+ expect(action.id).toBe('create-issue');
102
+ expect(action.displayName).toBe('Create Issue');
103
+ expect(action.inputSchema).toBeDefined();
104
+ expect(action.outputSchema).toBeDefined();
105
+ expect(typeof action.execute).toBe('function');
106
+ });
107
+ it('accepts optional description and category fields', () => {
108
+ const action = createValidAction({
109
+ description: 'Creates a new issue in the tracker',
110
+ category: 'Issues',
111
+ });
112
+ expect(action.description).toBe('Creates a new issue in the tracker');
113
+ expect(action.category).toBe('Issues');
114
+ });
115
+ it('accepts dynamicOptions field', () => {
116
+ const dynamicOpts = [
117
+ { fieldPath: 'projectId', dependsOn: ['orgId'] },
118
+ { fieldPath: 'labelId' },
119
+ ];
120
+ const action = createValidAction({
121
+ dynamicOptions: dynamicOpts,
122
+ });
123
+ expect(action.dynamicOptions).toHaveLength(2);
124
+ expect(action.dynamicOptions[0].fieldPath).toBe('projectId');
125
+ expect(action.dynamicOptions[0].dependsOn).toEqual(['orgId']);
126
+ expect(action.dynamicOptions[1].dependsOn).toBeUndefined();
127
+ });
128
+ });
129
+ describe('TriggerDefinition', () => {
130
+ it('accepts a valid trigger with all required fields', () => {
131
+ const trigger = createValidTrigger();
132
+ expect(trigger).toBeDefined();
133
+ expect(trigger.id).toBe('issue-created');
134
+ expect(trigger.displayName).toBe('Issue Created');
135
+ expect(trigger.eventType).toBe('issue.created');
136
+ expect(trigger.filterSchema).toBeDefined();
137
+ expect(typeof trigger.normalizeEvent).toBe('function');
138
+ expect(typeof trigger.registerWebhook).toBe('function');
139
+ expect(typeof trigger.deregisterWebhook).toBe('function');
140
+ });
141
+ it('accepts optional description field', () => {
142
+ const trigger = createValidTrigger({
143
+ description: 'Fires when a new issue is created',
144
+ });
145
+ expect(trigger.description).toBe('Fires when a new issue is created');
146
+ });
147
+ });
148
+ describe('ConditionDefinition', () => {
149
+ it('accepts a valid condition with all required fields', () => {
150
+ const condition = createValidCondition();
151
+ expect(condition).toBeDefined();
152
+ expect(condition.id).toBe('is-high-priority');
153
+ expect(condition.displayName).toBe('Is High Priority');
154
+ expect(condition.evaluationSchema).toBeDefined();
155
+ expect(typeof condition.evaluate).toBe('function');
156
+ });
157
+ it('accepts optional description field', () => {
158
+ const condition = createValidCondition({
159
+ description: 'Checks if an issue is marked as high priority',
160
+ });
161
+ expect(condition.description).toBe('Checks if an issue is marked as high priority');
162
+ });
163
+ });
164
+ describe('CredentialDefinition', () => {
165
+ it('accepts a valid credential with all required fields', () => {
166
+ const cred = createValidCredential();
167
+ expect(cred).toBeDefined();
168
+ expect(cred.id).toBe('oauth2-creds');
169
+ expect(cred.authType).toBe('oauth2');
170
+ expect(cred.requiredFields).toHaveLength(2);
171
+ expect(typeof cred.authenticate).toBe('function');
172
+ });
173
+ it('accepts credential fields with all properties', () => {
174
+ const field = {
175
+ name: 'apiKey',
176
+ label: 'API Key',
177
+ type: 'password',
178
+ secret: true,
179
+ description: 'Your provider API key',
180
+ };
181
+ expect(field).toBeDefined();
182
+ expect(field.secret).toBe(true);
183
+ expect(field.description).toBe('Your provider API key');
184
+ });
185
+ it('accepts credential fields with only required properties', () => {
186
+ const field = {
187
+ name: 'endpoint',
188
+ label: 'Endpoint URL',
189
+ type: 'url',
190
+ };
191
+ expect(field).toBeDefined();
192
+ expect(field.secret).toBeUndefined();
193
+ expect(field.description).toBeUndefined();
194
+ });
195
+ });
196
+ describe('ProviderPlugin', () => {
197
+ it('accepts a valid plugin with all required fields', () => {
198
+ const plugin = createValidPlugin();
199
+ expect(plugin).toBeDefined();
200
+ expect(plugin.id).toBe('github');
201
+ expect(plugin.displayName).toBe('GitHub');
202
+ expect(plugin.version).toBe('1.0.0');
203
+ expect(plugin.actions).toHaveLength(1);
204
+ expect(plugin.triggers).toHaveLength(1);
205
+ expect(plugin.conditions).toHaveLength(1);
206
+ expect(plugin.credentials).toHaveLength(1);
207
+ });
208
+ it('accepts optional description and icon fields', () => {
209
+ const plugin = createValidPlugin();
210
+ expect(plugin.description).toBe('GitHub integration for issues, PRs, and repos');
211
+ expect(plugin.icon).toBe('github-icon');
212
+ });
213
+ it('accepts a plugin without optional fields', () => {
214
+ const plugin = createValidPlugin({
215
+ description: undefined,
216
+ icon: undefined,
217
+ });
218
+ expect(plugin.description).toBeUndefined();
219
+ expect(plugin.icon).toBeUndefined();
220
+ });
221
+ });
222
+ describe('Supporting Types', () => {
223
+ it('ProviderExecutionContext has required credentials and optional env', () => {
224
+ const ctx = {
225
+ credentials: { token: 'abc123' },
226
+ };
227
+ expect(ctx.credentials).toEqual({ token: 'abc123' });
228
+ expect(ctx.env).toBeUndefined();
229
+ const ctxWithEnv = {
230
+ credentials: { token: 'abc123' },
231
+ env: { DEBUG: 'true' },
232
+ };
233
+ expect(ctxWithEnv.env).toEqual({ DEBUG: 'true' });
234
+ });
235
+ it('ActionResult has required success and optional data/error', () => {
236
+ const successResult = { success: true, data: { id: '42' } };
237
+ expect(successResult.success).toBe(true);
238
+ expect(successResult.data).toEqual({ id: '42' });
239
+ expect(successResult.error).toBeUndefined();
240
+ const failResult = { success: false, error: 'Not found' };
241
+ expect(failResult.success).toBe(false);
242
+ expect(failResult.error).toBe('Not found');
243
+ expect(failResult.data).toBeUndefined();
244
+ });
245
+ it('NormalizedEvent has all required fields', () => {
246
+ const evt = {
247
+ id: 'evt-001',
248
+ type: 'message.posted',
249
+ timestamp: new Date('2025-01-15T12:00:00Z'),
250
+ data: { channel: 'general', text: 'hello' },
251
+ raw: { original: true },
252
+ };
253
+ expect(evt.id).toBe('evt-001');
254
+ expect(evt.type).toBe('message.posted');
255
+ expect(evt.timestamp).toBeInstanceOf(Date);
256
+ expect(evt.data).toHaveProperty('channel');
257
+ expect(evt.raw).toEqual({ original: true });
258
+ });
259
+ it('WebhookConfig has required url and optional secret/events', () => {
260
+ const minimal = { url: 'https://hooks.example.com/abc' };
261
+ expect(minimal.url).toBe('https://hooks.example.com/abc');
262
+ expect(minimal.secret).toBeUndefined();
263
+ expect(minimal.events).toBeUndefined();
264
+ const full = {
265
+ url: 'https://hooks.example.com/abc',
266
+ secret: 'whsec_123',
267
+ events: ['push', 'pull_request'],
268
+ };
269
+ expect(full.secret).toBe('whsec_123');
270
+ expect(full.events).toEqual(['push', 'pull_request']);
271
+ });
272
+ it('WebhookRegistration has required id/url and optional metadata', () => {
273
+ const minimal = { id: 'wh-001', url: 'https://hooks.example.com/abc' };
274
+ expect(minimal.metadata).toBeUndefined();
275
+ const full = {
276
+ id: 'wh-001',
277
+ url: 'https://hooks.example.com/abc',
278
+ metadata: { createdAt: '2025-01-15' },
279
+ };
280
+ expect(full.metadata).toEqual({ createdAt: '2025-01-15' });
281
+ });
282
+ it('AuthResult has required valid and optional error/metadata', () => {
283
+ const success = { valid: true, metadata: { userId: 'u1' } };
284
+ expect(success.valid).toBe(true);
285
+ expect(success.error).toBeUndefined();
286
+ const failure = { valid: false, error: 'Invalid token' };
287
+ expect(failure.valid).toBe(false);
288
+ expect(failure.error).toBe('Invalid token');
289
+ });
290
+ it('ConditionContext has optional triggerEvent and stepOutputs', () => {
291
+ const empty = {};
292
+ expect(empty.triggerEvent).toBeUndefined();
293
+ expect(empty.stepOutputs).toBeUndefined();
294
+ const withEvent = {
295
+ triggerEvent: {
296
+ id: 'evt-001',
297
+ type: 'issue.created',
298
+ timestamp: new Date(),
299
+ data: {},
300
+ raw: null,
301
+ },
302
+ stepOutputs: { step1: { result: 'ok' } },
303
+ };
304
+ expect(withEvent.triggerEvent).toBeDefined();
305
+ expect(withEvent.stepOutputs).toHaveProperty('step1');
306
+ });
307
+ it('DynamicOptionField has required fieldPath and optional dependsOn', () => {
308
+ const simple = { fieldPath: 'project' };
309
+ expect(simple.fieldPath).toBe('project');
310
+ expect(simple.dependsOn).toBeUndefined();
311
+ const withDeps = {
312
+ fieldPath: 'repository',
313
+ dependsOn: ['organization', 'team'],
314
+ };
315
+ expect(withDeps.dependsOn).toEqual(['organization', 'team']);
316
+ });
317
+ });
318
+ });
319
+ // -------------------------------------------------------------------------
320
+ // 2. JSON Schema 7 Compatibility
321
+ // -------------------------------------------------------------------------
322
+ describe('JSON Schema 7 Compatibility', () => {
323
+ it('accepts simple type schemas', () => {
324
+ const action = createValidAction({
325
+ inputSchema: { type: 'string' },
326
+ outputSchema: { type: 'number' },
327
+ });
328
+ expect(action.inputSchema).toEqual({ type: 'string' });
329
+ expect(action.outputSchema).toEqual({ type: 'number' });
330
+ });
331
+ it('accepts object schemas with properties and required', () => {
332
+ const action = createValidAction({
333
+ inputSchema: {
334
+ type: 'object',
335
+ properties: {
336
+ title: { type: 'string', minLength: 1 },
337
+ description: { type: 'string' },
338
+ priority: { type: 'integer', minimum: 1, maximum: 5 },
339
+ },
340
+ required: ['title'],
341
+ additionalProperties: false,
342
+ },
343
+ outputSchema: {
344
+ type: 'object',
345
+ properties: {
346
+ id: { type: 'string' },
347
+ createdAt: { type: 'string', format: 'date-time' },
348
+ },
349
+ },
350
+ });
351
+ expect(action.inputSchema.type).toBe('object');
352
+ expect(action.inputSchema.required).toEqual(['title']);
353
+ });
354
+ it('accepts array schemas with items', () => {
355
+ const action = createValidAction({
356
+ inputSchema: {
357
+ type: 'object',
358
+ properties: {
359
+ tags: {
360
+ type: 'array',
361
+ items: { type: 'string' },
362
+ minItems: 1,
363
+ },
364
+ },
365
+ },
366
+ outputSchema: {
367
+ type: 'array',
368
+ items: {
369
+ type: 'object',
370
+ properties: {
371
+ id: { type: 'string' },
372
+ name: { type: 'string' },
373
+ },
374
+ },
375
+ },
376
+ });
377
+ expect(action.inputSchema.type).toBe('object');
378
+ expect(action.outputSchema.type).toBe('array');
379
+ });
380
+ it('accepts schemas with enum values', () => {
381
+ const action = createValidAction({
382
+ inputSchema: {
383
+ type: 'object',
384
+ properties: {
385
+ status: { type: 'string', enum: ['open', 'closed', 'in_progress'] },
386
+ },
387
+ },
388
+ outputSchema: { type: 'string' },
389
+ });
390
+ const statusProp = action.inputSchema.properties?.status;
391
+ expect(statusProp?.enum).toEqual(['open', 'closed', 'in_progress']);
392
+ });
393
+ it('accepts schemas with oneOf', () => {
394
+ const action = createValidAction({
395
+ inputSchema: {
396
+ oneOf: [
397
+ { type: 'object', properties: { issueId: { type: 'string' } }, required: ['issueId'] },
398
+ { type: 'object', properties: { issueKey: { type: 'string' } }, required: ['issueKey'] },
399
+ ],
400
+ },
401
+ outputSchema: { type: 'object' },
402
+ });
403
+ expect(action.inputSchema.oneOf).toHaveLength(2);
404
+ });
405
+ it('accepts schemas with allOf', () => {
406
+ const condition = createValidCondition({
407
+ evaluationSchema: {
408
+ allOf: [
409
+ { type: 'object', properties: { priority: { type: 'number' } } },
410
+ { type: 'object', properties: { status: { type: 'string' } } },
411
+ ],
412
+ },
413
+ });
414
+ expect(condition.evaluationSchema.allOf).toHaveLength(2);
415
+ });
416
+ it('accepts schemas on trigger filterSchema', () => {
417
+ const trigger = createValidTrigger({
418
+ filterSchema: {
419
+ type: 'object',
420
+ properties: {
421
+ project: { type: 'string' },
422
+ labels: {
423
+ type: 'array',
424
+ items: { type: 'string' },
425
+ },
426
+ },
427
+ required: ['project'],
428
+ },
429
+ });
430
+ expect(trigger.filterSchema.type).toBe('object');
431
+ expect(trigger.filterSchema.required).toEqual(['project']);
432
+ });
433
+ it('accepts schemas on condition evaluationSchema', () => {
434
+ const condition = createValidCondition({
435
+ evaluationSchema: {
436
+ type: 'object',
437
+ properties: {
438
+ threshold: { type: 'number', minimum: 0, maximum: 100 },
439
+ mode: { type: 'string', enum: ['strict', 'lenient'] },
440
+ },
441
+ },
442
+ });
443
+ expect(condition.evaluationSchema.type).toBe('object');
444
+ });
445
+ });
446
+ // -------------------------------------------------------------------------
447
+ // 3. ProviderPlugin Composition
448
+ // -------------------------------------------------------------------------
449
+ describe('ProviderPlugin Composition', () => {
450
+ it('accepts a plugin with all definition types populated', () => {
451
+ const plugin = createValidPlugin({
452
+ actions: [
453
+ createValidAction({ id: 'create-issue', displayName: 'Create Issue' }),
454
+ createValidAction({ id: 'update-issue', displayName: 'Update Issue' }),
455
+ createValidAction({ id: 'close-issue', displayName: 'Close Issue' }),
456
+ ],
457
+ triggers: [
458
+ createValidTrigger({ id: 'issue-created', displayName: 'Issue Created', eventType: 'issue.created' }),
459
+ createValidTrigger({ id: 'pr-merged', displayName: 'PR Merged', eventType: 'pr.merged' }),
460
+ ],
461
+ conditions: [
462
+ createValidCondition({ id: 'is-high-priority', displayName: 'Is High Priority' }),
463
+ createValidCondition({ id: 'is-assigned', displayName: 'Is Assigned' }),
464
+ ],
465
+ credentials: [
466
+ createValidCredential({ id: 'oauth2', authType: 'oauth2' }),
467
+ createValidCredential({ id: 'api-key', authType: 'apiKey' }),
468
+ ],
469
+ });
470
+ expect(plugin.actions).toHaveLength(3);
471
+ expect(plugin.triggers).toHaveLength(2);
472
+ expect(plugin.conditions).toHaveLength(2);
473
+ expect(plugin.credentials).toHaveLength(2);
474
+ });
475
+ it('accepts a plugin with empty arrays for all definition types', () => {
476
+ const plugin = createValidPlugin({
477
+ actions: [],
478
+ triggers: [],
479
+ conditions: [],
480
+ credentials: [],
481
+ });
482
+ expect(plugin.actions).toHaveLength(0);
483
+ expect(plugin.triggers).toHaveLength(0);
484
+ expect(plugin.conditions).toHaveLength(0);
485
+ expect(plugin.credentials).toHaveLength(0);
486
+ });
487
+ it('allows a plugin with mixed populated and empty definition arrays', () => {
488
+ const plugin = createValidPlugin({
489
+ actions: [createValidAction()],
490
+ triggers: [],
491
+ conditions: [createValidCondition()],
492
+ credentials: [],
493
+ });
494
+ expect(plugin.actions).toHaveLength(1);
495
+ expect(plugin.triggers).toHaveLength(0);
496
+ expect(plugin.conditions).toHaveLength(1);
497
+ expect(plugin.credentials).toHaveLength(0);
498
+ });
499
+ it('maintains correct action ids within a plugin', () => {
500
+ const plugin = createValidPlugin({
501
+ actions: [
502
+ createValidAction({ id: 'action-a' }),
503
+ createValidAction({ id: 'action-b' }),
504
+ ],
505
+ });
506
+ const ids = plugin.actions.map((a) => a.id);
507
+ expect(ids).toEqual(['action-a', 'action-b']);
508
+ });
509
+ });
510
+ // -------------------------------------------------------------------------
511
+ // 4. Execute/Evaluate Contract Tests (Runtime)
512
+ // -------------------------------------------------------------------------
513
+ describe('Execute/Evaluate Contract Tests', () => {
514
+ describe('ActionDefinition.execute()', () => {
515
+ it('returns ActionResult with success: true and data on success', async () => {
516
+ const action = createValidAction({
517
+ execute: async (input, _context) => ({
518
+ success: true,
519
+ data: { id: 'new-123', title: input.title },
520
+ }),
521
+ });
522
+ const context = { credentials: { token: 'test-token' } };
523
+ const result = await action.execute({ title: 'Test Issue' }, context);
524
+ expect(result.success).toBe(true);
525
+ expect(result.data).toEqual({ id: 'new-123', title: 'Test Issue' });
526
+ expect(result.error).toBeUndefined();
527
+ });
528
+ it('returns ActionResult with success: false and error on failure', async () => {
529
+ const action = createValidAction({
530
+ execute: async (_input, _context) => ({
531
+ success: false,
532
+ error: 'Permission denied',
533
+ }),
534
+ });
535
+ const context = { credentials: { token: 'bad-token' } };
536
+ const result = await action.execute({}, context);
537
+ expect(result.success).toBe(false);
538
+ expect(result.error).toBe('Permission denied');
539
+ expect(result.data).toBeUndefined();
540
+ });
541
+ it('receives input and context arguments correctly', async () => {
542
+ let capturedInput = {};
543
+ let capturedContext = { credentials: {} };
544
+ const action = createValidAction({
545
+ execute: async (input, context) => {
546
+ capturedInput = input;
547
+ capturedContext = context;
548
+ return { success: true };
549
+ },
550
+ });
551
+ const input = { title: 'Bug Report', priority: 1 };
552
+ const context = {
553
+ credentials: { apiKey: 'key-123' },
554
+ env: { REGION: 'us-east-1' },
555
+ };
556
+ await action.execute(input, context);
557
+ expect(capturedInput).toEqual(input);
558
+ expect(capturedContext.credentials).toEqual({ apiKey: 'key-123' });
559
+ expect(capturedContext.env).toEqual({ REGION: 'us-east-1' });
560
+ });
561
+ });
562
+ describe('TriggerDefinition.normalizeEvent()', () => {
563
+ it('returns NormalizedEvent with all required fields', () => {
564
+ const trigger = createValidTrigger();
565
+ const rawEvent = { action: 'created', issue: { id: 42 } };
566
+ const result = trigger.normalizeEvent(rawEvent);
567
+ expect(result.id).toBe('evt-001');
568
+ expect(result.type).toBe('issue.created');
569
+ expect(result.timestamp).toBeInstanceOf(Date);
570
+ expect(result.data).toBeDefined();
571
+ expect(result.raw).toBe(rawEvent);
572
+ });
573
+ it('preserves the raw event as-is', () => {
574
+ const trigger = createValidTrigger({
575
+ normalizeEvent: (raw) => ({
576
+ id: 'evt-002',
577
+ type: 'pr.opened',
578
+ timestamp: new Date(),
579
+ data: { number: 99 },
580
+ raw,
581
+ }),
582
+ });
583
+ const rawEvent = { action: 'opened', pull_request: { number: 99, title: 'Fix bug' } };
584
+ const result = trigger.normalizeEvent(rawEvent);
585
+ expect(result.raw).toBe(rawEvent);
586
+ expect(result.data).toEqual({ number: 99 });
587
+ });
588
+ });
589
+ describe('TriggerDefinition.registerWebhook()', () => {
590
+ it('returns WebhookRegistration with id and url', async () => {
591
+ const trigger = createValidTrigger({
592
+ registerWebhook: async (config) => ({
593
+ id: 'wh-999',
594
+ url: config.url,
595
+ metadata: { events: config.events },
596
+ }),
597
+ });
598
+ const config = {
599
+ url: 'https://hooks.example.com/ingest',
600
+ secret: 'whsec_test',
601
+ events: ['push', 'issue'],
602
+ };
603
+ const registration = await trigger.registerWebhook(config);
604
+ expect(registration.id).toBe('wh-999');
605
+ expect(registration.url).toBe('https://hooks.example.com/ingest');
606
+ expect(registration.metadata).toEqual({ events: ['push', 'issue'] });
607
+ });
608
+ });
609
+ describe('TriggerDefinition.deregisterWebhook()', () => {
610
+ it('completes without error given a valid registration', async () => {
611
+ let capturedRegistration;
612
+ const trigger = createValidTrigger({
613
+ deregisterWebhook: async (registration) => {
614
+ capturedRegistration = registration;
615
+ },
616
+ });
617
+ const registration = {
618
+ id: 'wh-001',
619
+ url: 'https://hooks.example.com/abc',
620
+ };
621
+ await trigger.deregisterWebhook(registration);
622
+ expect(capturedRegistration).toEqual(registration);
623
+ });
624
+ });
625
+ describe('ConditionDefinition.evaluate()', () => {
626
+ it('returns true when condition is met', async () => {
627
+ const condition = createValidCondition();
628
+ const result = await condition.evaluate({ priority: 5 }, {});
629
+ expect(result).toBe(true);
630
+ });
631
+ it('returns false when condition is not met', async () => {
632
+ const condition = createValidCondition();
633
+ const result = await condition.evaluate({ priority: 1 }, {});
634
+ expect(result).toBe(false);
635
+ });
636
+ it('receives ConditionContext with triggerEvent and stepOutputs', async () => {
637
+ let capturedContext = {};
638
+ const condition = createValidCondition({
639
+ evaluate: async (_params, context) => {
640
+ capturedContext = context;
641
+ return true;
642
+ },
643
+ });
644
+ const ctx = {
645
+ triggerEvent: {
646
+ id: 'evt-001',
647
+ type: 'issue.created',
648
+ timestamp: new Date('2025-01-15T12:00:00Z'),
649
+ data: { priority: 5 },
650
+ raw: {},
651
+ },
652
+ stepOutputs: {
653
+ 'step-1': { assignee: 'alice' },
654
+ },
655
+ };
656
+ await condition.evaluate({}, ctx);
657
+ expect(capturedContext.triggerEvent).toBeDefined();
658
+ expect(capturedContext.triggerEvent.id).toBe('evt-001');
659
+ expect(capturedContext.stepOutputs).toHaveProperty('step-1');
660
+ });
661
+ });
662
+ describe('CredentialDefinition.authenticate()', () => {
663
+ it('returns AuthResult with valid: true on success', async () => {
664
+ const cred = createValidCredential();
665
+ const result = await cred.authenticate({
666
+ clientId: 'id-123',
667
+ clientSecret: 'secret-456',
668
+ });
669
+ expect(result.valid).toBe(true);
670
+ expect(result.error).toBeUndefined();
671
+ expect(result.metadata).toEqual({ scope: 'read write' });
672
+ });
673
+ it('returns AuthResult with valid: false and error on failure', async () => {
674
+ const cred = createValidCredential();
675
+ const result = await cred.authenticate({
676
+ clientId: '',
677
+ clientSecret: '',
678
+ });
679
+ expect(result.valid).toBe(false);
680
+ expect(result.error).toBe('Missing credentials');
681
+ });
682
+ it('returns AuthResult with metadata on success', async () => {
683
+ const cred = createValidCredential({
684
+ authenticate: async (_credentials) => ({
685
+ valid: true,
686
+ metadata: { userId: 'u-1', org: 'acme', expiresIn: 3600 },
687
+ }),
688
+ });
689
+ const result = await cred.authenticate({ token: 'valid-token' });
690
+ expect(result.valid).toBe(true);
691
+ expect(result.metadata).toHaveProperty('userId');
692
+ expect(result.metadata).toHaveProperty('org');
693
+ expect(result.metadata).toHaveProperty('expiresIn');
694
+ });
695
+ });
696
+ });
697
+ // -------------------------------------------------------------------------
698
+ // 5. Auth Type Variant Tests
699
+ // -------------------------------------------------------------------------
700
+ describe('Auth Type Variant Tests', () => {
701
+ it('accepts oauth2 credential definition with client ID and client secret', () => {
702
+ const cred = createValidCredential({
703
+ id: 'github-oauth2',
704
+ authType: 'oauth2',
705
+ requiredFields: [
706
+ { name: 'clientId', label: 'Client ID', type: 'string' },
707
+ { name: 'clientSecret', label: 'Client Secret', type: 'password', secret: true },
708
+ ],
709
+ });
710
+ expect(cred.authType).toBe('oauth2');
711
+ expect(cred.requiredFields).toHaveLength(2);
712
+ const clientIdField = cred.requiredFields.find((f) => f.name === 'clientId');
713
+ expect(clientIdField).toBeDefined();
714
+ expect(clientIdField.type).toBe('string');
715
+ const secretField = cred.requiredFields.find((f) => f.name === 'clientSecret');
716
+ expect(secretField).toBeDefined();
717
+ expect(secretField.type).toBe('password');
718
+ expect(secretField.secret).toBe(true);
719
+ });
720
+ it('accepts apiKey credential definition with secret API key field', () => {
721
+ const cred = createValidCredential({
722
+ id: 'openai-api-key',
723
+ authType: 'apiKey',
724
+ requiredFields: [
725
+ {
726
+ name: 'apiKey',
727
+ label: 'API Key',
728
+ type: 'password',
729
+ secret: true,
730
+ description: 'Your OpenAI API key starting with sk-',
731
+ },
732
+ ],
733
+ });
734
+ expect(cred.authType).toBe('apiKey');
735
+ expect(cred.requiredFields).toHaveLength(1);
736
+ const apiKeyField = cred.requiredFields[0];
737
+ expect(apiKeyField.name).toBe('apiKey');
738
+ expect(apiKeyField.secret).toBe(true);
739
+ expect(apiKeyField.description).toContain('sk-');
740
+ });
741
+ it('accepts bearer credential definition with bearer token field', () => {
742
+ const cred = createValidCredential({
743
+ id: 'slack-bearer',
744
+ authType: 'bearer',
745
+ requiredFields: [
746
+ {
747
+ name: 'bearerToken',
748
+ label: 'Bearer Token',
749
+ type: 'password',
750
+ secret: true,
751
+ description: 'OAuth bearer token for Slack API',
752
+ },
753
+ ],
754
+ });
755
+ expect(cred.authType).toBe('bearer');
756
+ expect(cred.requiredFields).toHaveLength(1);
757
+ const tokenField = cred.requiredFields[0];
758
+ expect(tokenField.name).toBe('bearerToken');
759
+ expect(tokenField.type).toBe('password');
760
+ expect(tokenField.secret).toBe(true);
761
+ });
762
+ it('each auth type can successfully authenticate', async () => {
763
+ const oauth2 = createValidCredential({
764
+ authType: 'oauth2',
765
+ authenticate: async () => ({ valid: true }),
766
+ });
767
+ const apiKey = createValidCredential({
768
+ authType: 'apiKey',
769
+ authenticate: async () => ({ valid: true }),
770
+ });
771
+ const bearer = createValidCredential({
772
+ authType: 'bearer',
773
+ authenticate: async () => ({ valid: true }),
774
+ });
775
+ const [oauth2Result, apiKeyResult, bearerResult] = await Promise.all([
776
+ oauth2.authenticate({ clientId: 'id', clientSecret: 'secret' }),
777
+ apiKey.authenticate({ apiKey: 'sk-test' }),
778
+ bearer.authenticate({ bearerToken: 'xoxb-test' }),
779
+ ]);
780
+ expect(oauth2Result.valid).toBe(true);
781
+ expect(apiKeyResult.valid).toBe(true);
782
+ expect(bearerResult.valid).toBe(true);
783
+ });
784
+ it('each auth type can return authentication failure', async () => {
785
+ const oauth2 = createValidCredential({
786
+ authType: 'oauth2',
787
+ authenticate: async () => ({ valid: false, error: 'Invalid client credentials' }),
788
+ });
789
+ const apiKey = createValidCredential({
790
+ authType: 'apiKey',
791
+ authenticate: async () => ({ valid: false, error: 'API key revoked' }),
792
+ });
793
+ const bearer = createValidCredential({
794
+ authType: 'bearer',
795
+ authenticate: async () => ({ valid: false, error: 'Token expired' }),
796
+ });
797
+ const [oauth2Result, apiKeyResult, bearerResult] = await Promise.all([
798
+ oauth2.authenticate({}),
799
+ apiKey.authenticate({}),
800
+ bearer.authenticate({}),
801
+ ]);
802
+ expect(oauth2Result.valid).toBe(false);
803
+ expect(oauth2Result.error).toBe('Invalid client credentials');
804
+ expect(apiKeyResult.valid).toBe(false);
805
+ expect(apiKeyResult.error).toBe('API key revoked');
806
+ expect(bearerResult.valid).toBe(false);
807
+ expect(bearerResult.error).toBe('Token expired');
808
+ });
809
+ });
810
+ });