UNPKG

arvo-event-handler

Version:

A complete set of orthogonal event handler and orchestration primitives for Arvo based applications, featuring declarative state machines (XState), imperative resumables for agentic workflows, contract-based routing, OpenTelemetry observability, and in-me

421 lines (420 loc) 22.1 kB
"use strict"; var __assign = (this && this.__assign) || function () { __assign = Object.assign || function(t) { for (var s, i = 1, n = arguments.length; i < n; i++) { s = arguments[i]; for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p]; } return t; }; return __assign.apply(this, arguments); }; var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } return new (P || (P = Promise))(function (resolve, reject) { function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } step((generator = generator.apply(thisArg, _arguments || [])).next()); }); }; var __generator = (this && this.__generator) || function (thisArg, body) { 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); return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; function verb(n) { return function (v) { return step([n, v]); }; } function step(op) { if (f) throw new TypeError("Generator is already executing."); while (g && (g = 0, op[0] && (_ = 0)), _) try { 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; if (y = 0, t) op = [op[0] & 2, t.value]; switch (op[0]) { case 0: case 1: t = op; break; case 4: _.label++; return { value: op[1], done: false }; case 5: _.label++; y = op[1]; op = [0]; continue; case 7: op = _.ops.pop(); _.trys.pop(); continue; default: if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } if (t[2]) _.ops.pop(); _.trys.pop(); continue; } op = body.call(thisArg, _); } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; } }; Object.defineProperty(exports, "__esModule", { value: true }); exports.runArvoTestSuites = void 0; var api_1 = require("@opentelemetry/api"); var arvo_core_1 = require("arvo-core"); var validateExpectedError = function (handler, input, expectedError, stepIndex) { return __awaiter(void 0, void 0, void 0, function () { var error_1, matches; return __generator(this, function (_a) { switch (_a.label) { case 0: _a.trys.push([0, 2, , 4]); return [4 /*yield*/, handler.execute(input)]; case 1: _a.sent(); return [2 /*return*/, { success: false, error: "Step ".concat(stepIndex, ": Expected error but function succeeded"), events: [], }]; case 2: error_1 = _a.sent(); return [4 /*yield*/, expectedError(error_1)]; case 3: matches = _a.sent(); return [2 /*return*/, matches ? { success: true, events: [] } : { success: false, error: "Step ".concat(stepIndex, ": Error didn't match custom validator: ").concat(error_1.message), events: [], }]; case 4: return [2 /*return*/]; } }); }); }; var validateExpectedEvents = function (handler, input, expectedEvents, stepIndex) { return __awaiter(void 0, void 0, void 0, function () { var actualResult, matches, error_2; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, handler.execute(input)]; case 1: actualResult = (_a.sent()).events; _a.label = 2; case 2: _a.trys.push([2, 4, , 5]); return [4 /*yield*/, expectedEvents(actualResult)]; case 3: matches = _a.sent(); if (!matches) { return [2 /*return*/, { success: false, error: "Step ".concat(stepIndex, ": Custom validator returned false\nActual events: ").concat(actualResult.map(function (item) { return item.toString(2); }).join('\n')), events: actualResult, }]; } return [2 /*return*/, { success: true, events: actualResult }]; case 4: error_2 = _a.sent(); if (error_2 instanceof Error) { return [2 /*return*/, { success: false, 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')), events: actualResult, }]; } return [2 /*return*/, { success: false, error: "Step ".concat(stepIndex, ": Custom validator threw unknown error"), events: actualResult, }]; case 5: return [2 /*return*/]; } }); }); }; var executeStep = function (handler, step, stepIndex, previousEvents, tracer) { return __awaiter(void 0, void 0, void 0, function () { var stepSpan, result, error_3; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: stepSpan = tracer.startSpan("Step<".concat(stepIndex, ">"), { attributes: { 'test.step': stepIndex, 'test.previous.events.count': (_a = previousEvents === null || previousEvents === void 0 ? void 0 : previousEvents.length) !== null && _a !== void 0 ? _a : 0, }, }); _b.label = 1; case 1: _b.trys.push([1, 3, , 4]); 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 () { var currentInput, actualResult; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, step.input(previousEvents)]; case 1: currentInput = _a.sent(); stepSpan.setAttribute('test.input.type', currentInput.type); if (!step.expectedError) return [3 /*break*/, 3]; return [4 /*yield*/, validateExpectedError(handler, currentInput, step.expectedError, stepIndex)]; case 2: return [2 /*return*/, _a.sent()]; case 3: if (!step.expectedEvents) return [3 /*break*/, 5]; return [4 /*yield*/, validateExpectedEvents(handler, currentInput, step.expectedEvents, stepIndex)]; case 4: return [2 /*return*/, _a.sent()]; case 5: return [4 /*yield*/, handler.execute(currentInput)]; case 6: actualResult = (_a.sent()).events; return [2 /*return*/, { success: true, events: actualResult }]; } }); }); })]; case 2: result = _b.sent(); stepSpan.setStatus(result.success ? { code: api_1.SpanStatusCode.OK } : { code: api_1.SpanStatusCode.ERROR, message: result.error }); stepSpan.end(); return [2 /*return*/, __assign(__assign({}, result), { step: stepIndex })]; case 3: error_3 = _b.sent(); stepSpan.recordException(error_3); stepSpan.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error_3.message }); stepSpan.end(); return [2 /*return*/, { success: false, error: "Step ".concat(stepIndex, ": Unexpected exception: ").concat(error_3.message), events: [], step: stepIndex, }]; case 4: return [2 /*return*/]; } }); }); }; var executeAllSteps = function (handler, steps, tracer) { return __awaiter(void 0, void 0, void 0, function () { var previousEvents, stepIndex, stepResult; return __generator(this, function (_a) { switch (_a.label) { case 0: previousEvents = null; stepIndex = 0; _a.label = 1; case 1: if (!(stepIndex < steps.length)) return [3 /*break*/, 4]; return [4 /*yield*/, executeStep(handler, steps[stepIndex], stepIndex + 1, previousEvents, tracer)]; case 2: stepResult = _a.sent(); if (!stepResult.success) { return [2 /*return*/, stepResult]; } previousEvents = stepResult.events || []; _a.label = 3; case 3: stepIndex++; return [3 /*break*/, 1]; case 4: return [2 /*return*/, { success: true, step: steps.length - 1 }]; } }); }); }; var handleRepeatTest = function (testFn, repeat) { return __awaiter(void 0, void 0, void 0, function () { var results, failures, successCount, successRate, failureSummary, additionalFailures; return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, Promise.all(Array.from({ length: repeat.times }, function (_, i) { return testFn(); }))]; case 1: results = _a.sent(); failures = results.filter(function (r) { return !r.success; }); successCount = results.length - failures.length; successRate = (successCount / repeat.times) * 100; if (successRate < repeat.successThreshold) { failureSummary = failures .slice(0, 10) .map(function (f) { return " Iteration ".concat(f.iteration, ": ").concat(f.error); }) .join('\n'); additionalFailures = failures.length > 10 ? "\n ... and ".concat(failures.length - 10, " more failures") : ''; throw new Error("Success rate ".concat(successRate.toFixed(2), "% is below threshold ").concat(repeat.successThreshold, "%\n") + "Successes: ".concat(successCount, "/").concat(repeat.times, "\n") + "Sample failures:\n".concat(failureSummary).concat(additionalFailures)); } return [2 /*return*/]; } }); }); }; /** * Executes test suites for Arvo event handlers using the provided test framework adapter. * * This function registers test suites with your test framework (Vitest, Jest, Mocha, etc.) * by using the provided adapter. Each test suite can contain multiple configurations and * test cases. Test cases execute sequentially with each step receiving output from the * previous step, enabling testing of complex event-driven workflows. * * Features include OpenTelemetry tracing for observability, support for error validation, * event output validation, and optional retry logic for flaky tests. * * @param testSuites - Array of test suites to execute * @param adapter - Test framework adapter providing describe, test, and beforeEach functions * * @example * ```typescript * import { runArvoTestSuites } from 'arvo-event-handler/test'; * import { describe, test, beforeEach } from 'vitest'; * * const vitestAdapter = { describe, test, beforeEach }; * * const suites: ArvoTestSuite[] = [ * { * config: { name: 'User Handler', handler: userHandler }, * cases: [ * { * name: 'Should create user and send email', * steps: [ * { * input: () => createArvoEvent({}), * expectedEvents: (events) => events.length === 2 * }, * { * input: (prev) => prev[1], // Use second event from previous step * expectedEvents: (events) => events[0].type === 'email.sent' * } * ] * } * ] * } * ]; * * runArvoTestSuites(suites, vitestAdapter); * ``` * * @example * ```typescript * // Testing with retry logic for non-deterministic handlers * const suites: ArvoTestSuite[] = [ * { * config: { name: 'Flaky Handler', handler: flakyHandler }, * cases: [ * { * name: 'Should eventually succeed', * steps: [{ input: () => event, expectedEvents: (e) => e.length > 0 }], * repeat: { times: 10, successThreshold: 80 } // 80% success rate required * } * ] * } * ]; * * runArvoTestSuites(suites, vitestAdapter); * ``` * * @example * ```typescript * // Testing error cases * const suites: ArvoTestSuite[] = [ * { * config: { name: 'Error Handler', handler: errorHandler }, * cases: [ * { * name: 'Should throw validation error', * steps: [ * { * input: () => createArvoEvent({}), * expectedError: (error) => error.message.includes('validation') * } * ] * } * ] * } * ]; * * runArvoTestSuites(suites, vitestAdapter); * ``` */ var runArvoTestSuites = function (testSuites, adapter) { var _a, _b; var _loop_1 = function (config, cases) { var configs = Array.isArray(config) ? config : [config]; var _loop_2 = function (fnName, _handler, fn) { var handler = (_a = _handler) !== null && _a !== void 0 ? _a : { source: (_b = fn === null || fn === void 0 ? void 0 : fn.name) !== null && _b !== void 0 ? _b : 'unknown', execute: fn, }; adapter.describe(fnName !== null && fnName !== void 0 ? fnName : "Test<".concat(handler.source, ">"), function () { var tracer; adapter.beforeEach(function () { tracer = arvo_core_1.ArvoOpenTelemetry.getInstance().tracer; }); var _loop_3 = function (name_1, steps, repeat) { adapter.test(name_1, function () { return __awaiter(void 0, void 0, void 0, function () { var runTest, result; return __generator(this, function (_a) { switch (_a.label) { case 0: runTest = function (iteration) { return __awaiter(void 0, void 0, void 0, function () { var span, result, error_4; var _a; return __generator(this, function (_b) { switch (_b.label) { case 0: span = tracer.startSpan("Case<".concat(name_1, ">[").concat(iteration !== null && iteration !== void 0 ? iteration : 0, "]"), { attributes: { 'test.function.name': fnName, 'test.iteration': iteration, 'test.total.steps': steps.length, }, }); _b.label = 1; case 1: _b.trys.push([1, 3, 4, 5]); 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 () { return __generator(this, function (_a) { switch (_a.label) { case 0: return [4 /*yield*/, executeAllSteps(handler, steps, tracer)]; case 1: return [2 /*return*/, _a.sent()]; } }); }); })]; case 2: result = _b.sent(); span.setStatus(result.success ? { code: api_1.SpanStatusCode.OK } : { code: api_1.SpanStatusCode.ERROR, message: (_a = result.error) !== null && _a !== void 0 ? _a : 'Test suite failed' }); if (result.error) { span.setAttribute('test.error', result.error); } if (result.step !== undefined) { span.setAttribute('test.steps.completed', result.step + 1); } return [2 /*return*/, __assign(__assign({}, result), { iteration: iteration })]; case 3: error_4 = _b.sent(); span.recordException(error_4); span.setStatus({ code: api_1.SpanStatusCode.ERROR, message: error_4.message }); return [2 /*return*/, { success: false, error: "Unexpected exception: ".concat(error_4.message), iteration: iteration, }]; case 4: span.end(); return [7 /*endfinally*/]; case 5: return [2 /*return*/]; } }); }); }; if (!repeat) return [3 /*break*/, 2]; return [4 /*yield*/, handleRepeatTest(function () { return runTest(); }, repeat)]; case 1: _a.sent(); return [3 /*break*/, 4]; case 2: return [4 /*yield*/, runTest()]; case 3: result = _a.sent(); if (!result.success) { throw new Error(result.error); } _a.label = 4; case 4: return [2 /*return*/]; } }); }); }); }; for (var _i = 0, cases_1 = cases; _i < cases_1.length; _i++) { var _a = cases_1[_i], name_1 = _a.name, steps = _a.steps, repeat = _a.repeat; _loop_3(name_1, steps, repeat); } }); }; for (var _d = 0, configs_1 = configs; _d < configs_1.length; _d++) { var _e = configs_1[_d], fnName = _e.name, _handler = _e.handler, fn = _e.fn; _loop_2(fnName, _handler, fn); } }; for (var _i = 0, testSuites_1 = testSuites; _i < testSuites_1.length; _i++) { var _c = testSuites_1[_i], config = _c.config, cases = _c.cases; _loop_1(config, cases); } }; exports.runArvoTestSuites = runArvoTestSuites;