Skip to content

Commit cc41d0e

Browse files
authored
fix(testing-sdk): reject waitForData promises when execution completes (#318)
*Issue #, if available:* #317 *Description of changes:* Preventing hanging promises if execution completes while waitForData is pending. Before, waitForData would never resolve/reject when the execution completes and the operation was not found. Now, it will reject all of the pending promises when it completes. By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.
1 parent d539d96 commit cc41d0e

File tree

5 files changed

+351
-181
lines changed

5 files changed

+351
-181
lines changed

packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/__tests__/cloud-durable-test-runner.test.ts

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,10 @@ import {
1010
GetDurableExecutionCommandOutput,
1111
OperationStatus,
1212
OperationType,
13+
InvocationType,
1314
} from "@aws-sdk/client-lambda";
1415
import { CloudDurableTestRunner } from "../cloud-durable-test-runner";
16+
import { TestResult, WaitingOperationStatus } from "../../durable-test-runner";
1517

1618
jest.mock("@aws-sdk/client-lambda");
1719

@@ -832,4 +834,123 @@ describe("CloudDurableTestRunner", () => {
832834
expect(result).toBeDefined();
833835
});
834836
});
837+
838+
describe("Operation rejection scenarios", () => {
839+
it("should reject pending operation promises when execution completes successfully", async () => {
840+
setupMockApiResponses(mockSend, {
841+
executionResponse: {
842+
Status: ExecutionStatus.SUCCEEDED,
843+
Result: '{"success": true}',
844+
$metadata: {},
845+
DurableExecutionArn: undefined,
846+
DurableExecutionName: undefined,
847+
FunctionArn: undefined,
848+
StartTimestamp: undefined,
849+
},
850+
});
851+
852+
const runner = new CloudDurableTestRunner<{ success: boolean }>({
853+
functionName: mockFunctionArn,
854+
config: {
855+
invocationType: InvocationType.Event,
856+
},
857+
});
858+
859+
// Get an operation that will never be found in the history
860+
const operation = runner.getOperation("nonexistent-operation");
861+
862+
const runPromise = runner.run();
863+
864+
const waitPromise = operation.waitForData();
865+
866+
const [waitResult, runResult] = await Promise.allSettled([
867+
waitPromise,
868+
runPromise,
869+
jest.advanceTimersByTimeAsync(1000),
870+
]);
871+
872+
expect(runResult.status).toBe("fulfilled");
873+
expect(
874+
(
875+
runResult as PromiseFulfilledResult<TestResult<unknown>>
876+
).value.getResult(),
877+
).not.toBeUndefined();
878+
879+
// The waiting operation should be rejected
880+
expect(waitResult.status).toBe("rejected");
881+
expect((waitResult as PromiseRejectedResult).reason).toEqual(
882+
new Error(
883+
"Operation was not found after execution completion. Expected status: STARTED. This typically means the operation was never executed or the test is waiting for the wrong operation.",
884+
),
885+
);
886+
});
887+
888+
it("should reject pending operation promises when execution fails", async () => {
889+
setupMockApiResponses(mockSend, {
890+
executionResponse: {
891+
Status: ExecutionStatus.FAILED,
892+
Error: {
893+
ErrorMessage: "ExecutionFailed",
894+
ErrorType: "TestError",
895+
},
896+
$metadata: {},
897+
DurableExecutionArn: undefined,
898+
DurableExecutionName: undefined,
899+
FunctionArn: undefined,
900+
StartTimestamp: undefined,
901+
},
902+
});
903+
904+
const runner = new CloudDurableTestRunner<{ success: boolean }>({
905+
functionName: mockFunctionArn,
906+
config: {
907+
invocationType: InvocationType.Event,
908+
},
909+
});
910+
911+
// Get operations that will never be found
912+
const operation1 = runner.getOperationById("missing-op-1");
913+
const operation2 = runner.getOperationById("missing-op-2");
914+
915+
const waitPromise1 = operation1.waitForData();
916+
const waitPromise2 = operation2.waitForData(
917+
WaitingOperationStatus.COMPLETED,
918+
);
919+
920+
const runPromise = runner.run();
921+
922+
// The run should complete with failed execution
923+
const [waitResult1, waitResult2, runResult] = await Promise.allSettled([
924+
waitPromise1,
925+
waitPromise2,
926+
runPromise,
927+
jest.advanceTimersByTimeAsync(1000),
928+
]);
929+
930+
expect(runResult.status).toBe("fulfilled");
931+
expect(
932+
(
933+
runResult as PromiseFulfilledResult<TestResult<unknown>>
934+
).value.getError(),
935+
).toEqual({
936+
errorMessage: "ExecutionFailed",
937+
errorType: "TestError",
938+
});
939+
940+
// The waiting operation should be rejected
941+
expect(waitResult1.status).toBe("rejected");
942+
expect((waitResult1 as PromiseRejectedResult).reason).toEqual(
943+
new Error(
944+
"Operation was not found after execution completion. Expected status: STARTED. This typically means the operation was never executed or the test is waiting for the wrong operation.",
945+
),
946+
);
947+
948+
expect(waitResult2.status).toBe("rejected");
949+
expect((waitResult2 as PromiseRejectedResult).reason).toEqual(
950+
new Error(
951+
"Operation was not found after execution completion. Expected status: COMPLETED. This typically means the operation was never executed or the test is waiting for the wrong operation.",
952+
),
953+
);
954+
});
955+
});
835956
});

packages/aws-durable-execution-sdk-js-testing/src/test-runner/cloud/cloud-durable-test-runner.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ export class CloudDurableTestRunner<ResultType>
126126
);
127127
} finally {
128128
historyPoller.stopPolling();
129+
this.waitManager.clearWaitingOperations();
129130
}
130131
}
131132

packages/aws-durable-execution-sdk-js-testing/src/test-runner/local/__tests__/integration/local-durable-test-runner.integration.test.ts

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,26 @@ describe("LocalDurableTestRunner Integration", () => {
301301
expect(result.getResult()).toBe(true);
302302
});
303303

304+
it("should reject waiting promise if execution completes", async () => {
305+
const handler = withDurableExecution(() => {
306+
return Promise.resolve("result");
307+
});
308+
309+
const runner = new LocalDurableTestRunner({
310+
handlerFunction: handler,
311+
});
312+
313+
const resultPromise = runner.run();
314+
315+
await expect(
316+
runner.getOperation("non-existent").waitForData(),
317+
).rejects.toThrow(
318+
"Operation was not found after execution completion. Expected status: STARTED. This typically means the operation was never executed or the test is waiting for the wrong operation.",
319+
);
320+
321+
expect((await resultPromise).getResult()).toBe("result");
322+
});
323+
304324
// enable when language SDK supports concurrent waits
305325
it.skip("should prevent scheduled function interference in parallel wait scenario", async () => {
306326
// This test creates a scenario where multiple wait operations could create

0 commit comments

Comments
 (0)