From 821fb5547836061da43a9e0803a81d146049627c Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Mon, 1 Dec 2025 13:11:59 -0800 Subject: [PATCH 1/3] chore(testing-sdk): improve exported types and jsdoc in testing library --- .../src/utils/test-helper.ts | 5 +- .../src/test-runner/__tests__/index.test.ts | 2 +- .../cloud-durable-test-runner.test.ts | 9 +- .../cloud/cloud-durable-test-runner.ts | 245 ++++++++++++--- .../__tests__/cloud-operation.test.ts | 2 +- .../cloud/operations/cloud-operation.ts | 5 +- .../test-runner/common/indexed-operations.ts | 8 +- .../__tests__/operation-with-data.test.ts | 2 +- .../common/operations/operation-with-data.ts | 83 +++-- .../src/test-runner/durable-test-runner.ts | 139 --------- .../src/test-runner/index.ts | 31 +- ...or-callback-operations.integration.test.ts | 2 +- .../interfaces/durable-test-runner-factory.ts | 7 +- .../local/local-durable-test-runner.ts | 156 +++++++--- .../__tests__/operation-wait-manager.test.ts | 2 +- .../__tests__/status-matcher.test.ts | 2 +- .../operations/local-operation-storage.ts | 2 +- .../operations/operation-wait-manager.ts | 15 +- .../local/operations/status-matcher.ts | 2 +- .../src/test-runner/local/result-formatter.ts | 2 +- .../local/test-execution-orchestrator.ts | 2 +- .../test-runner/types/durable-operation.ts | 285 ++++++++++++++++++ .../test-runner/types/durable-test-runner.ts | 179 +++++++++++ 23 files changed, 891 insertions(+), 296 deletions(-) delete mode 100644 packages/aws-durable-execution-sdk-js-testing/src/test-runner/durable-test-runner.ts create mode 100644 packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-operation.ts create mode 100644 packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-test-runner.ts diff --git a/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts b/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts index 4af1db2e..a598d812 100644 --- a/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts +++ b/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts @@ -1,3 +1,4 @@ +import { LambdaClient } from "@aws-sdk/client-lambda"; import { LocalDurableTestRunner, CloudDurableTestRunner, @@ -66,9 +67,9 @@ export function createTests(testDef: TestDefinition) { const runner = new CloudDurableTestRunner({ functionName, - clientConfig: { + client: new LambdaClient({ endpoint: process.env.LAMBDA_ENDPOINT, - }, + }), config: { invocationType: testDef.invocationType, }, diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts index 41a7ec49..e206169c 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts @@ -1,7 +1,7 @@ import * as indexExports from "../index"; import * as localExports from "../local"; import * as cloudExports from "../cloud"; -import * as durableTestRunnerExports from "../durable-test-runner"; +import * as durableTestRunnerExports from "../types/durable-test-runner"; describe("test-runner/index.ts exports", () => { it("should correctly re-export all exports from local", () => { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts index 5e9aebf5..2e6246b9 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts @@ -13,7 +13,8 @@ import { InvocationType, } from "@aws-sdk/client-lambda"; import { CloudDurableTestRunner } from "../cloud-durable-test-runner"; -import { TestResult, WaitingOperationStatus } from "../../durable-test-runner"; +import { TestResult } from "../../types/durable-test-runner"; +import { WaitingOperationStatus } from "../../types/durable-operation"; jest.mock("@aws-sdk/client-lambda"); @@ -125,7 +126,7 @@ describe("CloudDurableTestRunner", () => { expect(LambdaClient).toHaveBeenCalledWith({}); }); - it("should initialize with custom Lambda client configuration", () => { + it("should initialize with custom Lambda client", () => { const customConfig = { region: "us-west-2", credentials: { @@ -136,11 +137,11 @@ describe("CloudDurableTestRunner", () => { const runner = new CloudDurableTestRunner<{ success: boolean }>({ functionName: mockFunctionArn, - clientConfig: customConfig, + client: new LambdaClient(customConfig), }); expect(runner).toBeDefined(); - expect(LambdaClient).toHaveBeenCalledWith(customConfig); + expect(LambdaClient).toHaveBeenCalledTimes(1); }); it("should use custom poll interval", async () => { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts index 90a57b8e..f4a1feb8 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts @@ -1,6 +1,5 @@ import { InvokeCommand, - LambdaClientConfig, LambdaClient, GetDurableExecutionCommand, GetDurableExecutionHistoryCommand, @@ -8,15 +7,13 @@ import { } from "@aws-sdk/client-lambda"; import { IndexedOperations } from "../common/indexed-operations"; import { OperationStorage } from "../common/operation-storage"; -import { - OperationEvents, - OperationWithData, -} from "../common/operations/operation-with-data"; +import { OperationEvents } from "../common/operations/operation-with-data"; import { DurableTestRunner, InvokeRequest, TestResult, -} from "../durable-test-runner"; +} from "../types/durable-test-runner"; +import { DurableOperation } from "../types/durable-operation"; import { OperationWaitManager } from "../local/operations/operation-wait-manager"; import { ResultFormatter } from "../local/result-formatter"; import { HistoryPoller } from "./history-poller"; @@ -29,40 +26,101 @@ import { CloudOperation } from "./operations/cloud-operation"; export { InvocationType }; -interface CloudDurableTestRunnerConfigInternal { - pollInterval: number; - invocationType: InvocationType; +/** + * Options for the cloud durable test runner. + * @public + */ +export interface CloudDurableTestRunnerConfig { + /** + * Interval with wich the APIs for GetDurableExecutionHistory and GetDurableExecution + * are fetched from the API. + * + * @defaultValue 1000ms + */ + pollInterval?: number; + /** + * Invocation type for the Lambda invocation. + * + * @defaultValue {@link InvocationType.RequestResponse} + */ + invocationType?: InvocationType; } -export type CloudDurableTestRunnerConfig = - Partial; - +/** + * Parameters for creating a CloudDurableTestRunner instance. + * @public + */ export interface CloudDurableTestRunnerParameters { + /** The name or ARN of the Lambda function to invoke for testing */ functionName: string; - clientConfig?: LambdaClientConfig; + /** + * Optional AWS Lambda client + * @defaultValue new LambdaClient() + */ + client?: LambdaClient; + /** Optional configuration for the test runner */ config?: CloudDurableTestRunnerConfig; } -export class CloudDurableTestRunner - implements DurableTestRunner +/** + * A test runner for durable execution functions running in the AWS cloud. + * This runner invokes Lambda functions and polls for execution history to provide + * testing capabilities for durable operations. + * + * + * @example + * ```typescript + * const runner = new CloudDurableTestRunner({ + * functionName: 'my-durable-function', + * }); + * + * const execution = await runner.run({ payload: { input: 'test' } }); + * const result = execution.getResult(); + * const stepOperation = runner.getOperation('myStep'); + * ``` + * + * @example + * ```typescript + * const runner = new CloudDurableTestRunner({ + * functionName: 'my-durable-function', + * client: new LambdaClient({ region: 'us-east-1' }), + * config: { pollInterval: 500 } + * }); + * + * const execution = await runner.run({ payload: { input: 'test' } }); + * const result = execution.getResult(); + * const stepOperation = runner.getOperation('myStep'); + * ``` + * + * @public + */ +export class CloudDurableTestRunner< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TResult = any, +> implements DurableTestRunner { - private readonly functionArn: string; + private readonly functionName: string; private readonly client: LambdaClient; - private readonly formatter = new ResultFormatter(); + private readonly formatter = new ResultFormatter(); private indexedOperations = new IndexedOperations([]); private waitManager = new OperationWaitManager(this.indexedOperations); private operationStorage: OperationStorage; private readonly apiClient: DurableApiClient; - private readonly config: CloudDurableTestRunnerConfigInternal; + private readonly config: Required; + /** + * Creates a new CloudDurableTestRunner instance. + * + * @param params - Configuration parameters for the test runner + */ constructor({ - functionName: functionArn, - clientConfig, + functionName, + client, config, }: CloudDurableTestRunnerParameters) { - this.client = new LambdaClient(clientConfig ?? {}); + this.client = client ?? new LambdaClient(); this.apiClient = createDurableApiClient(() => this.client); - this.functionArn = functionArn; + this.functionName = functionName; this.operationStorage = new OperationStorage( this.waitManager, this.indexedOperations, @@ -75,10 +133,31 @@ export class CloudDurableTestRunner }; } - async run(params?: InvokeRequest): Promise> { + /** + * Runs the durable function and returns the test result. + * + * This method invokes the Lambda function, polls for execution history, + * and returns a comprehensive test result containing the function output + * and all operation details. + * + * @param params - Optional parameters for the function invocation + * @returns A promise that resolves to the test result containing function output and operation history + * @throws Will throw an error if no execution ARN is returned from the Lambda invocation which can occur + * if the function is not a durable function. + * + * @example + * ```typescript + * const result = await runner.run({ + * payload: { userId: '123', action: 'process' } + * }); + * console.log('Function result:', result.getResult()); + * console.log('Operations count:', result.getOperations().length); + * ``` + */ + async run(params?: InvokeRequest): Promise> { const asyncInvokeResult = await this.client.send( new InvokeCommand({ - FunctionName: this.functionArn, + FunctionName: this.functionName, Payload: params?.payload ? JSON.stringify(params.payload) : undefined, InvocationType: this.config.invocationType, }), @@ -87,7 +166,9 @@ export class CloudDurableTestRunner const durableExecutionArn = asyncInvokeResult.DurableExecutionArn; if (!durableExecutionArn) { - throw new Error("No execution ARN found on invocation response"); + throw new Error( + "No execution ARN found on invocation response. Is the function specified a durable function?", + ); } const testExecutionState = new TestExecutionState(); @@ -130,12 +211,51 @@ export class CloudDurableTestRunner } } - getOperation(name: string): CloudOperation { + /** + * Gets an operation by name, defaulting to the first occurrence (index 0). + * + * This is a convenience method equivalent to calling `getOperationByNameAndIndex(name, 0)`. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param name - The name of the operation to retrieve + * @returns An operation instance that can be used to inspect operation details + * + * @example + * ```typescript + * const stepOp = runner.getOperation('processData'); + * await stepOp.waitForData(); + * const details = stepOp.getStepDetails(); + * ``` + */ + getOperation< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(name: string): DurableOperation { return this.getOperationByNameAndIndex(name, 0); } - getOperationByIndex(index: number): CloudOperation { - const operation = new CloudOperation( + /** + * Gets an operation by its execution order index. + * + * Operations are indexed in the order they were executed, starting from 0. + * This method is useful when you know the execution order of operations. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param index - The zero-based index of the operation in execution order + * @returns An operation instance for the operation at the specified index + * + * @example + * ```typescript + * // Get the first operation that was executed + * const firstOp = runner.getOperationByIndex(0); + * await firstOp.waitForData(); + * ``` + */ + getOperationByIndex< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(index: number): DurableOperation { + const operation = new CloudOperation( this.waitManager, this.indexedOperations, this.apiClient, @@ -151,11 +271,30 @@ export class CloudDurableTestRunner return operation; } - getOperationByNameAndIndex( - name: string, - index: number, - ): CloudOperation { - const operation = new CloudOperation( + /** + * Gets an operation by name and index when multiple operations have the same name. + * + * When a durable function executes the same named operation multiple times, + * this method allows you to access a specific occurrence by its index. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param name - The name of the operation + * @param index - The zero-based index among operations with the same name + * @returns An operation instance for the specified named operation occurrence + * + * @example + * ```typescript + * // Get the second occurrence of an operation named 'validateInput' + * const secondValidation = runner.getOperationByNameAndIndex('validateInput', 1); + * await secondValidation.waitForData(); + * const isValid = secondValidation.getStepDetails()?.result; + * ``` + */ + getOperationByNameAndIndex< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(name: string, index: number): DurableOperation { + const operation = new CloudOperation( this.waitManager, this.indexedOperations, this.apiClient, @@ -172,8 +311,28 @@ export class CloudDurableTestRunner return operation; } - getOperationById(id: string): CloudOperation { - const operation = new CloudOperation( + /** + * Gets an operation by its unique identifier. + * + * Each operation in a durable execution has a unique ID. This method + * allows you to retrieve an operation when you know its specific ID. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param id - The unique identifier of the operation + * @returns An operation instance for the operation with the specified ID + * + * @example + * ```typescript + * const operation = runner.getOperationById('op-abc123'); + * await operation.waitForData(); + * const result = operation.getContextDetails()?.result; + * ``` + */ + getOperationById< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(id: string): DurableOperation { + const operation = new CloudOperation( this.waitManager, this.indexedOperations, this.apiClient, @@ -189,6 +348,22 @@ export class CloudDurableTestRunner return operation; } + /** + * Resets the test runner state, clearing all cached operations and history. + * + * This method should be called between test runs to ensure a clean state. + * It clears the operation index, wait manager, and operation storage, + * allowing the runner to be reused for multiple test executions. + * + * @example + * ```typescript + * beforeEach(() => { + * runner.reset(); + * }); + * ``` + * + * @beta + */ reset() { this.indexedOperations = new IndexedOperations([]); this.waitManager = new OperationWaitManager(this.indexedOperations); diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/__tests__/cloud-operation.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/__tests__/cloud-operation.test.ts index 76a2cb66..15ee2cc7 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/__tests__/cloud-operation.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/__tests__/cloud-operation.test.ts @@ -3,7 +3,7 @@ import { CloudOperation } from "../cloud-operation"; import { OperationWaitManager } from "../../../local/operations/operation-wait-manager"; import { IndexedOperations } from "../../../common/indexed-operations"; import { DurableApiClient } from "../../../common/create-durable-api-client"; -import { WaitingOperationStatus } from "../../../durable-test-runner"; +import { WaitingOperationStatus } from "../../../types/durable-operation"; describe("CloudOperation", () => { const waitManager = new OperationWaitManager(); diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/cloud-operation.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/cloud-operation.ts index 4b19950c..f78c6563 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/cloud-operation.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/operations/cloud-operation.ts @@ -5,9 +5,12 @@ import { OperationEvents, OperationWithData, } from "../../common/operations/operation-with-data"; -import { WaitingOperationStatus } from "../../durable-test-runner"; +import { WaitingOperationStatus } from "../../types/durable-operation"; import { OperationWaitManager } from "../../local/operations/operation-wait-manager"; +/** + * @internal + */ export class CloudOperation< OperationResultValue = unknown, > extends OperationWithData { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/indexed-operations.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/indexed-operations.ts index 201fc742..c73cc663 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/indexed-operations.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/indexed-operations.ts @@ -5,6 +5,8 @@ import { Event, OperationType } from "@aws-sdk/client-lambda"; * Optimized way of retrieving operations by id and name/index. * * Avoids re-iterating over the operations list every time an operation needs to be fetched. + * + * @internal */ export class IndexedOperations { private executionOperation: OperationEvents | undefined = undefined; @@ -101,7 +103,7 @@ export class IndexedOperations { /** * Get an operation by its ID - * @param id The operation ID + * @param id - The operation ID * @returns The operation with the matching ID */ getById(id: string): OperationEvents | undefined { @@ -115,8 +117,8 @@ export class IndexedOperations { /** * Get an operation by name and index - * @param name The operation name - * @param index The index of the operation among operations with the same name. Defaults to 0 + * @param name - The operation name + * @param index - The index of the operation among operations with the same name. Defaults to 0 * @returns The operation at the specified name and index */ getByNameAndIndex(name: string, index = 0): OperationEvents | undefined { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/__tests__/operation-with-data.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/__tests__/operation-with-data.test.ts index 5636714c..df3f8ec5 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/__tests__/operation-with-data.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/__tests__/operation-with-data.test.ts @@ -5,7 +5,7 @@ import { } from "@aws-sdk/client-lambda"; import { OperationSubType } from "@aws/durable-execution-sdk-js"; -import { WaitingOperationStatus } from "../../../durable-test-runner"; +import { WaitingOperationStatus } from "../../../types/durable-operation"; import { OperationWaitManager } from "../../../local/operations/operation-wait-manager"; import { OperationWithData } from "../operation-with-data"; import { IndexedOperations } from "../../indexed-operations"; diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/operation-with-data.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/operation-with-data.ts index 1b406e4a..b65c3afd 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/operation-with-data.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/common/operations/operation-with-data.ts @@ -8,11 +8,15 @@ import { SendDurableExecutionCallbackHeartbeatResponse, SendDurableExecutionCallbackSuccessResponse, } from "@aws-sdk/client-lambda"; +import { WaitingOperationStatus } from "../../types/durable-operation"; import { DurableOperation, - TestResultError, - WaitingOperationStatus, -} from "../../durable-test-runner"; + CallbackDetails, + ChainedInvokeDetails, + ContextDetails, + StepDetails, + WaitResultDetails, +} from "../../types/durable-operation"; import { OperationWaitManager } from "../../local/operations/operation-wait-manager"; import { doesStatusMatch } from "../../local/operations/status-matcher"; import { tryJsonParse } from "../utils"; @@ -21,42 +25,32 @@ import { transformErrorObjectToErrorResult } from "../../../utils"; import { OperationSubType } from "@aws/durable-execution-sdk-js"; import { DurableApiClient } from "../create-durable-api-client"; -export interface OperationResultContextDetails { - readonly result: ResultValue | undefined; - readonly error: TestResultError | undefined; -} - -export interface OperationResultStepDetails { - readonly attempt: number | undefined; - readonly nextAttemptTimestamp: Date | undefined; - readonly result: ResultValue | undefined; - readonly error: TestResultError | undefined; -} - -export interface OperationResultCallbackDetails { - readonly callbackId: string; - readonly error?: TestResultError; - readonly result?: ResultValue; -} - -export interface OperationResultChainedInvokeDetails { - readonly error?: TestResultError; - readonly result?: ResultValue; -} - -export interface WaitResultDetails { - readonly waitSeconds?: number; - readonly scheduledEndTimestamp?: Date; -} - +/** + * Container for operation data and associated events. + * @public + */ export interface OperationEvents { + /** The operation data */ operation: Operation; + /** The list of events associated with the operation */ events: Event[]; } -export class OperationWithData< - OperationResultValue = unknown, -> implements DurableOperation { +/** + * An instance of an operation. This operation may or may not have data available, depending on + * the current state of the execution. + * @internal + */ +export class OperationWithData + implements DurableOperation +{ + /** + * Creates a new OperationWithData instance. + * @param waitManager - Manager for waiting on operation status changes + * @param operationIndex - Index of operations for finding related operations + * @param apiClient - Client for making API calls to the durable execution service + * @param checkpointOperationData - Optional operation data from checkpoint + */ constructor( private readonly waitManager: OperationWaitManager, private readonly operationIndex: IndexedOperations, @@ -81,9 +75,7 @@ export class OperationWithData< return this; } - getContextDetails(): - | OperationResultContextDetails - | undefined { + getContextDetails(): ContextDetails | undefined { const operationData = this.getOperationData(); if (!operationData) { @@ -102,9 +94,7 @@ export class OperationWithData< }; } - getStepDetails(): - | OperationResultStepDetails - | undefined { + getStepDetails(): StepDetails | undefined { const operationData = this.getOperationData(); if (!operationData) { @@ -126,7 +116,7 @@ export class OperationWithData< } private getWaitForCallbackDetails(): - | OperationResultCallbackDetails + | CallbackDetails | undefined { const createCallbackOperation = this.getChildOperations() ?.find((operation) => operation.getType() === OperationType.CALLBACK) @@ -143,7 +133,7 @@ export class OperationWithData< private getCreateCallbackDetails( operationData: Operation, - ): OperationResultCallbackDetails | undefined { + ): CallbackDetails | undefined { const callbackDetails = operationData.CallbackDetails; if (callbackDetails?.CallbackId === undefined) { throw new Error("Could not find callback ID in callback details"); @@ -157,7 +147,7 @@ export class OperationWithData< } getChainedInvokeDetails(): - | OperationResultChainedInvokeDetails + | ChainedInvokeDetails | undefined { const operationData = this.getOperationData(); @@ -189,9 +179,7 @@ export class OperationWithData< }; } - getCallbackDetails(): - | OperationResultCallbackDetails - | undefined { + getCallbackDetails(): CallbackDetails | undefined { const operationData = this.getOperationData(); if (!operationData) { @@ -261,9 +249,6 @@ export class OperationWithData< ); } - // TODO: need subtypes to properly get operations by path for map/parallel - // getChildOperationByPath(path: (string | number)[]): EnhancedOperationData; - getOperationData(): Operation | undefined { return this.checkpointOperationData?.operation; } diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/durable-test-runner.ts deleted file mode 100644 index 8dbd5b91..00000000 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/durable-test-runner.ts +++ /dev/null @@ -1,139 +0,0 @@ -// Main interface for customers to interface with for both the local -// and cloud-based test runners. - -import { - ErrorObject, - OperationStatus, - Event, - ExecutionStatus, - SendDurableExecutionCallbackHeartbeatResponse, - SendDurableExecutionCallbackFailureResponse, - SendDurableExecutionCallbackSuccessResponse, -} from "@aws-sdk/client-lambda"; -import { OperationWithData } from "./common/operations/operation-with-data"; - -export enum WaitingOperationStatus { - /** - * Fires when the operation starts. - */ - STARTED = "STARTED", - /** - * Submitted is the same as COMPLETED, except for the case where the operation is waitForCallback. - * In that case, SUBMITTED will fire when the waitForCallback submitter is completed. - */ - SUBMITTED = "SUBMITTED", - /** - * Fires when the operation is completed. This includes a status of CANCELLED, FAILED, STOPPED, - * SUCCEEDED, or TIMED_OUT. - */ - COMPLETED = "COMPLETED", -} - -// TODO: use the real Lambda InvokeRequest parameters -export interface InvokeRequest { - payload?: unknown; -} - -export interface DurableTestRunner< - T extends DurableOperation, - ResultType, -> { - // Run the durable execution and return the result/error. - run(params?: InvokeRequest): Promise>; - - // Methods to get the operation to assert on the operation result - - // Get the first operation with this name - getOperation(name: string): T; - // Get the operation called at this index. - getOperationByIndex(index: number): T; - // If a name is passed, get the operation by index filtered by name. - getOperationByNameAndIndex(name: string, index: number): T; - // Get the operation with this ID - getOperationById(id: string): T; - // Get the first operation with this path. - // TODO: add when parallel/map are added - // getOperationByPath(path: (string | number)[]): T; -} - -// Interfaces for individual operation level interactions - -export interface DurableOperation extends OperationWithData { - waitForData( - status?: WaitingOperationStatus, - ): Promise>; - - // Callback APIs - sendCallbackSuccess( - result?: string, - ): Promise; - sendCallbackFailure( - error?: ErrorObject, - ): Promise; - sendCallbackHeartbeat(): Promise; -} - -// Interfaces available after the completion of the execution from the `run` method - -export interface TestResultError { - errorMessage: string | undefined; - errorType: string | undefined; - errorData: string | undefined; - stackTrace: string[] | undefined; -} - -export interface TestResult { - // Returns a list of all completed operations - getOperations(params?: { - // Filter by operation status (completed, failed, etc.) - status: OperationStatus; - }): OperationWithData[]; - - // Description of the individual invocations to the Lambda handler. - // Can be used to assert on the data for a specific invocation and validate - // the number of invocations that were run. - getInvocations(): Invocation[]; - - /** - * Gets the status of the execution. - */ - getStatus(): ExecutionStatus | undefined; - - /** - * Gets the result of the execution. - * @throws The execution error if the execution failed. - */ - getResult(): T | undefined; - /** - * Gets the error from an execution. - * @throws An error if the execution succeeded. - */ - getError(): TestResultError; - - /** - * Returns the history events for the execution. - */ - getHistoryEvents(): Event[]; - - /** - * Prints a table of all operations to the console with their details. - */ - print(config?: { - parentId?: boolean; - name?: boolean; - type?: boolean; - subType?: boolean; - status?: boolean; - startTime?: boolean; - endTime?: boolean; - duration?: boolean; - }): void; -} - -// Tracks a single invocation of the handler function -export interface Invocation { - startTimestamp: Date | undefined; - endTimestamp: Date | undefined; - requestId: string | undefined; - error: TestResultError | undefined; -} diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts index 85d80d22..6abb5585 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts @@ -1,3 +1,28 @@ -export * from "./local"; -export * from "./cloud"; -export * from "./durable-test-runner"; +export { LocalDurableTestRunner } from "./local"; +export type { + LocalDurableTestRunnerParameters, + LocalDurableTestRunnerSetupParameters, +} from "./local"; + +export { CloudDurableTestRunner, InvocationType } from "./cloud"; +export type { + CloudDurableTestRunnerConfig, + CloudDurableTestRunnerParameters, +} from "./cloud"; + +export type { + InvokeRequest, + Invocation, + DurableTestRunner, + TestResultError, + TestResult, +} from "./types/durable-test-runner"; + +export type { + DurableOperation, + ContextDetails as OperationResultContextDetails, + StepDetails as OperationResultStepDetails, + CallbackDetails as OperationResultCallbackDetails, + ChainedInvokeDetails as OperationResultChainedInvokeDetails, + WaitResultDetails, +} from "./types/durable-operation"; diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/wait-for-callback-operations.integration.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/wait-for-callback-operations.integration.test.ts index 74f72f3f..afd8841c 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/wait-for-callback-operations.integration.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/wait-for-callback-operations.integration.test.ts @@ -1,5 +1,5 @@ import { LocalDurableTestRunner } from "../../local-durable-test-runner"; -import { WaitingOperationStatus } from "../../../durable-test-runner"; +import { WaitingOperationStatus } from "../../../types/durable-operation"; import { DurableContext, withDurableExecution, diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/interfaces/durable-test-runner-factory.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/interfaces/durable-test-runner-factory.ts index a7586df2..7f2bcf0d 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/interfaces/durable-test-runner-factory.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/interfaces/durable-test-runner-factory.ts @@ -1,8 +1,9 @@ import { DurableLambdaHandler } from "@aws/durable-execution-sdk-js"; -import { InvokeRequest, TestResult } from "../../durable-test-runner"; +import { InvokeRequest, TestResult } from "../../types/durable-test-runner"; /** * Configuration parameters for LocalDurableTestRunner. + * @public */ export interface LocalDurableTestRunnerParameters { /** The handler function to run the execution on */ @@ -11,6 +12,7 @@ export interface LocalDurableTestRunnerParameters { /** * Factory interface for creating local durable test runner instances. + * @internal */ export interface ILocalDurableTestRunnerFactory { /** @@ -25,6 +27,9 @@ export interface ILocalDurableTestRunnerFactory { ): ILocalDurableTestRunnerExecutor; } +/** + * @internal + */ export interface ILocalDurableTestRunnerExecutor { /** * Executes the durable function and returns the result. diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts index 948a26ab..d75ea836 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts @@ -2,7 +2,8 @@ import { TestResult, InvokeRequest, DurableTestRunner, -} from "../durable-test-runner"; +} from "../types/durable-test-runner"; +import { DurableOperation } from "../types/durable-operation"; import { DurableLambdaHandler } from "@aws/durable-execution-sdk-js"; import { LocalOperationStorage } from "./operations/local-operation-storage"; import { OperationWaitManager } from "./operations/operation-wait-manager"; @@ -27,13 +28,22 @@ import { } from "@aws-sdk/client-lambda"; import { CheckpointWorkerApiClient } from "./api-client/checkpoint-worker-api-client"; -export type LocalTestRunnerHandlerFunction = DurableLambdaHandler; - export type { LocalDurableTestRunnerParameters }; -export class LocalDurableTestRunnerFactory implements ILocalDurableTestRunnerFactory { +/** + * Factory for creating LocalDurableTestRunner instances. + * Used internally to support nested function execution during testing. + * @internal + */ +export class LocalDurableTestRunnerFactory + implements ILocalDurableTestRunnerFactory +{ /** - * Creates new runner instances for nested function execution + * Creates new runner instances for nested function execution. + * + * @typeParam T - The expected result type of the durable function + * @param params - Configuration parameters for the test runner + * @returns A new LocalDurableTestRunner instance */ createRunner( params: LocalDurableTestRunnerParameters, @@ -84,14 +94,37 @@ export interface LocalDurableTestRunnerSetupParameters { /** * Local test runner for durable executions that runs handlers in-process * with a local checkpoint server for development and testing scenarios. + * + * This test runner executes durable functions locally without requiring + * AWS Lambda infrastructure, making it ideal for unit testing and local + * development workflows. + * + * @typeParam ResultType - The expected result type of the durable function + * + * @example + * ```typescript + * import { LocalDurableTestRunner } from '@aws/durable-execution-sdk-js-testing'; + * import { withDurableExecution } from '@aws/durable-execution-sdk-js'; + * + * const handler = withDurableExecution(async (input, context) => { + * const result = await context.step('process', () => processData(input)); + * return result; + * }); + * + * const runner = new LocalDurableTestRunner({ handlerFunction: handler }); + * + * const execution = await runner.run({ payload: { data: 'test' } }); + * const result = execution.getResult(); + * ``` + * + * @public */ -export class LocalDurableTestRunner implements DurableTestRunner< - OperationWithData, - ResultType -> { +export class LocalDurableTestRunner + implements DurableTestRunner +{ private operationStorage: LocalOperationStorage; private waitManager: OperationWaitManager; - private readonly resultFormatter: ResultFormatter; + private readonly resultFormatter: ResultFormatter; private operationIndex: IndexedOperations; static skipTime = false; static fakeClock: InstalledClock | undefined; @@ -102,13 +135,12 @@ export class LocalDurableTestRunner implements DurableTestRunner< /** * Creates a new LocalDurableTestRunner instance and starts the checkpoint server. * - * @param params Configuration parameters - * @param params.handlerFunction The durable function handler to execute + * @param params - Configuration parameters */ constructor({ handlerFunction }: LocalDurableTestRunnerParameters) { this.operationIndex = new IndexedOperations([]); this.waitManager = new OperationWaitManager(this.operationIndex); - this.resultFormatter = new ResultFormatter(); + this.resultFormatter = new ResultFormatter(); this.handlerFunction = handlerFunction; @@ -161,10 +193,10 @@ export class LocalDurableTestRunner implements DurableTestRunner< * The method will not resolve until the handler function completes successfully * or throws an error. * - * @param params Optional parameters for the execution + * @param params - Optional parameters for the execution * @returns Promise that resolves with the execution result */ - async run(params?: InvokeRequest): Promise> { + async run(params?: InvokeRequest): Promise> { try { const orchestrator = new TestExecutionOrchestrator( this.handlerFunction, @@ -270,12 +302,19 @@ export class LocalDurableTestRunner implements DurableTestRunner< return this; } - // Inherited methods from DurableTestRunner - getOperation( - name: string, - index?: number, - ): OperationWithData { - const operation = new OperationWithData( + /** + * Gets the first operation with the specified name. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param name - The name of the operation to retrieve + * @param index - Optional index for operations with the same name (defaults to 0) + * @returns An operation instance that can be used to inspect operation details + */ + getOperation< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(name: string, index?: number): DurableOperation { + const operation = new OperationWithData( this.waitManager, this.operationIndex, this.durableApi, @@ -290,10 +329,18 @@ export class LocalDurableTestRunner implements DurableTestRunner< return operation; } - getOperationByIndex( - index: number, - ): OperationWithData { - const operation = new OperationWithData( + /** + * Gets an operation by its execution order index. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param index - The zero-based index of the operation in execution order + * @returns An operation instance for the operation at the specified index + */ + getOperationByIndex< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(index: number): DurableOperation { + const operation = new OperationWithData( this.waitManager, this.operationIndex, this.durableApi, @@ -307,11 +354,19 @@ export class LocalDurableTestRunner implements DurableTestRunner< return operation; } - getOperationByNameAndIndex( - name: string, - index: number, - ): OperationWithData { - const operation = new OperationWithData( + /** + * Gets an operation by name and index when multiple operations have the same name. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param name - The name of the operation + * @param index - The zero-based index among operations with the same name + * @returns An operation instance for the specified named operation occurrence + */ + getOperationByNameAndIndex< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(name: string, index: number): DurableOperation { + const operation = new OperationWithData( this.waitManager, this.operationIndex, this.durableApi, @@ -326,10 +381,18 @@ export class LocalDurableTestRunner implements DurableTestRunner< return operation; } - getOperationById( - id: string, - ): OperationWithData { - const operation = new OperationWithData( + /** + * Gets an operation by its unique identifier. + * + * @typeParam TOperationResult - The expected result type of the operation + * @param id - The unique identifier of the operation + * @returns An operation instance for the operation with the specified ID + */ + getOperationById< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TOperationResult = any, + >(id: string): DurableOperation { + const operation = new OperationWithData( this.waitManager, this.operationIndex, this.durableApi, @@ -343,6 +406,20 @@ export class LocalDurableTestRunner implements DurableTestRunner< return operation; } + /** + * Resets the test runner state, clearing all cached operations and history. + * + * This method should be called between test runs to ensure a clean state. + * It clears the operation index, wait manager, and operation storage, + * allowing the runner to be reused for multiple test executions. + * + * @example + * ```typescript + * beforeEach(() => { + * runner.reset(); + * }); + * ``` + */ reset() { this.operationIndex = new IndexedOperations([]); this.waitManager = new OperationWaitManager(this.operationIndex); @@ -362,9 +439,6 @@ export class LocalDurableTestRunner implements DurableTestRunner< * once before running any tests, typically in a test setup hook like `beforeAll`. * * @param params - Optional configuration parameters for the test environment - * @param params.skipTime - Whether to enable time skipping using fake timers. When true, - * allows tests to skip over time-based operations like `setTimeout`, `setInterval`, `context.wait`, - * and `context.step` retries without actually waiting for the specified duration. * * @returns Promise that resolves when the test environment setup is complete * @@ -388,9 +462,9 @@ export class LocalDurableTestRunner implements DurableTestRunner< * @remarks * - If fake timers are already installed (for example, if `jest.useFakeTimers()` was called previously), * this function will throw an error and setup will not succeed. - * - Must be paired with {@link teardownTestEnvironment} to properly clean up resources + * - Must be paired with {@link LocalDurableTestRunner.teardownTestEnvironment} to properly clean up resources * - * @see {@link teardownTestEnvironment} for cleaning up the test environment + * @see {@link LocalDurableTestRunner.teardownTestEnvironment} for cleaning up the test environment * @see {@link LocalDurableTestRunnerSetupParameters} for configuration options */ static async setupTestEnvironment( @@ -447,11 +521,11 @@ export class LocalDurableTestRunner implements DurableTestRunner< * @remarks * - This method safely uninstalls fake timers that were installed during setup if * skipTime was enabled. - * - Must be called after {@link setupTestEnvironment} to prevent resource leaks + * - Must be called after {@link LocalDurableTestRunner.setupTestEnvironment} to prevent resource leaks * - Failure to call this method may leave the checkpoint server running and * consume system resources * - * @see {@link setupTestEnvironment} for initializing the test environment + * @see {@link LocalDurableTestRunner.setupTestEnvironment} for initializing the test environment */ static async teardownTestEnvironment() { this.fakeClock?.uninstall(); diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/operation-wait-manager.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/operation-wait-manager.test.ts index e53690a5..d407113f 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/operation-wait-manager.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/operation-wait-manager.test.ts @@ -1,6 +1,6 @@ import { OperationWaitManager } from "../operation-wait-manager"; import { OperationStatus, OperationType } from "@aws-sdk/client-lambda"; -import { WaitingOperationStatus } from "../../../durable-test-runner"; +import { WaitingOperationStatus } from "../../../types/durable-operation"; import { IndexedOperations } from "../../../common/indexed-operations"; import { OperationEvents, diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/status-matcher.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/status-matcher.test.ts index 04ab8df8..7c053875 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/status-matcher.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/__tests__/status-matcher.test.ts @@ -1,5 +1,5 @@ import { OperationStatus } from "@aws-sdk/client-lambda"; -import { WaitingOperationStatus } from "../../../durable-test-runner"; +import { WaitingOperationStatus } from "../../../types/durable-operation"; import { doesStatusMatch } from "../status-matcher"; describe("doesStatusMatch", () => { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/local-operation-storage.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/local-operation-storage.ts index 8f556007..b04d4736 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/local-operation-storage.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/local-operation-storage.ts @@ -1,6 +1,6 @@ import { IndexedOperations } from "../../common/indexed-operations"; import { OperationEvents } from "../../common/operations/operation-with-data"; -import { DurableOperation } from "../../durable-test-runner"; +import { DurableOperation } from "../../types/durable-operation"; import { OperationWaitManager } from "./operation-wait-manager"; import { OperationStorage } from "../../common/operation-storage"; import { Event } from "@aws-sdk/client-lambda"; diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/operation-wait-manager.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/operation-wait-manager.ts index 4fd17188..80b48fe8 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/operation-wait-manager.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/operation-wait-manager.ts @@ -3,10 +3,8 @@ import { OperationStatus, OperationType, } from "@aws-sdk/client-lambda"; -import { - DurableOperation, - WaitingOperationStatus, -} from "../../durable-test-runner"; +import { WaitingOperationStatus } from "../../types/durable-operation"; +import { DurableOperation } from "../../types/durable-operation"; import { doesStatusMatch } from "./status-matcher"; import { OperationEvents } from "../../common/operations/operation-with-data"; import { IndexedOperations } from "../../common/indexed-operations"; @@ -21,6 +19,7 @@ interface WaitingOperation { /** * Manages waiting operations and promise resolution for mock operations. + * @internal */ export class OperationWaitManager { private readonly waitingOperations = new Set(); @@ -29,8 +28,8 @@ export class OperationWaitManager { /** * Creates a promise that resolves when the specified operation reaches the expected status. - * @param operation The mock operation to wait for - * @param status The expected status (defaults to STARTED) + * @param operation - The mock operation to wait for + * @param status - The expected status (defaults to STARTED) * @returns Promise that resolves with the mock operation when the status is reached */ waitForOperation( @@ -80,8 +79,8 @@ export class OperationWaitManager { /** * Handles checkpoint operations and resolves waiting operations. - * @param checkpointOperationsReceived All checkpoint operations received - * @param trackedDurableOperations Operations that just got populated with data + * @param checkpointOperationsReceived - All checkpoint operations received + * @param trackedDurableOperations - Operations that just got populated with data */ handleCheckpointReceived( checkpointOperationsReceived: OperationEvents[], diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/status-matcher.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/status-matcher.ts index bf4a48fb..dbfdb844 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/status-matcher.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/operations/status-matcher.ts @@ -1,5 +1,5 @@ import { OperationStatus } from "@aws-sdk/client-lambda"; -import { WaitingOperationStatus } from "../../durable-test-runner"; +import { WaitingOperationStatus } from "../../types/durable-operation"; const STARTED_STATUSES: OperationStatus[] = Object.values(OperationStatus); const COMPLETED_STATUSES: OperationStatus[] = [ diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/result-formatter.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/result-formatter.ts index 5f595886..f7d744b3 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/result-formatter.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/result-formatter.ts @@ -1,4 +1,4 @@ -import { TestResult, TestResultError } from "../durable-test-runner"; +import { TestResult, TestResultError } from "../types/durable-test-runner"; import { tryJsonParse } from "../common/utils"; import { TestExecutionResult } from "../common/test-execution-state"; import { Event, EventType, ExecutionStatus } from "@aws-sdk/client-lambda"; diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/test-execution-orchestrator.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/test-execution-orchestrator.ts index b5197e50..f57d032a 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/test-execution-orchestrator.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/test-execution-orchestrator.ts @@ -16,7 +16,7 @@ import { TestExecutionResult, TestExecutionState, } from "../common/test-execution-state"; -import { InvokeRequest } from "../durable-test-runner"; +import { InvokeRequest } from "../types/durable-test-runner"; import { OperationAction, OperationType, diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-operation.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-operation.ts new file mode 100644 index 00000000..80d85514 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-operation.ts @@ -0,0 +1,285 @@ +import { + Operation, + Event, + OperationType, + OperationStatus, + SendDurableExecutionCallbackSuccessResponse, + ErrorObject, + SendDurableExecutionCallbackFailureResponse, + SendDurableExecutionCallbackHeartbeatResponse, +} from "@aws-sdk/client-lambda"; +import { OperationSubType } from "@aws/durable-execution-sdk-js"; +import { TestResultError } from "./durable-test-runner"; + +/** + * Details for a context operation result, containing either a successful result or an error. + * @public + */ +export interface ContextDetails { + /** The successful result of the context operation, if available */ + readonly result: TResult | undefined; + /** The error that occurred during the context operation, if any */ + readonly error: TestResultError | undefined; +} + +/** + * Details for a step operation result, including retry information and outcome. + * @public + */ +export interface StepDetails { + /** The current attempt number for this step operation */ + readonly attempt: number | undefined; + /** The timestamp when the next attempt will be made, if retries are scheduled */ + readonly nextAttemptTimestamp: Date | undefined; + /** The successful result of the step operation, if available */ + readonly result: TResult | undefined; + /** The error that occurred during the step operation, if any */ + readonly error: TestResultError | undefined; +} + +/** + * Details for a callback operation result, including the callback identifier and outcome. + * @public + */ +export interface CallbackDetails { + /** The unique identifier for the callback operation */ + readonly callbackId: string; + /** The error that occurred during the callback operation, if any */ + readonly error?: TestResultError; + /** The successful result of the callback operation, if available */ + readonly result?: TResult; +} + +/** + * Details for a chained invoke operation result. + * @public + */ +export interface ChainedInvokeDetails { + /** The error that occurred during the chained invoke operation, if any */ + readonly error?: TestResultError; + /** The successful result of the chained invoke operation, if available */ + readonly result?: TResult; +} + +/** + * Details for a wait operation, including duration and scheduling information. + * @public + */ +export interface WaitResultDetails { + /** The duration of the wait operation in seconds */ + readonly waitSeconds?: number; + /** The timestamp when the wait operation is scheduled to complete */ + readonly scheduledEndTimestamp?: Date; +} + +/** + * An enum for different available waiting operation statuses. + * + * @example + * ```typescript + * const wait = runner.getOperation("my-wait") + * const myWait = await wait.waitForData() + * assert(myWait.getWaitDetails()?.waitSeconds === 10) + * ``` + * + * @example + * ```typescript + * const callback = runner.getOperation("my-callback") + * const myCallback = await callback.waitForData(WaitingOperationStatus.SUBMITTED) + * myCallback.sendCallbackSuccess("Hello World") + * ``` + * @public + */ +export enum WaitingOperationStatus { + /** + * Fires when the operation starts. + */ + STARTED = "STARTED", + /** + * Submitted is the same as COMPLETED, except for the case where the operation is waitForCallback. + * In that case, SUBMITTED will fire when the waitForCallback submitter is completed. + */ + SUBMITTED = "SUBMITTED", + /** + * Fires when the operation is completed. This includes a status of CANCELLED, FAILED, STOPPED, + * SUCCEEDED, or TIMED_OUT. + */ + COMPLETED = "COMPLETED", +} + +/** + * Interface for individual operation level interactions in durable executions. + * Provides methods to inspect operation details, wait for data, and send callback responses. + * @public + */ +export interface DurableOperation< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TResult = any, +> { + /** + * Wait for data for the current operation. If data is not found by the time + * the execution completes, this function will throw an error. + * + * @example + * ```typescript + * const wait = runner.getOperation("my-wait") + * const myWait = await wait.waitForData() + * assert(myWait.getWaitDetails()?.waitSeconds === 10) + * ``` + * + * @example + * ```typescript + * const callback = runner.getOperation("my-callback") + * const myCallback = await callback.waitForData(WaitingOperationStatus.SUBMITTED) + * myCallback.sendCallbackSuccess("Hello World") + * ``` + * + * @param status - The operation status to wait for. Default value is STARTED + * @returns A promise that resolves to this OperationWithData instance once the specified status is reached + * @throws Will throw an error if data is not available when the execution completes + */ + waitForData( + status?: WaitingOperationStatus, + ): Promise>; + + /** + * Gets the details for a context operation. + * @returns The context operation details, or undefined if no operation data is available + * @throws Will throw an error if the operation type is not CONTEXT + */ + getContextDetails(): ContextDetails | undefined; + + /** + * Gets the details for a step operation. + * @returns The step operation details, or undefined if no operation data is available + * @throws Will throw an error if the operation type is not STEP + */ + getStepDetails(): StepDetails | undefined; + + /** + * Gets the details for a chained invoke operation. + * @returns The chained invoke operation details, or undefined if no operation data is available + * @throws Will throw an error if the operation type is not CHAINED_INVOKE + */ + getChainedInvokeDetails(): ChainedInvokeDetails | undefined; + + /** + * Gets the details for a callback operation. + * @returns The callback operation details, or undefined if no operation data is available + * @throws Will throw an error if the operation is not a valid callback type + */ + getCallbackDetails(): CallbackDetails | undefined; + + /** + * Gets the details for a wait operation. + * @returns The wait operation details, or undefined if no operation data is available + * @throws Will throw an error if the operation type is not WAIT or if wait event details are missing + */ + getWaitDetails(): WaitResultDetails | undefined; + + /** + * Gets all child operations of this operation. + * @returns An array of child operations, or undefined if no operation data is available + * @throws Will throw an error if the operation ID is not available + */ + getChildOperations(): DurableOperation[] | undefined; + + /** + * Gets the raw operation data. + * @returns The operation data, or undefined if not available + */ + getOperationData(): Operation | undefined; + + /** + * Gets the events associated with this operation. + * @returns An array of events, or undefined if not available + */ + getEvents(): Event[] | undefined; + + /** + * Gets the unique identifier of this operation. + * @returns The operation ID, or undefined if not available + */ + getId(): string | undefined; + + /** + * Gets the unique identifier of the parent operation. + * @returns The parent operation ID, or undefined if not available + */ + getParentId(): string | undefined; + + /** + * Gets the name of this operation. + * @returns The operation name, or undefined if not available + */ + getName(): string | undefined; + + /** + * Gets the type of this operation. + * @returns The operation type, or undefined if not available + */ + getType(): OperationType | undefined; + + /** + * Gets the subtype of this operation. + * @returns The operation subtype, or undefined if not available + */ + getSubType(): OperationSubType | undefined; + + /** + * Checks if this operation is a wait-for-callback operation. + * @returns True if this is a wait-for-callback operation, false otherwise + */ + isWaitForCallback(): boolean; + + /** + * Checks if this operation is a callback operation. + * @returns True if this is a callback operation, false otherwise + */ + isCallback(): boolean; + + /** + * Gets the current status of this operation. + * @returns The operation status, or undefined if not available + */ + getStatus(): OperationStatus | undefined; + + /** + * Gets the timestamp when this operation started. + * @returns The start timestamp, or undefined if not available + */ + getStartTimestamp(): Date | undefined; + + /** + * Gets the timestamp when this operation ended. + * @returns The end timestamp, or undefined if not available + */ + getEndTimestamp(): Date | undefined; + + /** + * Sends a successful callback result to the durable execution service. + * @param result - Optional result data to send with the callback + * @returns A promise that resolves to the callback success response + * @throws Will throw an error if callback details are not available + */ + sendCallbackSuccess( + result?: string, + ): Promise; + + /** + * Sends a callback failure to the durable execution service. + * @param error - Optional error object to send with the callback failure + * @returns A promise that resolves to the callback failure response + * @throws Will throw an error if callback details are not available + */ + sendCallbackFailure( + error?: ErrorObject, + ): Promise; + + /** + * Sends a callback heartbeat to the durable execution service to keep the callback active. + * @returns A promise that resolves to the callback heartbeat response + * @throws Will throw an error if callback details are not available + */ + sendCallbackHeartbeat(): Promise; +} diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-test-runner.ts new file mode 100644 index 00000000..6d675b89 --- /dev/null +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/types/durable-test-runner.ts @@ -0,0 +1,179 @@ +// Main interface for customers to interface with for both the local +// and cloud-based test runners. + +import { + OperationStatus, + Event, + ExecutionStatus, +} from "@aws-sdk/client-lambda"; +import { DurableOperation } from "./durable-operation"; + +/** + * Request parameters for invoking a durable function. + * @public + */ +export interface InvokeRequest { + /** Optional payload to send to the durable function */ + payload?: unknown; +} + +/** + * Main interface for durable function test runners. + * Provides methods to run durable executions and retrieve operations for testing. + * + * @typeParam TDurableOperation - The type of operations returned by this test runner + * @typeParam ResultType - The expected result type of the durable function + * @public + */ +export interface DurableTestRunner< + TDurableOperation extends DurableOperation, + ResultType, +> { + /** + * Runs the durable execution and returns the test result. + * + * @param params - Optional parameters for the function invocation + * @returns A promise that resolves to the test result containing function output and operation history + */ + run(params?: InvokeRequest): Promise>; + + /** + * Gets the first operation with the specified name. + * + * @param name - The name of the operation to retrieve + * @returns An operation instance that can be used to inspect operation details + */ + getOperation(name: string): TDurableOperation; + + /** + * Gets an operation by its execution order index. + * + * @param index - The zero-based index of the operation in execution order + * @returns An operation instance for the operation at the specified index + */ + getOperationByIndex(index: number): TDurableOperation; + + /** + * Gets an operation by name and index when multiple operations have the same name. + * + * @param name - The name of the operation + * @param index - The zero-based index among operations with the same name + * @returns An operation instance for the specified named operation occurrence + */ + getOperationByNameAndIndex(name: string, index: number): TDurableOperation; + + /** + * Gets an operation by its unique identifier. + * + * @param id - The unique identifier of the operation + * @returns An operation instance for the operation with the specified ID + */ + getOperationById(id: string): TDurableOperation; +} + +/** + * Error information from a test execution or operation. + * @public + */ +export interface TestResultError { + /** The error message, if available */ + errorMessage: string | undefined; + /** The type/category of the error, if available */ + errorType: string | undefined; + /** Additional error data in string format, if available */ + errorData: string | undefined; + /** Stack trace lines from the error, if available */ + stackTrace: string[] | undefined; +} + +/** + * Result of a durable function test execution. + * Contains the function result, operation history, and methods to inspect the execution. + * + * @typeParam TResult - The expected result type of the durable function + * @public + */ +export interface TestResult< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TResult = any, +> { + /** + * Gets a list of all operations from the execution. + * + * @param params - Optional filter parameters + * @returns An array of operations that occurred during the execution + */ + getOperations(params?: { status: OperationStatus }): DurableOperation[]; + + /** + * Gets informatiAn about individual Lambda handler invocations. + * + * Can be used to assert on the data for a specific invocation and validate + * the number of invocations that were run during the durable execution. + * + * @returns An array of invocation details + */ + getInvocations(): Invocation[]; + + /** + * Gets the status of the durable execution. + * + * @returns The execution status, or undefined if not available + */ + getStatus(): ExecutionStatus | undefined; + + /** + * Gets the final result of the durable execution. + * + * @returns The execution result, or undefined if not available + * @throws Will throw the execution error if the execution failed + */ + getResult(): TResult | undefined; + + /** + * Gets the error from a failed durable execution. + * + * @returns The execution error details + * @throws Will throw an error if the execution succeeded + */ + getError(): TestResultError; + + /** + * Gets the complete history of events for the durable execution. + * + * @returns An array of all events that occurred during the execution + */ + getHistoryEvents(): Event[]; + + /** + * Prints a formatted table of all operations to the console with their details. + * + * @param config - Optional configuration to control which columns to display + */ + print(config?: { + parentId?: boolean; + name?: boolean; + type?: boolean; + subType?: boolean; + status?: boolean; + startTime?: boolean; + endTime?: boolean; + duration?: boolean; + }): void; +} + +/** + * Tracks a single invocation of the durable function handler. + * Contains timing information, request ID, and any errors that occurred. + * @public + */ +export interface Invocation { + /** The timestamp when the handler invocation started */ + startTimestamp: Date | undefined; + /** The timestamp when the handler invocation ended */ + endTimestamp: Date | undefined; + /** The AWS request ID for this invocation */ + requestId: string | undefined; + /** Error information if the invocation failed */ + error: TestResultError | undefined; +} From 27999faf51f2d5092e030ea6ab0bdc7e35762bb6 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Mon, 1 Dec 2025 13:59:21 -0800 Subject: [PATCH 2/3] update readme contents --- .../README.md | 6 +- .../README.md | 408 ++++++++++++++---- .../src/cli/run-durable.ts | 2 +- .../local/local-durable-test-runner.ts | 6 +- 4 files changed, 341 insertions(+), 81 deletions(-) diff --git a/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md b/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md index 7c627453..99e154b5 100644 --- a/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md +++ b/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md @@ -1,6 +1,6 @@ -# @aws/durable-execution-sdk-js-eslint-plugin +# AWS Durable Execution ESLint Plugin -ESLint plugin for AWS Lambda durable functions best practices. +ESLint plugin for AWS Durable Execution SDK best practices. ## Installation @@ -145,7 +145,7 @@ The plugin detects these durable operations: ### No Nested Durable Operations -Nesting durable operations with the same context object can cause runtime errors and unexpected behavior in AWS Lambda durable functions. This rule helps catch these issues at development time. +Nesting durable operations with the same context object can cause runtime errors and unexpected behavior in AWS Durable Executions. This rule helps catch these issues at development time. ### No Closure in Durable Operations diff --git a/packages/aws-durable-execution-sdk-js-testing/README.md b/packages/aws-durable-execution-sdk-js-testing/README.md index d9732c19..2b85b43b 100644 --- a/packages/aws-durable-execution-sdk-js-testing/README.md +++ b/packages/aws-durable-execution-sdk-js-testing/README.md @@ -1,32 +1,177 @@ -# Durable Execution Testing SDK +# AWS Durable Execution Testing SDK for JavaScript -Testing utilities for AWS Durable Execution SDK for JavaScript/TypeScript. +Testing utilities for the AWS Durable Execution SDK for JavaScript and TypeScript. -## Overview +## Features -This package provides tools for testing durable functions both locally and in the cloud: +This package provides testing tools for durable functions both locally and in the cloud: - **LocalDurableTestRunner** - Execute and test durable functions locally without AWS deployment -- **CloudDurableTestRunner** - Test against deployed Lambda functions in AWS -- **run-durable CLI** - Command-line tool for quick local testing -- **Test helpers** - Utilities for assertions and test setup + - **Time-skipping support** - Skip wait operations and delays for faster test execution + - **Function registration** - Register additional functions for chained invoke testing + - **Full operation inspection** - Access detailed information about steps, waits, callbacks, and more + - **Mock-friendly** - Works seamlessly with Jest and other mocking frameworks + +- **CloudDurableTestRunner** - Test against deployed AWS Lambda functions + - **Real environment testing** - Validate behavior against actual your actual deployed AWS Lambda function + - **Configurable invocation** - Support for different invocation types and polling intervals + +- **Testing Capabilities** + - **Operation assertions** - Inspect individual operations by name, index, or ID + - **Status and result validation** - Verify execution status, results, and error conditions + - **Callback testing** - Send callback responses and verify callback behavior + - **Retry validation** - Test step retry logic and failure scenarios + +## Installation + +```bash +npm install --save-dev @aws/durable-execution-sdk-js-testing +``` + +## Quick Start + +### Handler Code + +```typescript +import { + withDurableExecution, + DurableContext, +} from "@aws/durable-execution-sdk-js"; + +const handler = async (event: any, context: DurableContext) => { + // Execute a durable step with automatic retry + const userData = await context.step("fetch-user", async () => + fetchUserFromDB(event.userId), + ); + + // Wait for 5 seconds + await context.wait({ seconds: 5 }); + + // Process data in another step + const result = await context.step("process-user", async () => + processUser(userData), + ); + + return result; +}; + +export const lambdaHandler = withDurableExecution(handler); +``` + +### Test Code + +#### Local Testing + +```typescript +import { + LocalDurableTestRunner, + WaitingOperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; +import { lambdaHandler } from "./lambdaHandler"; + +beforeAll(() => + LocalDurableTestRunner.setupTestEnvironment({ + skipTime: true, + }), +); + +afterAll(() => LocalDurableTestRunner.teardownTestEnvironment()); + +describe("LocalDurableTestRunner", () => { + let runner; + + beforeEach(() => { + runner = new LocalDurableTestRunner({ + handlerFunction: lambdaHandler, + }); + }); + + it("should wait for 5 seconds and return result", async () => { + const execution = await runner.run({ payload: { userId: "123" } }); + + expect(execution.getStatus()).toBe("SUCCEEDED"); + expect(execution.getOperations()).toHaveLength(3); // fetch-user, wait, process-user + + // Check the fetch-user step + const fetchStep = runner.getOperation("fetch-user"); + await fetchStep.waitForData(WaitingOperationStatus.COMPLETED); + const fetchDetails = fetchStep.getStepDetails(); + expect(fetchDetails?.result).toBeDefined(); + + // Check the wait operation + const waitOp = runner.getOperationByIndex(1); + const waitDetails = waitOp.getWaitDetails(); + expect(waitDetails?.waitSeconds).toBe(5); + + // Check the process-user step + const processStep = runner.getOperation("process-user"); + await processStep.waitForData(WaitingOperationStatus.COMPLETED); + const processDetails = processStep.getStepDetails(); + expect(processDetails?.result).toBeDefined(); + }); +}); +``` + +#### Cloud Testing + +```typescript +import { + CloudDurableTestRunner, + WaitingOperationStatus, +} from "@aws/durable-execution-sdk-js-testing"; + +describe("CloudDurableTestRunner", () => { + let runner; + + beforeEach(() => { + runner = new CloudDurableTestRunner({ + functionName: "my-durable-function", // Your deployed function name + }); + }); + + it("should wait for 5 seconds and return result", async () => { + const execution = await runner.run({ payload: { userId: "123" } }); + + expect(execution.getStatus()).toBe("SUCCEEDED"); + expect(execution.getOperations()).toHaveLength(3); // fetch-user, wait, process-user + + // Check the fetch-user step + const fetchStep = runner.getOperation("fetch-user"); + const fetchDetails = fetchStep.getStepDetails(); + expect(fetchDetails?.result).toBeDefined(); + expect(fetchDetails?.attempt).toBe(1); + + // Check the wait operation + const waitOp = runner.getOperationByIndex(1); + const waitDetails = waitOp.getWaitDetails(); + expect(waitDetails?.waitSeconds).toBe(5); + + // Check the process-user step + const processStep = runner.getOperation("process-user"); + const processDetails = processStep.getStepDetails(); + expect(processDetails?.result).toBeDefined(); + }); +}); +``` ## LocalDurableTestRunner -Run durable functions locally with a simulated checkpoint server. +Run durable functions locally with a simulated durable execution backend. ```typescript import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; import { handler } from "./my-durable-function"; -await LocalDurableTestRunner.setupTestEnvironment(); +// Set up test environment with optional time skipping +await LocalDurableTestRunner.setupTestEnvironment({ + skipTime: true, // Skip wait delays for faster tests +}); const runner = new LocalDurableTestRunner({ handlerFunction: handler, - skipTime: true, // Skip wait delays for faster tests }); -const execution = await runner.run(); +const execution = await runner.run({ payload: { test: "data" } }); // Assert on results expect(execution.getStatus()).toBe("SUCCEEDED"); @@ -36,38 +181,130 @@ expect(execution.getResult()).toEqual(expectedValue); const operations = execution.getOperations(); expect(operations).toHaveLength(3); +// Clean up test environment await LocalDurableTestRunner.teardownTestEnvironment(); ``` -## CloudDurableTestRunner +### Mocking -Test against deployed Lambda functions in AWS. +Mock external dependencies using Jest or your preferred testing framework: ```typescript -import { CloudDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; +import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; -const runner = new CloudDurableTestRunner({ - functionName: "MyDurableFunction", - region: "us-east-1", +// Mock external functions +jest.mock("../services/userService", () => ({ + fetchUserFromDB: jest.fn(), + processUser: jest.fn(), +})); + +import { fetchUserFromDB, processUser } from "../services/userService"; +import { lambdaHandler } from "./lambdaHandler"; + +const mockFetchUser = fetchUserFromDB as jest.MockedFunction< + typeof fetchUserFromDB +>; +const mockProcessUser = processUser as jest.MockedFunction; + +describe("Mocked Dependencies", () => { + let runner: LocalDurableTestRunner; + + beforeAll(() => + LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }), + ); + afterAll(() => LocalDurableTestRunner.teardownTestEnvironment()); + + beforeEach(() => { + runner = new LocalDurableTestRunner({ handlerFunction: lambdaHandler }); + + // Reset mocks + mockFetchUser.mockClear(); + mockProcessUser.mockClear(); + }); + + it("should call mocked functions and return expected results", async () => { + // Setup mocks + const userData = { id: "123", name: "John Doe" }; + const processedResult = { id: "123", processed: true }; + + mockFetchUser.mockResolvedValue(userData); + mockProcessUser.mockResolvedValue(processedResult); + + // Run test + const execution = await runner.run({ payload: { userId: "123" } }); + + // Verify results + expect(execution.getStatus()).toBe("SUCCEEDED"); + expect(execution.getResult()).toEqual(processedResult); + + // Verify mock calls + expect(mockFetchUser).toHaveBeenCalledWith("123"); + expect(mockProcessUser).toHaveBeenCalledWith(userData); + + // Verify operations + const fetchStep = runner.getOperation("fetch-user"); + const fetchDetails = fetchStep.getStepDetails(); + expect(fetchDetails?.result).toEqual(userData); + }); }); +``` -const execution = await runner.run({ payload: { userId: "123" } }); +### Function Registration -expect(execution.getStatus()).toBe("SUCCEEDED"); +Register additional functions that can be invoked during local testing of chained invocations: + +#### Handler Code + +```typescript +const mainHandler = withDurableExecution(async (event, context) => { + await context.invoke("workflow-a:$LATEST"); + await context.invoke("workflow-b:$LATEST"); + + await context.invoke("utility-function"); + await context.invoke("utility-helper"); +}); ``` -## run-durable CLI +#### Test code -Quick command-line tool for testing durable functions locally without writing test code. +```typescript +const runner = new LocalDurableTestRunner({ handlerFunction: mainHandler }); -**See [RUN_DURABLE_CLI.md](./RUN_DURABLE_CLI.md) for complete CLI documentation.** +// Register durable functions +runner.registerDurableFunction("workflow-a:$LATEST", durableWorkflowA); -```bash -# Basic usage -npm run run-durable -- +// Register regular functions +runner.registerFunction("utility-function", utilityHandler); -# With options -npm run run-durable -- [no-skip-time] [verbose] [show-history] +// Method chaining is supported +runner + .registerDurableFunction("workflow-b:$LATEST", durableWorkflowB) + .registerFunction("helper", helperHandler); +``` + +## CloudDurableTestRunner + +Test against deployed Lambda functions in AWS. + +```typescript +import { + CloudDurableTestRunner, + InvocationType, +} from "@aws/durable-execution-sdk-js-testing"; +import { LambdaClient } from "@aws-sdk/client-lambda"; + +const runner = new CloudDurableTestRunner({ + functionName: "MyDurableFunction", + client: new LambdaClient({ region: "us-east-1" }), // optional + config: { + pollInterval: 1000, // optional, default 1000ms + invocationType: InvocationType.RequestResponse, // optional + }, +}); + +const execution = await runner.run({ payload: { userId: "123" } }); + +expect(execution.getStatus()).toBe("SUCCEEDED"); ``` ## Test Result API @@ -77,10 +314,8 @@ Both runners return a `TestResult` object with methods for assertions: ```typescript const execution = await runner.run(); -// Get execution status +// Get execution status and results execution.getStatus(); // "SUCCEEDED" | "FAILED" | "RUNNING" | etc. - -// Get result or error execution.getResult(); // Returns the function result execution.getError(); // Returns error details if failed @@ -88,14 +323,13 @@ execution.getError(); // Returns error details if failed execution.getOperations(); // All operations execution.getOperations({ status: "SUCCEEDED" }); // Filter by status -// Get history events +// Get execution history and invocations execution.getHistoryEvents(); // Detailed event history +execution.getInvocations(); // Handler invocation details -// Get invocations -execution.getInvocations(); // All handler invocations - -// Print operations table -execution.print(); // Console table of operations +// Print operations table to console +execution.print(); // Default columns +execution.print({ name: true, status: true, duration: true }); // Custom columns ``` ## Operation Assertions @@ -103,67 +337,91 @@ execution.print(); // Console table of operations Access specific operations for detailed assertions: ```typescript -// By name -const operation = runner.getOperation("my-step"); +// Get operations by different methods +const operation = runner.getOperation("my-step"); // By name +const firstOp = runner.getOperationByIndex(0); // By execution order +const secondNamedOp = runner.getOperationByNameAndIndex("my-step", 1); // By name + index +const opById = runner.getOperationById("abc123"); // By unique ID + +// Wait for operation data to be available +await operation.waitForData(); // Default to STARTED status +await operation.waitForData(WaitingOperationStatus.COMPLETED); + +// Get operation details based on type +const stepDetails = operation.getStepDetails(); // For step operations +const contextDetails = operation.getContextDetails(); // For context operations +const callbackDetails = operation.getCallbackDetails(); // For callback operations +const waitDetails = operation.getWaitDetails(); // For wait operations + +// Get basic operation information +operation.getName(); +operation.getStatus(); +operation.getStartTimestamp(); +operation.getEndTimestamp(); +``` -// By index -const firstOp = runner.getOperationByIndex(0); +### Callback Operations -// By name and index -const secondNamedOp = runner.getOperationByNameAndIndex("my-step", 1); +For callback operations, you can send responses: -// By ID -const opById = runner.getOperationById("abc123"); +```typescript +const callback = runner.getOperation("my-callback"); +await callback.waitForData(WaitingOperationStatus.SUBMITTED); + +// Send callback responses +await callback.sendCallbackSuccess("result data"); +await callback.sendCallbackFailure({ errorMessage: "Failed" }); +await callback.sendCallbackHeartbeat(); ``` ## Configuration Options -### LocalDurableTestRunner Options +### LocalDurableTestRunner ```typescript -{ - handlerFunction: LambdaHandler; // Required: The durable function handler - skipTime?: boolean; // Optional: Skip wait delays (default: false) -} +// Constructor +new LocalDurableTestRunner({ handlerFunction: handler }); + +// Environment setup +await LocalDurableTestRunner.setupTestEnvironment({ skipTime: true }); + +// Environment teardown +await LocalDurableTestRunner.teardownTestEnvironment(); ``` -### CloudDurableTestRunner Options +### CloudDurableTestRunner ```typescript -{ - functionName: string; // Required: Lambda function name or ARN - region?: string; // Optional: AWS region - invocationType?: string; // Optional: 'RequestResponse' | 'Event' -} +// Basic configuration +new CloudDurableTestRunner({ functionName: "MyFunction:$LATEST" }); + +// Advanced configuration +new CloudDurableTestRunner({ + functionName: "MyFunction:$LATEST", + client: new LambdaClient({ region: "us-east-1" }), + config: { pollInterval: 500, invocationType: InvocationType.Event }, +}); ``` -## Example Test +## Reset Runner State + +The `reset()` method is required if you reuse the same runner instance between tests. Data about an individual execution is cleared from the runner instance when `reset()` is called. ```typescript -import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing"; -import { handler } from "../my-function"; +describe("Reusing Runner Instance", () => { + let runner: LocalDurableTestRunner; -describe("My Durable Function", () => { - beforeAll(async () => { - await LocalDurableTestRunner.setupTestEnvironment(); + beforeAll(() => { + // Create runner once + runner = new LocalDurableTestRunner({ handlerFunction: handler }); }); - afterAll(async () => { - await LocalDurableTestRunner.teardownTestEnvironment(); + beforeEach(() => { + // Reset state when reusing the same instance + runner.reset(); }); - it("should complete successfully", async () => { - const runner = new LocalDurableTestRunner({ - handlerFunction: handler, - skipTime: true, - }); - - const execution = await runner.run({ payload: { test: "data" } }); - - expect(execution.getStatus()).toBe("SUCCEEDED"); - expect(execution.getResult()).toEqual({ success: true }); - expect(execution.getOperations()).toHaveLength(2); - }); + // ... tests }); ``` diff --git a/packages/aws-durable-execution-sdk-js-testing/src/cli/run-durable.ts b/packages/aws-durable-execution-sdk-js-testing/src/cli/run-durable.ts index a21884fc..266a3a9b 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/cli/run-durable.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/cli/run-durable.ts @@ -72,7 +72,7 @@ async function runDurable() { } try { - const result = execution.getResult(); + const result = execution.getResult() as unknown; console.log("\nResult:"); console.log(JSON.stringify(result, null, 2)); } catch { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts index d75ea836..efb0e2cc 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/local-durable-test-runner.ts @@ -119,8 +119,10 @@ export interface LocalDurableTestRunnerSetupParameters { * * @public */ -export class LocalDurableTestRunner - implements DurableTestRunner +export class LocalDurableTestRunner< + // eslint-disable-next-line @typescript-eslint/no-explicit-any + TResult = any, +> implements DurableTestRunner { private operationStorage: LocalOperationStorage; private waitManager: OperationWaitManager; From e8f241ed7428a35f2e00210ff0c572621fa2b5c1 Mon Sep 17 00:00:00 2001 From: Anthony Ting Date: Mon, 1 Dec 2025 14:46:34 -0800 Subject: [PATCH 3/3] fix missing exported types --- README.md | 8 +-- .../README.md | 2 +- .../src/utils/test-helper.ts | 4 +- .../package.json | 2 +- .../src/index.ts | 1 + .../src/test-runner/__tests__/index.test.ts | 66 ------------------- .../cloud-durable-test-runner.test.ts | 2 +- .../cloud/cloud-durable-test-runner.ts | 2 - .../src/test-runner/index.ts | 12 ++-- .../aws-durable-execution-sdk-js/package.json | 2 +- 10 files changed, 18 insertions(+), 83 deletions(-) delete mode 100644 packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts diff --git a/README.md b/README.md index c52f9aa9..011ef04f 100644 --- a/README.md +++ b/README.md @@ -2,11 +2,11 @@ This repository contains the code for the following 3 packages published to NPM: -* [@aws/durable-execution-sdk-js](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js) -* [@aws/durable-execution-sdk-js-testing](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-testing) -* [@aws/durable-execution-sdk-js-eslint-plugin](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-eslint-plugin) +- [@aws/durable-execution-sdk-js](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js) +- [@aws/durable-execution-sdk-js-testing](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-testing) +- [@aws/durable-execution-sdk-js-eslint-plugin](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-eslint-plugin) -The repository also contains example durable execution located [here](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-examples). +The repository also contains example durable functions located [here](https://github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-examples). ## Security diff --git a/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md b/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md index 99e154b5..7cfb6d9a 100644 --- a/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md +++ b/packages/aws-durable-execution-sdk-js-eslint-plugin/README.md @@ -145,7 +145,7 @@ The plugin detects these durable operations: ### No Nested Durable Operations -Nesting durable operations with the same context object can cause runtime errors and unexpected behavior in AWS Durable Executions. This rule helps catch these issues at development time. +Nesting durable operations with the same context object can cause runtime errors and unexpected behavior in AWS Lambda durable functions. This rule helps catch these issues at development time. ### No Closure in Durable Operations diff --git a/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts b/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts index a598d812..e54aa599 100644 --- a/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts +++ b/packages/aws-durable-execution-sdk-js-examples/src/utils/test-helper.ts @@ -1,10 +1,10 @@ import { LambdaClient } from "@aws-sdk/client-lambda"; +import { DurableLambdaHandler } from "@aws/durable-execution-sdk-js"; import { LocalDurableTestRunner, CloudDurableTestRunner, DurableTestRunner, DurableOperation, - LocalTestRunnerHandlerFunction, InvocationType, } from "@aws/durable-execution-sdk-js-testing"; @@ -17,7 +17,7 @@ type TestCallback = ( export interface TestDefinition { name: string; functionName: string; - handler: LocalTestRunnerHandlerFunction; + handler: DurableLambdaHandler; tests: TestCallback; invocationType?: InvocationType; localRunnerConfig?: { diff --git a/packages/aws-durable-execution-sdk-js-testing/package.json b/packages/aws-durable-execution-sdk-js-testing/package.json index 4490a6e3..69a93e14 100644 --- a/packages/aws-durable-execution-sdk-js-testing/package.json +++ b/packages/aws-durable-execution-sdk-js-testing/package.json @@ -1,6 +1,6 @@ { "name": "@aws/durable-execution-sdk-js-testing", - "description": "Durable Executions Testing SDK for TypeScript", + "description": "AWS Durable Execution Testing SDK for TypeScript", "version": "1.0.0", "license": "Apache-2.0", "repository": "ssh:github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js-testing", diff --git a/packages/aws-durable-execution-sdk-js-testing/src/index.ts b/packages/aws-durable-execution-sdk-js-testing/src/index.ts index ffdf1459..7d40a5bd 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/index.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/index.ts @@ -3,4 +3,5 @@ export { OperationType, OperationStatus, ExecutionStatus, + InvocationType, } from "@aws-sdk/client-lambda"; diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts deleted file mode 100644 index e206169c..00000000 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/__tests__/index.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import * as indexExports from "../index"; -import * as localExports from "../local"; -import * as cloudExports from "../cloud"; -import * as durableTestRunnerExports from "../types/durable-test-runner"; - -describe("test-runner/index.ts exports", () => { - it("should correctly re-export all exports from local", () => { - // Get all exported keys from local - const localKeys = Object.keys(localExports); - - // Check that all keys from local are in index exports - for (const key of localKeys) { - expect(Object.keys(indexExports)).toContain(key); - expect((indexExports as Record)[key]).toBe( - (localExports as Record)[key], - ); - } - }); - - it("should correctly re-export all exports from cloud", () => { - // Get all exported keys from cloud - const cloudKeys = Object.keys(cloudExports); - - // Check that all keys from cloud are in index exports - for (const key of cloudKeys) { - expect(Object.keys(indexExports)).toContain(key); - expect((indexExports as Record)[key]).toBe( - (cloudExports as Record)[key], - ); - } - }); - - it("should correctly re-export all exports from durable-test-runner", () => { - // Get all exported keys from durable-test-runner - const durableKeys = Object.keys(durableTestRunnerExports); - - // Check that all keys from durable-test-runner are in index exports - for (const key of durableKeys) { - expect(Object.keys(indexExports)).toContain(key); - expect((indexExports as Record)[key]).toBe( - (durableTestRunnerExports as Record)[key], - ); - } - }); - - it("should only export items from local and durable-test-runner", () => { - // Get all exported keys - const indexKeys = Object.keys(indexExports); - const localKeys = Object.keys(localExports); - const durableKeys = Object.keys(durableTestRunnerExports); - const cloudKeys = Object.keys(cloudExports); - - // The total count of exports should match the sum of the individual modules - expect(indexKeys.length).toBe( - localKeys.length + durableKeys.length + cloudKeys.length, - ); - - // Every key in indexExports should be in either localExports or durableTestRunnerExports - for (const key of indexKeys) { - const isInLocal = localKeys.includes(key); - const isInDurable = durableKeys.includes(key); - const isInCloud = cloudKeys.includes(key); - expect(isInLocal || isInDurable || isInCloud).toBe(true); - } - }); -}); diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts index 2e6246b9..825e61dc 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts @@ -123,7 +123,7 @@ describe("CloudDurableTestRunner", () => { }); expect(runner).toBeDefined(); - expect(LambdaClient).toHaveBeenCalledWith({}); + expect(LambdaClient).toHaveBeenCalledWith(); }); it("should initialize with custom Lambda client", () => { diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts index f4a1feb8..6a78619c 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts @@ -24,8 +24,6 @@ import { } from "../common/create-durable-api-client"; import { CloudOperation } from "./operations/cloud-operation"; -export { InvocationType }; - /** * Options for the cloud durable test runner. * @public diff --git a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts index 6abb5585..446d1269 100644 --- a/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts +++ b/packages/aws-durable-execution-sdk-js-testing/src/test-runner/index.ts @@ -4,7 +4,7 @@ export type { LocalDurableTestRunnerSetupParameters, } from "./local"; -export { CloudDurableTestRunner, InvocationType } from "./cloud"; +export { CloudDurableTestRunner } from "./cloud"; export type { CloudDurableTestRunnerConfig, CloudDurableTestRunnerParameters, @@ -20,9 +20,11 @@ export type { export type { DurableOperation, - ContextDetails as OperationResultContextDetails, - StepDetails as OperationResultStepDetails, - CallbackDetails as OperationResultCallbackDetails, - ChainedInvokeDetails as OperationResultChainedInvokeDetails, + ContextDetails, + StepDetails, + CallbackDetails, + ChainedInvokeDetails, WaitResultDetails, } from "./types/durable-operation"; + +export { WaitingOperationStatus } from "./types/durable-operation"; diff --git a/packages/aws-durable-execution-sdk-js/package.json b/packages/aws-durable-execution-sdk-js/package.json index db55f7a5..7d1b8c13 100644 --- a/packages/aws-durable-execution-sdk-js/package.json +++ b/packages/aws-durable-execution-sdk-js/package.json @@ -1,6 +1,6 @@ { "name": "@aws/durable-execution-sdk-js", - "description": "Durable Executions Language SDK for TypeScript", + "description": "AWS Durable Execution Language SDK for TypeScript", "license": "Apache-2.0", "version": "1.0.0", "repository": "ssh:github.com/aws/aws-durable-execution-sdk-js/tree/main/packages/aws-durable-execution-sdk-js",