arvo-event-handler 3.0.16 → 3.0.18

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.
@@ -44,4 +44,4 @@ export type EventValidationConfig = {
44
44
  * - URI and version compatibility checks
45
45
  * - Schema-based data validation
46
46
  */
47
- export declare function validateInputEvent({ event, selfContract: _selfContract, serviceContracts, span, }: EventValidationConfig): EventValidationResult;
47
+ export declare function validateInputEvent({ event, selfContract, serviceContracts, span, }: EventValidationConfig): EventValidationResult;
@@ -22,7 +22,7 @@ var arvo_core_1 = require("arvo-core");
22
22
  */
23
23
  function validateInputEvent(_a) {
24
24
  var _b;
25
- var event = _a.event, _selfContract = _a.selfContract, serviceContracts = _a.serviceContracts, span = _a.span;
25
+ var event = _a.event, selfContract = _a.selfContract, serviceContracts = _a.serviceContracts, span = _a.span;
26
26
  var resolvedContract = null;
27
27
  var contractType;
28
28
  var parsedEventDataSchema = arvo_core_1.EventDataschemaUtil.parse(event);
@@ -37,26 +37,13 @@ function validateInputEvent(_a) {
37
37
  error: new Error(errorMessage),
38
38
  };
39
39
  }
40
- var selfContract;
41
- if (_selfContract instanceof arvo_core_1.VersionedArvoContract) {
42
- selfContract = _selfContract;
43
- }
44
- else {
45
- if (!_selfContract.versions[parsedEventDataSchema.version]) {
46
- var errorMessage = "Contract resolution failed: No matching contract found for event (id='".concat(event.id, "', type='").concat(event.type, "', dataschema='").concat(event.dataschema, "')");
47
- (0, arvo_core_1.logToSpan)({
48
- level: 'WARNING',
49
- message: errorMessage,
50
- }, span);
51
- return {
52
- type: 'CONTRACT_UNRESOLVED',
53
- };
54
- }
55
- selfContract = _selfContract.version(parsedEventDataSchema.version);
56
- }
57
- if (event.type === selfContract.accepts.type) {
40
+ var selfType = selfContract instanceof arvo_core_1.VersionedArvoContract ? selfContract.accepts.type : selfContract.type;
41
+ if (event.type === selfType) {
58
42
  contractType = 'self';
59
- resolvedContract = selfContract;
43
+ resolvedContract =
44
+ selfContract instanceof arvo_core_1.VersionedArvoContract
45
+ ? selfContract
46
+ : selfContract.version(parsedEventDataSchema.version);
60
47
  }
61
48
  else {
62
49
  contractType = 'service';
package/dist/index.d.ts CHANGED
@@ -28,8 +28,10 @@ import { ArvoEventHandlerOpenTelemetryOptions, ArvoEventHandlerOtelSpanOptions,
28
28
  import { coalesce, coalesceOrDefault, getValueOrDefault, isNullOrUndefined } from './utils';
29
29
  import { SimpleEventBroker } from './utils/SimpleEventBroker';
30
30
  import { createSimpleEventBroker } from './utils/SimpleEventBroker/helper';
31
+ import { runArvoTestSuites } from './runArvoTestSuites';
32
+ import { ArvoTestStep, ArvoTestCase, ArvoTestConfig, ArvoTestSuite, ArvoTestResult, IArvoTestFramework } from './runArvoTestSuites/types';
31
33
  declare const xstate: {
32
34
  emit: typeof emit;
33
35
  assign: typeof assign;
34
36
  };
35
- export { ArvoEventHandler, createArvoEventHandler, IArvoEventHandler, ArvoEventHandlerFunctionOutput, ArvoEventHandlerFunctionInput, ArvoEventHandlerFunction, PartialExcept, isNullOrUndefined, getValueOrDefault, coalesce, coalesceOrDefault, ArvoEventHandlerOpenTelemetryOptions, EventHandlerFactory, ContractViolation, ConfigViolation, ExecutionViolation, ArvoMachine, setupArvoMachine, ArvoMachineContext, EnqueueArvoEventActionParam, IMachineRegistry, MachineRegistry, MachineExecutionEngine, IMachineExectionEngine, ExecuteMachineInput, ExecuteMachineOutput, IMachineMemory, SimpleMachineMemory, MachineMemoryRecord, ArvoOrchestratorParam, TransactionViolation, TransactionViolationCause, ArvoOrchestrator, createArvoOrchestrator, SimpleEventBroker, createSimpleEventBroker, TelemetredSimpleMachineMemory, xstate, ArvoResumable, createArvoResumable, ArvoResumableHandler, ArvoResumableState, ArvoDomain, resolveEventDomain, isTransactionViolationError, OrchestrationExecutionStatus, ArvoEventHandlerOtelSpanOptions, };
37
+ export { ArvoEventHandler, createArvoEventHandler, IArvoEventHandler, ArvoEventHandlerFunctionOutput, ArvoEventHandlerFunctionInput, ArvoEventHandlerFunction, PartialExcept, isNullOrUndefined, getValueOrDefault, coalesce, coalesceOrDefault, ArvoEventHandlerOpenTelemetryOptions, EventHandlerFactory, ContractViolation, ConfigViolation, ExecutionViolation, ArvoMachine, setupArvoMachine, ArvoMachineContext, EnqueueArvoEventActionParam, IMachineRegistry, MachineRegistry, MachineExecutionEngine, IMachineExectionEngine, ExecuteMachineInput, ExecuteMachineOutput, IMachineMemory, SimpleMachineMemory, MachineMemoryRecord, ArvoOrchestratorParam, TransactionViolation, TransactionViolationCause, ArvoOrchestrator, createArvoOrchestrator, SimpleEventBroker, createSimpleEventBroker, TelemetredSimpleMachineMemory, xstate, ArvoResumable, createArvoResumable, ArvoResumableHandler, ArvoResumableState, ArvoDomain, resolveEventDomain, isTransactionViolationError, OrchestrationExecutionStatus, ArvoEventHandlerOtelSpanOptions, runArvoTestSuites, ArvoTestStep, ArvoTestCase, ArvoTestConfig, ArvoTestSuite, ArvoTestResult, IArvoTestFramework, };
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.OrchestrationExecutionStatus = exports.isTransactionViolationError = exports.resolveEventDomain = exports.ArvoDomain = exports.createArvoResumable = exports.ArvoResumable = exports.xstate = exports.TelemetredSimpleMachineMemory = exports.createSimpleEventBroker = exports.SimpleEventBroker = exports.createArvoOrchestrator = exports.ArvoOrchestrator = exports.TransactionViolationCause = exports.TransactionViolation = exports.SimpleMachineMemory = exports.MachineExecutionEngine = exports.MachineRegistry = exports.setupArvoMachine = exports.ArvoMachine = exports.ExecutionViolation = exports.ConfigViolation = exports.ContractViolation = exports.coalesceOrDefault = exports.coalesce = exports.getValueOrDefault = exports.isNullOrUndefined = exports.createArvoEventHandler = exports.ArvoEventHandler = void 0;
6
+ exports.runArvoTestSuites = exports.OrchestrationExecutionStatus = exports.isTransactionViolationError = exports.resolveEventDomain = exports.ArvoDomain = exports.createArvoResumable = exports.ArvoResumable = exports.xstate = exports.TelemetredSimpleMachineMemory = exports.createSimpleEventBroker = exports.SimpleEventBroker = exports.createArvoOrchestrator = exports.ArvoOrchestrator = exports.TransactionViolationCause = exports.TransactionViolation = exports.SimpleMachineMemory = exports.MachineExecutionEngine = exports.MachineRegistry = exports.setupArvoMachine = exports.ArvoMachine = exports.ExecutionViolation = exports.ConfigViolation = exports.ContractViolation = exports.coalesceOrDefault = exports.coalesce = exports.getValueOrDefault = exports.isNullOrUndefined = exports.createArvoEventHandler = exports.ArvoEventHandler = void 0;
7
7
  var xstate_1 = require("xstate");
8
8
  var ArvoDomain_1 = require("./ArvoDomain");
9
9
  Object.defineProperty(exports, "ArvoDomain", { enumerable: true, get: function () { return ArvoDomain_1.ArvoDomain; } });
@@ -51,6 +51,8 @@ var SimpleEventBroker_1 = require("./utils/SimpleEventBroker");
51
51
  Object.defineProperty(exports, "SimpleEventBroker", { enumerable: true, get: function () { return SimpleEventBroker_1.SimpleEventBroker; } });
52
52
  var helper_1 = require("./utils/SimpleEventBroker/helper");
53
53
  Object.defineProperty(exports, "createSimpleEventBroker", { enumerable: true, get: function () { return helper_1.createSimpleEventBroker; } });
54
+ var runArvoTestSuites_1 = require("./runArvoTestSuites");
55
+ Object.defineProperty(exports, "runArvoTestSuites", { enumerable: true, get: function () { return runArvoTestSuites_1.runArvoTestSuites; } });
54
56
  var xstate = {
55
57
  emit: xstate_1.emit,
56
58
  assign: xstate_1.assign,
@@ -0,0 +1,89 @@
1
+ import type { ArvoTestSuite, IArvoTestFramework } from './types.js';
2
+ /**
3
+ * Executes test suites for Arvo event handlers using the provided test framework adapter.
4
+ *
5
+ * This function registers test suites with your test framework (Vitest, Jest, Mocha, etc.)
6
+ * by using the provided adapter. Each test suite can contain multiple configurations and
7
+ * test cases. Test cases execute sequentially with each step receiving output from the
8
+ * previous step, enabling testing of complex event-driven workflows.
9
+ *
10
+ * Features include OpenTelemetry tracing for observability, support for error validation,
11
+ * event output validation, and optional retry logic for flaky tests.
12
+ *
13
+ * @param testSuites - Array of test suites to execute
14
+ * @param adapter - Test framework adapter providing describe, test, and beforeEach functions
15
+ *
16
+ * @example
17
+ * ```typescript
18
+ * import { runArvoTestSuites } from 'arvo-event-handler/test';
19
+ * import { describe, test, beforeEach } from 'vitest';
20
+ *
21
+ * const vitestAdapter = { describe, test, beforeEach };
22
+ *
23
+ * const suites: ArvoTestSuite[] = [
24
+ * {
25
+ * config: { name: 'User Handler', handler: userHandler },
26
+ * cases: [
27
+ * {
28
+ * name: 'Should create user and send email',
29
+ * steps: [
30
+ * {
31
+ * input: () => createArvoEvent({}),
32
+ * expectedEvents: (events) => events.length === 2
33
+ * },
34
+ * {
35
+ * input: (prev) => prev[1], // Use second event from previous step
36
+ * expectedEvents: (events) => events[0].type === 'email.sent'
37
+ * }
38
+ * ]
39
+ * }
40
+ * ]
41
+ * }
42
+ * ];
43
+ *
44
+ * runArvoTestSuites(suites, vitestAdapter);
45
+ * ```
46
+ *
47
+ * @example
48
+ * ```typescript
49
+ * // Testing with retry logic for non-deterministic handlers
50
+ * const suites: ArvoTestSuite[] = [
51
+ * {
52
+ * config: { name: 'Flaky Handler', handler: flakyHandler },
53
+ * cases: [
54
+ * {
55
+ * name: 'Should eventually succeed',
56
+ * steps: [{ input: () => event, expectedEvents: (e) => e.length > 0 }],
57
+ * repeat: { times: 10, successThreshold: 80 } // 80% success rate required
58
+ * }
59
+ * ]
60
+ * }
61
+ * ];
62
+ *
63
+ * runArvoTestSuites(suites, vitestAdapter);
64
+ * ```
65
+ *
66
+ * @example
67
+ * ```typescript
68
+ * // Testing error cases
69
+ * const suites: ArvoTestSuite[] = [
70
+ * {
71
+ * config: { name: 'Error Handler', handler: errorHandler },
72
+ * cases: [
73
+ * {
74
+ * name: 'Should throw validation error',
75
+ * steps: [
76
+ * {
77
+ * input: () => createArvoEvent({}),
78
+ * expectedError: (error) => error.message.includes('validation')
79
+ * }
80
+ * ]
81
+ * }
82
+ * ]
83
+ * }
84
+ * ];
85
+ *
86
+ * runArvoTestSuites(suites, vitestAdapter);
87
+ * ```
88
+ */
89
+ export declare const runArvoTestSuites: (testSuites: ArvoTestSuite[], adapter: IArvoTestFramework) => void;
@@ -0,0 +1,420 @@
1
+ "use strict";
2
+ var __assign = (this && this.__assign) || function () {
3
+ __assign = Object.assign || function(t) {
4
+ for (var s, i = 1, n = arguments.length; i < n; i++) {
5
+ s = arguments[i];
6
+ for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p))
7
+ t[p] = s[p];
8
+ }
9
+ return t;
10
+ };
11
+ return __assign.apply(this, arguments);
12
+ };
13
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
14
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
15
+ return new (P || (P = Promise))(function (resolve, reject) {
16
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
17
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
18
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
19
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
20
+ });
21
+ };
22
+ var __generator = (this && this.__generator) || function (thisArg, body) {
23
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
24
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
25
+ function verb(n) { return function (v) { return step([n, v]); }; }
26
+ function step(op) {
27
+ if (f) throw new TypeError("Generator is already executing.");
28
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
29
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
30
+ if (y = 0, t) op = [op[0] & 2, t.value];
31
+ switch (op[0]) {
32
+ case 0: case 1: t = op; break;
33
+ case 4: _.label++; return { value: op[1], done: false };
34
+ case 5: _.label++; y = op[1]; op = [0]; continue;
35
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
36
+ default:
37
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
38
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
39
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
40
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
41
+ if (t[2]) _.ops.pop();
42
+ _.trys.pop(); continue;
43
+ }
44
+ op = body.call(thisArg, _);
45
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
46
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
47
+ }
48
+ };
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.runArvoTestSuites = void 0;
51
+ var api_1 = require("@opentelemetry/api");
52
+ var arvo_core_1 = require("arvo-core");
53
+ var validateExpectedError = function (handler, input, expectedError, stepIndex) { return __awaiter(void 0, void 0, void 0, function () {
54
+ var error_1, matches;
55
+ return __generator(this, function (_a) {
56
+ switch (_a.label) {
57
+ case 0:
58
+ _a.trys.push([0, 2, , 4]);
59
+ return [4 /*yield*/, handler.execute(input)];
60
+ case 1:
61
+ _a.sent();
62
+ return [2 /*return*/, {
63
+ success: false,
64
+ error: "Step ".concat(stepIndex, ": Expected error but function succeeded"),
65
+ events: [],
66
+ }];
67
+ case 2:
68
+ error_1 = _a.sent();
69
+ return [4 /*yield*/, expectedError(error_1)];
70
+ case 3:
71
+ matches = _a.sent();
72
+ return [2 /*return*/, matches
73
+ ? { success: true, events: [] }
74
+ : {
75
+ success: false,
76
+ error: "Step ".concat(stepIndex, ": Error didn't match custom validator: ").concat(error_1.message),
77
+ events: [],
78
+ }];
79
+ case 4: return [2 /*return*/];
80
+ }
81
+ });
82
+ }); };
83
+ var validateExpectedEvents = function (handler, input, expectedEvents, stepIndex) { return __awaiter(void 0, void 0, void 0, function () {
84
+ var actualResult, matches, error_2;
85
+ return __generator(this, function (_a) {
86
+ switch (_a.label) {
87
+ case 0: return [4 /*yield*/, handler.execute(input)];
88
+ case 1:
89
+ actualResult = (_a.sent()).events;
90
+ _a.label = 2;
91
+ case 2:
92
+ _a.trys.push([2, 4, , 5]);
93
+ return [4 /*yield*/, expectedEvents(actualResult)];
94
+ case 3:
95
+ matches = _a.sent();
96
+ if (!matches) {
97
+ return [2 /*return*/, {
98
+ success: false,
99
+ error: "Step ".concat(stepIndex, ": Custom validator returned false\nActual events: ").concat(actualResult.map(function (item) { return item.toString(2); }).join('\n')),
100
+ events: actualResult,
101
+ }];
102
+ }
103
+ return [2 /*return*/, { success: true, events: actualResult }];
104
+ case 4:
105
+ error_2 = _a.sent();
106
+ if (error_2 instanceof Error) {
107
+ return [2 /*return*/, {
108
+ success: false,
109
+ error: "Step ".concat(stepIndex, ": Custom validator threw error: ").concat(error_2.message, "\nActual events: ").concat(actualResult.map(function (item) { return item.toString(2); }).join('\n')),
110
+ events: actualResult,
111
+ }];
112
+ }
113
+ return [2 /*return*/, {
114
+ success: false,
115
+ error: "Step ".concat(stepIndex, ": Custom validator threw unknown error"),
116
+ events: actualResult,
117
+ }];
118
+ case 5: return [2 /*return*/];
119
+ }
120
+ });
121
+ }); };
122
+ var executeStep = function (handler, step, stepIndex, previousEvents, tracer) { return __awaiter(void 0, void 0, void 0, function () {
123
+ var stepSpan, result, error_3;
124
+ var _a;
125
+ return __generator(this, function (_b) {
126
+ switch (_b.label) {
127
+ case 0:
128
+ stepSpan = tracer.startSpan("Step<".concat(stepIndex, ">"), {
129
+ attributes: {
130
+ 'test.step': stepIndex,
131
+ 'test.previous.events.count': (_a = previousEvents === null || previousEvents === void 0 ? void 0 : previousEvents.length) !== null && _a !== void 0 ? _a : 0,
132
+ },
133
+ });
134
+ _b.label = 1;
135
+ case 1:
136
+ _b.trys.push([1, 3, , 4]);
137
+ return [4 /*yield*/, api_1.context.with(api_1.trace.setSpan(api_1.context.active(), stepSpan), function () { return __awaiter(void 0, void 0, void 0, function () {
138
+ var currentInput, actualResult;
139
+ return __generator(this, function (_a) {
140
+ switch (_a.label) {
141
+ case 0: return [4 /*yield*/, step.input(previousEvents)];
142
+ case 1:
143
+ currentInput = _a.sent();
144
+ stepSpan.setAttribute('test.input.type', currentInput.type);
145
+ if (!step.expectedError) return [3 /*break*/, 3];
146
+ return [4 /*yield*/, validateExpectedError(handler, currentInput, step.expectedError, stepIndex)];
147
+ case 2: return [2 /*return*/, _a.sent()];
148
+ case 3:
149
+ if (!step.expectedEvents) return [3 /*break*/, 5];
150
+ return [4 /*yield*/, validateExpectedEvents(handler, currentInput, step.expectedEvents, stepIndex)];
151
+ case 4: return [2 /*return*/, _a.sent()];
152
+ case 5: return [4 /*yield*/, handler.execute(currentInput)];
153
+ case 6:
154
+ actualResult = (_a.sent()).events;
155
+ return [2 /*return*/, { success: true, events: actualResult }];
156
+ }
157
+ });
158
+ }); })];
159
+ case 2:
160
+ result = _b.sent();
161
+ stepSpan.setStatus(result.success ? { code: api_1.SpanStatusCode.OK } : { code: api_1.SpanStatusCode.ERROR, message: result.error });
162
+ stepSpan.end();
163
+ return [2 /*return*/, __assign(__assign({}, result), { step: stepIndex })];
164
+ case 3:
165
+ error_3 = _b.sent();
166
+ stepSpan.recordException(error_3);
167
+ stepSpan.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error_3.message });
168
+ stepSpan.end();
169
+ return [2 /*return*/, {
170
+ success: false,
171
+ error: "Step ".concat(stepIndex, ": Unexpected exception: ").concat(error_3.message),
172
+ events: [],
173
+ step: stepIndex,
174
+ }];
175
+ case 4: return [2 /*return*/];
176
+ }
177
+ });
178
+ }); };
179
+ var executeAllSteps = function (handler, steps, tracer) { return __awaiter(void 0, void 0, void 0, function () {
180
+ var previousEvents, stepIndex, stepResult;
181
+ return __generator(this, function (_a) {
182
+ switch (_a.label) {
183
+ case 0:
184
+ previousEvents = null;
185
+ stepIndex = 0;
186
+ _a.label = 1;
187
+ case 1:
188
+ if (!(stepIndex < steps.length)) return [3 /*break*/, 4];
189
+ return [4 /*yield*/, executeStep(handler, steps[stepIndex], stepIndex + 1, previousEvents, tracer)];
190
+ case 2:
191
+ stepResult = _a.sent();
192
+ if (!stepResult.success) {
193
+ return [2 /*return*/, stepResult];
194
+ }
195
+ previousEvents = stepResult.events || [];
196
+ _a.label = 3;
197
+ case 3:
198
+ stepIndex++;
199
+ return [3 /*break*/, 1];
200
+ case 4: return [2 /*return*/, { success: true, step: steps.length - 1 }];
201
+ }
202
+ });
203
+ }); };
204
+ var handleRepeatTest = function (testFn, repeat) { return __awaiter(void 0, void 0, void 0, function () {
205
+ var results, failures, successCount, successRate, failureSummary, additionalFailures;
206
+ return __generator(this, function (_a) {
207
+ switch (_a.label) {
208
+ case 0: return [4 /*yield*/, Promise.all(Array.from({ length: repeat.times }, function (_, i) { return testFn(); }))];
209
+ case 1:
210
+ results = _a.sent();
211
+ failures = results.filter(function (r) { return !r.success; });
212
+ successCount = results.length - failures.length;
213
+ successRate = (successCount / repeat.times) * 100;
214
+ if (successRate < repeat.successThreshold) {
215
+ failureSummary = failures
216
+ .slice(0, 10)
217
+ .map(function (f) { return " Iteration ".concat(f.iteration, ": ").concat(f.error); })
218
+ .join('\n');
219
+ additionalFailures = failures.length > 10 ? "\n ... and ".concat(failures.length - 10, " more failures") : '';
220
+ throw new Error("Success rate ".concat(successRate.toFixed(2), "% is below threshold ").concat(repeat.successThreshold, "%\n") +
221
+ "Successes: ".concat(successCount, "/").concat(repeat.times, "\n") +
222
+ "Sample failures:\n".concat(failureSummary).concat(additionalFailures));
223
+ }
224
+ return [2 /*return*/];
225
+ }
226
+ });
227
+ }); };
228
+ /**
229
+ * Executes test suites for Arvo event handlers using the provided test framework adapter.
230
+ *
231
+ * This function registers test suites with your test framework (Vitest, Jest, Mocha, etc.)
232
+ * by using the provided adapter. Each test suite can contain multiple configurations and
233
+ * test cases. Test cases execute sequentially with each step receiving output from the
234
+ * previous step, enabling testing of complex event-driven workflows.
235
+ *
236
+ * Features include OpenTelemetry tracing for observability, support for error validation,
237
+ * event output validation, and optional retry logic for flaky tests.
238
+ *
239
+ * @param testSuites - Array of test suites to execute
240
+ * @param adapter - Test framework adapter providing describe, test, and beforeEach functions
241
+ *
242
+ * @example
243
+ * ```typescript
244
+ * import { runArvoTestSuites } from 'arvo-event-handler/test';
245
+ * import { describe, test, beforeEach } from 'vitest';
246
+ *
247
+ * const vitestAdapter = { describe, test, beforeEach };
248
+ *
249
+ * const suites: ArvoTestSuite[] = [
250
+ * {
251
+ * config: { name: 'User Handler', handler: userHandler },
252
+ * cases: [
253
+ * {
254
+ * name: 'Should create user and send email',
255
+ * steps: [
256
+ * {
257
+ * input: () => createArvoEvent({}),
258
+ * expectedEvents: (events) => events.length === 2
259
+ * },
260
+ * {
261
+ * input: (prev) => prev[1], // Use second event from previous step
262
+ * expectedEvents: (events) => events[0].type === 'email.sent'
263
+ * }
264
+ * ]
265
+ * }
266
+ * ]
267
+ * }
268
+ * ];
269
+ *
270
+ * runArvoTestSuites(suites, vitestAdapter);
271
+ * ```
272
+ *
273
+ * @example
274
+ * ```typescript
275
+ * // Testing with retry logic for non-deterministic handlers
276
+ * const suites: ArvoTestSuite[] = [
277
+ * {
278
+ * config: { name: 'Flaky Handler', handler: flakyHandler },
279
+ * cases: [
280
+ * {
281
+ * name: 'Should eventually succeed',
282
+ * steps: [{ input: () => event, expectedEvents: (e) => e.length > 0 }],
283
+ * repeat: { times: 10, successThreshold: 80 } // 80% success rate required
284
+ * }
285
+ * ]
286
+ * }
287
+ * ];
288
+ *
289
+ * runArvoTestSuites(suites, vitestAdapter);
290
+ * ```
291
+ *
292
+ * @example
293
+ * ```typescript
294
+ * // Testing error cases
295
+ * const suites: ArvoTestSuite[] = [
296
+ * {
297
+ * config: { name: 'Error Handler', handler: errorHandler },
298
+ * cases: [
299
+ * {
300
+ * name: 'Should throw validation error',
301
+ * steps: [
302
+ * {
303
+ * input: () => createArvoEvent({}),
304
+ * expectedError: (error) => error.message.includes('validation')
305
+ * }
306
+ * ]
307
+ * }
308
+ * ]
309
+ * }
310
+ * ];
311
+ *
312
+ * runArvoTestSuites(suites, vitestAdapter);
313
+ * ```
314
+ */
315
+ var runArvoTestSuites = function (testSuites, adapter) {
316
+ var _a, _b;
317
+ var _loop_1 = function (config, cases) {
318
+ var configs = Array.isArray(config) ? config : [config];
319
+ var _loop_2 = function (fnName, _handler, fn) {
320
+ var handler = (_a = _handler) !== null && _a !== void 0 ? _a : {
321
+ source: (_b = fn === null || fn === void 0 ? void 0 : fn.name) !== null && _b !== void 0 ? _b : 'unknown',
322
+ execute: fn,
323
+ };
324
+ adapter.describe(fnName !== null && fnName !== void 0 ? fnName : "Test<".concat(handler.source, ">"), function () {
325
+ var tracer;
326
+ adapter.beforeEach(function () {
327
+ tracer = arvo_core_1.ArvoOpenTelemetry.getInstance().tracer;
328
+ });
329
+ var _loop_3 = function (name_1, steps, repeat) {
330
+ adapter.test(name_1, function () { return __awaiter(void 0, void 0, void 0, function () {
331
+ var runTest, result;
332
+ return __generator(this, function (_a) {
333
+ switch (_a.label) {
334
+ case 0:
335
+ runTest = function (iteration) { return __awaiter(void 0, void 0, void 0, function () {
336
+ var span, result, error_4;
337
+ var _a;
338
+ return __generator(this, function (_b) {
339
+ switch (_b.label) {
340
+ case 0:
341
+ span = tracer.startSpan("Case<".concat(name_1, ">[").concat(iteration !== null && iteration !== void 0 ? iteration : 0, "]"), {
342
+ attributes: {
343
+ 'test.function.name': fnName,
344
+ 'test.iteration': iteration,
345
+ 'test.total.steps': steps.length,
346
+ },
347
+ });
348
+ _b.label = 1;
349
+ case 1:
350
+ _b.trys.push([1, 3, 4, 5]);
351
+ return [4 /*yield*/, api_1.context.with(api_1.trace.setSpan(api_1.context.active(), span), function () { return __awaiter(void 0, void 0, void 0, function () {
352
+ return __generator(this, function (_a) {
353
+ switch (_a.label) {
354
+ case 0: return [4 /*yield*/, executeAllSteps(handler, steps, tracer)];
355
+ case 1: return [2 /*return*/, _a.sent()];
356
+ }
357
+ });
358
+ }); })];
359
+ case 2:
360
+ result = _b.sent();
361
+ span.setStatus(result.success
362
+ ? { code: api_1.SpanStatusCode.OK }
363
+ : { code: api_1.SpanStatusCode.ERROR, message: (_a = result.error) !== null && _a !== void 0 ? _a : 'Test suite failed' });
364
+ if (result.error) {
365
+ span.setAttribute('test.error', result.error);
366
+ }
367
+ if (result.step !== undefined) {
368
+ span.setAttribute('test.steps.completed', result.step + 1);
369
+ }
370
+ return [2 /*return*/, __assign(__assign({}, result), { iteration: iteration })];
371
+ case 3:
372
+ error_4 = _b.sent();
373
+ span.recordException(error_4);
374
+ span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error_4.message });
375
+ return [2 /*return*/, {
376
+ success: false,
377
+ error: "Unexpected exception: ".concat(error_4.message),
378
+ iteration: iteration,
379
+ }];
380
+ case 4:
381
+ span.end();
382
+ return [7 /*endfinally*/];
383
+ case 5: return [2 /*return*/];
384
+ }
385
+ });
386
+ }); };
387
+ if (!repeat) return [3 /*break*/, 2];
388
+ return [4 /*yield*/, handleRepeatTest(function () { return runTest(); }, repeat)];
389
+ case 1:
390
+ _a.sent();
391
+ return [3 /*break*/, 4];
392
+ case 2: return [4 /*yield*/, runTest()];
393
+ case 3:
394
+ result = _a.sent();
395
+ if (!result.success) {
396
+ throw new Error(result.error);
397
+ }
398
+ _a.label = 4;
399
+ case 4: return [2 /*return*/];
400
+ }
401
+ });
402
+ }); });
403
+ };
404
+ for (var _i = 0, cases_1 = cases; _i < cases_1.length; _i++) {
405
+ var _a = cases_1[_i], name_1 = _a.name, steps = _a.steps, repeat = _a.repeat;
406
+ _loop_3(name_1, steps, repeat);
407
+ }
408
+ });
409
+ };
410
+ for (var _d = 0, configs_1 = configs; _d < configs_1.length; _d++) {
411
+ var _e = configs_1[_d], fnName = _e.name, _handler = _e.handler, fn = _e.fn;
412
+ _loop_2(fnName, _handler, fn);
413
+ }
414
+ };
415
+ for (var _i = 0, testSuites_1 = testSuites; _i < testSuites_1.length; _i++) {
416
+ var _c = testSuites_1[_i], config = _c.config, cases = _c.cases;
417
+ _loop_1(config, cases);
418
+ }
419
+ };
420
+ exports.runArvoTestSuites = runArvoTestSuites;
@@ -0,0 +1,176 @@
1
+ import type { ArvoEvent } from 'arvo-core';
2
+ import IArvoEventHandler from '../IArvoEventHandler';
3
+ /**
4
+ * Defines a single test step in an event handler test sequence.
5
+ *
6
+ * Each step receives the output events from the previous step, generates a new input event,
7
+ * and validates either the output events or expected errors. Steps are executed sequentially,
8
+ * allowing you to test complex event-driven workflows.
9
+ */
10
+ export type ArvoTestStep = {
11
+ /**
12
+ * Generates the input event for this step based on previous step's output.
13
+ * Receives null on the first step or an array of ArvoEvents from the previous step.
14
+ */
15
+ input: ((prev: ArvoEvent[] | null) => ArvoEvent) | ((prev: ArvoEvent[] | null) => Promise<ArvoEvent>);
16
+ } & ({
17
+ /**
18
+ * Optional validator for output events when the handler executes successfully.
19
+ *
20
+ * @param event - Array of events emitted by the handler
21
+ * @returns true if the events match expectations, false otherwise
22
+ */
23
+ expectedEvents?: ((event: ArvoEvent[]) => boolean) | ((event: ArvoEvent[]) => Promise<boolean>);
24
+ expectedError?: never;
25
+ } | {
26
+ /**
27
+ * Optional validator for errors when the handler is expected to throw.
28
+ *
29
+ * @param error - The error thrown by the handler
30
+ * @returns true if the error matches expectations, false otherwise
31
+ */
32
+ expectedError?: ((error: Error) => boolean) | ((error: Error) => Promise<boolean>);
33
+ expectedEvents?: never;
34
+ });
35
+ /**
36
+ * Defines a complete test case containing one or more sequential steps.
37
+ *
38
+ * Test cases execute steps in order, passing output events from each step to the next.
39
+ * This allows testing of complex event chains and workflows. Optional repeat configuration
40
+ * helps handle flaky tests by running them multiple times and checking success rate.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * const testCase: ArvoTestCase = {
45
+ * name: 'User registration flow',
46
+ * steps: [
47
+ * { input: () => createUserEvent(), expectedEvents: (e) => e.length === 2 },
48
+ * { input: (prev) => prev[0], expectedEvents: (e) => e[0].type === 'email.sent' }
49
+ * ],
50
+ * repeat: { times: 10, successThreshold: 95 }
51
+ * };
52
+ * ```
53
+ */
54
+ export type ArvoTestCase = {
55
+ /** Descriptive name for the test case, displayed in test output. */
56
+ name: string;
57
+ /** Sequential steps to execute in order. Must contain at least one step. */
58
+ steps: [ArvoTestStep, ...ArvoTestStep[]];
59
+ /**
60
+ * Optional configuration for running the test multiple times.
61
+ * Useful for testing non-deterministic handlers or catching intermittent failures.
62
+ * The test passes only if the success rate meets or exceeds the threshold.
63
+ */
64
+ repeat?: {
65
+ /** Number of times to execute the entire test case */
66
+ times: number;
67
+ /**
68
+ * Minimum percentage of successful runs required for the test to pass (0-100).
69
+ * For example, 95 means at least 95% of runs must succeed.
70
+ */
71
+ successThreshold: number;
72
+ };
73
+ };
74
+ /**
75
+ * Configuration for the event handler or function under test.
76
+ *
77
+ * Supports testing either an IArvoEventHandler instance or a raw async function.
78
+ * Multiple configs can be provided to test the same cases against different implementations.
79
+ */
80
+ export type ArvoTestConfig = {
81
+ /**
82
+ * Optional display name for the test suite.
83
+ * If not provided, defaults to the handler source or function name.
84
+ */
85
+ name?: string;
86
+ } & ({
87
+ /** IArvoEventHandler instance to test. */
88
+ handler?: IArvoEventHandler;
89
+ fn?: never;
90
+ } | {
91
+ /** Raw async function to test that accepts an ArvoEvent and returns events. */
92
+ fn?: (event: ArvoEvent) => Promise<{
93
+ events: ArvoEvent[];
94
+ }>;
95
+ handler?: never;
96
+ });
97
+ /**
98
+ * Complete test suite definition combining configuration and test cases.
99
+ *
100
+ * A test suite can test one or more handlers/functions against the same set of test cases.
101
+ * When multiple configs are provided, each test case runs against all configs, enabling
102
+ * cross-implementation testing and comparison.
103
+ *
104
+ * @example
105
+ * ```typescript
106
+ * const suite: ArvoTestSuite = {
107
+ * config: [
108
+ * { name: 'V1 Handler', handler: handlerV1 },
109
+ * { name: 'V2 Handler', handler: handlerV2 }
110
+ * ],
111
+ * cases: [
112
+ * {
113
+ * name: 'Should process user event',
114
+ * steps: [{ input: () => userEvent, expectedEvents: (e) => e.length > 0 }]
115
+ * },
116
+ * {
117
+ * name: 'Step 2',
118
+ * steps: [{ input: () => someEvent, expectedEvents: (e) => expect(e.type).toBe('com.some.event') }]
119
+ * }
120
+ * ]
121
+ * };
122
+ * ```
123
+ */
124
+ export type ArvoTestSuite = {
125
+ /** Handler or function configuration(s) to test. */
126
+ config: ArvoTestConfig | ArvoTestConfig[];
127
+ /** Array of test cases to execute against the configured handler(s) */
128
+ cases: ArvoTestCase[];
129
+ };
130
+ /**
131
+ * Result of executing a single test run.
132
+ */
133
+ export type ArvoTestResult = {
134
+ /** Whether the test execution passed all steps successfully */
135
+ success: boolean;
136
+ /** Detailed error message if the test failed. */
137
+ error?: string;
138
+ /** Iteration number when using repeat configuration. */
139
+ iteration?: number;
140
+ /** Index of the last completed step (0-based). */
141
+ step?: number;
142
+ };
143
+ /**
144
+ * Adapter interface for integrating with different test frameworks.
145
+ *
146
+ * Provides a unified interface for test framework primitives (describe, test, beforeEach),
147
+ * enabling the same test suites to run on Vitest, Jest, Mocha, or any other framework.
148
+ * Implementations should wrap their framework's native functions.
149
+ *
150
+ * @example
151
+ * ```typescript
152
+ * // Vitest adapter
153
+ * import { describe, test, beforeEach } from 'vitest';
154
+ *
155
+ * const vitestAdapter: IArvoTestFramework = {
156
+ * describe,
157
+ * test,
158
+ * beforeEach
159
+ * };
160
+ *
161
+ * // Mocha adapter
162
+ * const mochaAdapter: IArvoTestFramework = {
163
+ * describe,
164
+ * test: it,
165
+ * beforeEach
166
+ * };
167
+ * ```
168
+ */
169
+ export interface IArvoTestFramework {
170
+ /** Groups related tests into a test suite. */
171
+ describe(name: string, fn: () => void): void;
172
+ /** Defines a single test case. */
173
+ test(name: string, fn: () => Promise<void>): void;
174
+ /** Runs setup logic before each test in the current describe block. */
175
+ beforeEach(fn: () => void): void;
176
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arvo-event-handler",
3
- "version": "3.0.16",
3
+ "version": "3.0.18",
4
4
  "description": "Type-safe event handler system with versioning, telemetry, and contract validation for distributed Arvo event-driven architectures, featuring routing and multi-handler support.",
5
5
  "main": "dist/index.js",
6
6
  "scripts": {
@@ -46,7 +46,7 @@
46
46
  "dependencies": {
47
47
  "@opentelemetry/api": "^1.9.0",
48
48
  "@opentelemetry/core": "^1.30.1",
49
- "arvo-core": "^3.0.16",
49
+ "arvo-core": "^3.0.18",
50
50
  "uuid": "^11.1.0",
51
51
  "xstate": "^5.23.0",
52
52
  "zod": "^3.25.74",