From be963f84803f39058ce9bb93a12a38c27734c8f7 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 18 Nov 2025 23:54:03 +0100 Subject: [PATCH 01/17] feat(idempotency): initial support for durable functions --- .../idempotency/src/IdempotencyHandler.ts | 19 ++++++++++++++++--- packages/idempotency/src/makeIdempotent.ts | 13 ++++++++++--- .../src/types/IdempotencyOptions.ts | 3 +++ packages/idempotency/src/types/index.ts | 1 + 4 files changed, 30 insertions(+), 6 deletions(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 96505222b7..4642f2a7a2 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -17,6 +17,7 @@ import type { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js import type { IdempotencyRecord } from './persistence/IdempotencyRecord.js'; import type { AnyFunction, + DurableMode, IdempotencyHandlerOptions, } from './types/IdempotencyOptions.js'; @@ -177,7 +178,11 @@ export class IdempotencyHandler { * window, we might get an `IdempotencyInconsistentStateError`. In such * cases we can safely retry the handling a few times. */ - public async handle(): Promise> { + public async handle({ + durableMode, + }: { + durableMode: DurableMode; + }): Promise> { // early return if we should skip idempotency completely if (this.shouldSkipIdempotency()) { return await this.#functionToMakeIdempotent.apply( @@ -190,7 +195,7 @@ export class IdempotencyHandler { for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) { try { const { isIdempotent, result } = - await this.#saveInProgressOrReturnExistingResult(); + await this.#saveInProgressOrReturnExistingResult({ durableMode }); if (isIdempotent) return result as ReturnType; return await this.getFunctionResult(); @@ -356,7 +361,11 @@ export class IdempotencyHandler { * Before returning a result, we might neede to look up the idempotency record * and validate it to ensure that it is consistent with the payload to be hashed. */ - readonly #saveInProgressOrReturnExistingResult = async (): Promise<{ + readonly #saveInProgressOrReturnExistingResult = async ({ + durableMode, + }: { + durableMode?: DurableMode; + } = {}): Promise<{ isIdempotent: boolean; result: JSONValue; }> => { @@ -381,6 +390,10 @@ export class IdempotencyHandler { { cause: error } ); if (error.name === 'IdempotencyItemAlreadyExistsError') { + if (durableMode === 'ReplayMode') { + return returnValue; + } + let idempotencyRecord = (error as IdempotencyItemAlreadyExistsError) .existingRecord; if (idempotencyRecord === undefined) { diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index 2e6e9b274f..23660bdd6e 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -134,16 +134,23 @@ function makeIdempotent( functionPayloadToBeHashed = args[0]; } } - + // 'arn:aws:lambda:eu-south-1:801530346538:function:test-aamorosi:$LATEST/durable-execution/8893c114-49cc-4a35-9b17-32a40e9a8cf1/a11e1ae0-266a-33e6-ac46-d72f543cd215' + const durableArn = args[1]?.executionContext?.durableExecutionArn; + // const durableMode: 'ExecutionMode' | 'ReplayMode' = 'ExecutionMode'; + const durableMode = args[1]?.durableExecutionMode; + const durableSuffix = `${durableArn.split('/').slice(-2).join('/')}`; return new IdempotencyHandler({ functionToMakeIdempotent: fn, idempotencyConfig: idempotencyConfig, persistenceStore: persistenceStore, keyPrefix: keyPrefix, functionArguments: args, - functionPayloadToBeHashed, + functionPayloadToBeHashed: { + cx: functionPayloadToBeHashed, + d: durableSuffix, + }, thisArg: this, - }).handle() as ReturnType; + }).handle({ durableMode }) as ReturnType; }; } diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index e586764646..c243293ca4 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -206,6 +206,8 @@ type IdempotencyConfigOptions = { responseHook?: ResponseHook; }; +type DurableMode = 'ExecutionMode' | 'ReplayMode'; + export type { AnyFunction, IdempotencyConfigOptions, @@ -213,4 +215,5 @@ export type { IdempotencyLambdaHandlerOptions, IdempotencyHandlerOptions, ResponseHook, + DurableMode, }; diff --git a/packages/idempotency/src/types/index.ts b/packages/idempotency/src/types/index.ts index 1d77754342..26c192c8df 100644 --- a/packages/idempotency/src/types/index.ts +++ b/packages/idempotency/src/types/index.ts @@ -15,6 +15,7 @@ export type { } from './DynamoDBPersistence.js'; export type { AnyFunction, + DurableMode, IdempotencyConfigOptions, IdempotencyHandlerOptions, IdempotencyLambdaHandlerOptions, From 56c4ff88518cbead46efdf1ff15cb6f837721f03 Mon Sep 17 00:00:00 2001 From: Andrea Amorosi Date: Tue, 18 Nov 2025 23:56:45 +0100 Subject: [PATCH 02/17] feat(idempotency): conditionally set payload to be hashed --- packages/idempotency/src/makeIdempotent.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index 23660bdd6e..87a2cf061f 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -134,9 +134,9 @@ function makeIdempotent( functionPayloadToBeHashed = args[0]; } } - // 'arn:aws:lambda:eu-south-1:801530346538:function:test-aamorosi:$LATEST/durable-execution/8893c114-49cc-4a35-9b17-32a40e9a8cf1/a11e1ae0-266a-33e6-ac46-d72f543cd215' + // i.e 'arn:aws:lambda:eu-south-1:801530346538:function:test-aamorosi:$LATEST/durable-execution/8893c114-49cc-4a35-9b17-32a40e9a8cf1/a11e1ae0-266a-33e6-ac46-d72f543cd215' const durableArn = args[1]?.executionContext?.durableExecutionArn; - // const durableMode: 'ExecutionMode' | 'ReplayMode' = 'ExecutionMode'; + // i.e. 'ReplayMode' = 'ExecutionMode'; const durableMode = args[1]?.durableExecutionMode; const durableSuffix = `${durableArn.split('/').slice(-2).join('/')}`; return new IdempotencyHandler({ @@ -145,10 +145,12 @@ function makeIdempotent( persistenceStore: persistenceStore, keyPrefix: keyPrefix, functionArguments: args, - functionPayloadToBeHashed: { - cx: functionPayloadToBeHashed, - d: durableSuffix, - }, + functionPayloadToBeHashed: durableMode + ? { + cx: functionPayloadToBeHashed, + d: durableSuffix, + } + : functionPayloadToBeHashed, thisArg: this, }).handle({ durableMode }) as ReturnType; }; From a3ee9529dd3188ffabcee2666ad8612221fbdac2 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 17:08:57 +0000 Subject: [PATCH 03/17] Update payload hashing logic --- packages/idempotency/src/makeIdempotent.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index 87a2cf061f..bfe278a71e 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -145,12 +145,7 @@ function makeIdempotent( persistenceStore: persistenceStore, keyPrefix: keyPrefix, functionArguments: args, - functionPayloadToBeHashed: durableMode - ? { - cx: functionPayloadToBeHashed, - d: durableSuffix, - } - : functionPayloadToBeHashed, + functionPayloadToBeHashed, thisArg: this, }).handle({ durableMode }) as ReturnType; }; From 93438170bb35af101a3d351f468ac399eafc2652 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 17:13:45 +0000 Subject: [PATCH 04/17] Remove unused durableArn --- packages/idempotency/src/makeIdempotent.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index bfe278a71e..f2bca6c171 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -134,11 +134,10 @@ function makeIdempotent( functionPayloadToBeHashed = args[0]; } } - // i.e 'arn:aws:lambda:eu-south-1:801530346538:function:test-aamorosi:$LATEST/durable-execution/8893c114-49cc-4a35-9b17-32a40e9a8cf1/a11e1ae0-266a-33e6-ac46-d72f543cd215' - const durableArn = args[1]?.executionContext?.durableExecutionArn; - // i.e. 'ReplayMode' = 'ExecutionMode'; + + const durableMode = args[1]?.durableExecutionMode; - const durableSuffix = `${durableArn.split('/').slice(-2).join('/')}`; + return new IdempotencyHandler({ functionToMakeIdempotent: fn, idempotencyConfig: idempotencyConfig, From afe28087597139fe3ce040e649d66842f5b5f747 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 17:22:18 +0000 Subject: [PATCH 05/17] Handle the case where the context is DurableContext type --- packages/idempotency/src/makeIdempotent.ts | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index f2bca6c171..2d97aaa4db 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -17,6 +17,15 @@ const isContext = (arg: unknown): arg is Context => { ); }; +const isDurableContext = (arg: unknown): boolean => { + return ( + arg !== undefined && + arg !== null && + typeof arg === 'object' && + 'step' in arg + ); +} + const isFnHandler = ( fn: AnyFunction, args: Parameters @@ -26,7 +35,7 @@ const isFnHandler = ( fn !== undefined && fn !== null && typeof fn === 'function' && - isContext(args[1]) + (isContext(args[1])|| isDurableContext(args[1])) ); }; @@ -125,7 +134,9 @@ function makeIdempotent( let functionPayloadToBeHashed: JSONValue; if (isFnHandler(fn, args)) { - idempotencyConfig.registerLambdaContext(args[1]); + // If it's a durable context, retrieve the lambdaContext property + // Otherwise use the context + idempotencyConfig.registerLambdaContext(args[1]?.lambdaContext || args[1]); functionPayloadToBeHashed = args[0]; } else { if (isOptionsWithDataIndexArgument(options)) { @@ -135,7 +146,6 @@ function makeIdempotent( } } - const durableMode = args[1]?.durableExecutionMode; return new IdempotencyHandler({ From 94bfc85ae7be48139806d8aa6d4a82ced2f09ffb Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 17:46:32 +0000 Subject: [PATCH 06/17] Fix tests --- packages/idempotency/src/IdempotencyHandler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index 4642f2a7a2..a1f71c9493 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -181,8 +181,8 @@ export class IdempotencyHandler { public async handle({ durableMode, }: { - durableMode: DurableMode; - }): Promise> { + durableMode?: DurableMode; + } = {}): Promise> { // early return if we should skip idempotency completely if (this.shouldSkipIdempotency()) { return await this.#functionToMakeIdempotent.apply( From c1c0490639815996fa75afb74142c0f96a78f7c0 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 17:46:41 +0000 Subject: [PATCH 07/17] Add durable test (broken) --- .../idempotency/tests/unit/durable.test.ts | 44 +++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 packages/idempotency/tests/unit/durable.test.ts diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts new file mode 100644 index 0000000000..33d854b0ce --- /dev/null +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -0,0 +1,44 @@ +import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js" +import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing" +import { makeIdempotent } from "src/makeIdempotent.js" +import { PersistenceLayerTestClass } from "tests/helpers/idempotencyUtils.js"; +import { describe, beforeAll, afterAll, it, expect } from "vitest" + +const mockIdempotencyOptions = { + persistenceStore: new PersistenceLayerTestClass(), +}; + +beforeAll(()=> LocalDurableTestRunner.setupTestEnvironment()) +afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) + +describe("Given a durable function using the idempotency utility", ()=> { + it("Allows a replayed execution",async ()=> { + + const handlerFunction = withDurableExecution( + makeIdempotent( + async (event, context: DurableContext) => { + console.log({ event, context }); + console.log('starting function'); + + await context.wait('wait step', { seconds: 10 }); + + console.log('Reached second step'); + + const now = new Date().toISOString(); + return { statusCode: 200, message: 'success', now }; + }, + mockIdempotencyOptions, + ), + ); + + + const runner = new LocalDurableTestRunner({handlerFunction}) + const payload = {"key":"value"} + const execution = await runner.run({payload}) + const result = execution.getResult() + expect(result).toContain({statusCode: 200}) + + + }) + +}) From 690a6631876daeac89b574576be5e44a9a2641ba Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 18:20:58 +0000 Subject: [PATCH 08/17] Fix tests --- .../idempotency/tests/unit/durable.test.ts | 54 ++++++++++--------- 1 file changed, 29 insertions(+), 25 deletions(-) diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts index 33d854b0ce..e03ebac623 100644 --- a/packages/idempotency/tests/unit/durable.test.ts +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -2,7 +2,7 @@ import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing" import { makeIdempotent } from "src/makeIdempotent.js" import { PersistenceLayerTestClass } from "tests/helpers/idempotencyUtils.js"; -import { describe, beforeAll, afterAll, it, expect } from "vitest" +import { describe, beforeAll, afterAll, it, expect, vi } from "vitest" const mockIdempotencyOptions = { persistenceStore: new PersistenceLayerTestClass(), @@ -12,33 +12,37 @@ beforeAll(()=> LocalDurableTestRunner.setupTestEnvironment()) afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) describe("Given a durable function using the idempotency utility", ()=> { - it("Allows a replayed execution",async ()=> { - - const handlerFunction = withDurableExecution( - makeIdempotent( - async (event, context: DurableContext) => { - console.log({ event, context }); - console.log('starting function'); - - await context.wait('wait step', { seconds: 10 }); - - console.log('Reached second step'); - - const now = new Date().toISOString(); - return { statusCode: 200, message: 'success', now }; - }, - mockIdempotencyOptions, - ), - ); - - - const runner = new LocalDurableTestRunner({handlerFunction}) + it("allows an execution with a replay",async ()=> { + + const handlerFunction = withDurableExecution(async (event, context: DurableContext) => { + // Awaiting the testing sdk to implement this function + (context as any).lambdaContext = { + getRemainingTimeInMillis: vi.fn(() => 300000) // 5 minutes, + } + + const inner = makeIdempotent( + async (event, context: DurableContext) => { + try { + await context.wait('wait step', { seconds: 5 }); + return { statusCode: 200 }; + } catch (error) { + console.error(error) + return { statusCode: 400, message: error } + } + }, + mockIdempotencyOptions, + ) + return inner(event, context) + }) + + + const runner = new LocalDurableTestRunner({handlerFunction, skipTime: true}) const payload = {"key":"value"} const execution = await runner.run({payload}) - const result = execution.getResult() - expect(result).toContain({statusCode: 200}) - + const result = execution.getResult() + console.log({result}) + expect(result).toEqual({statusCode: 200}) }) }) From 8e187b0b57f559b1707ea337ca1143ba8b70f129 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 18:21:41 +0000 Subject: [PATCH 09/17] Remove unused log --- packages/idempotency/tests/unit/durable.test.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts index e03ebac623..69f5ccabd6 100644 --- a/packages/idempotency/tests/unit/durable.test.ts +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -41,7 +41,6 @@ describe("Given a durable function using the idempotency utility", ()=> { const execution = await runner.run({payload}) const result = execution.getResult() - console.log({result}) expect(result).toEqual({statusCode: 200}) }) From a3c80160fd3ef866aabd78cf51693041f4339fd6 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 20:48:58 +0000 Subject: [PATCH 10/17] Improve unit tests --- .../idempotency/tests/unit/durable.test.ts | 171 +++++++++++++++--- 1 file changed, 141 insertions(+), 30 deletions(-) diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts index 69f5ccabd6..feb28537ed 100644 --- a/packages/idempotency/tests/unit/durable.test.ts +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -2,7 +2,12 @@ import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing" import { makeIdempotent } from "src/makeIdempotent.js" import { PersistenceLayerTestClass } from "tests/helpers/idempotencyUtils.js"; +import { IdempotencyAlreadyInProgressError, IdempotencyItemAlreadyExistsError } from "../../src/errors.js"; +import { IdempotencyRecord } from "../../src/persistence/index.js"; +import { IdempotencyRecordStatus } from "../../src/constants.js"; import { describe, beforeAll, afterAll, it, expect, vi } from "vitest" +import { IdempotencyHandler } from "src/IdempotencyHandler.js"; +import { IdempotencyConfig } from "src/IdempotencyConfig.js"; const mockIdempotencyOptions = { persistenceStore: new PersistenceLayerTestClass(), @@ -12,36 +17,142 @@ beforeAll(()=> LocalDurableTestRunner.setupTestEnvironment()) afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) describe("Given a durable function using the idempotency utility", ()=> { - it("allows an execution with a replay",async ()=> { - - const handlerFunction = withDurableExecution(async (event, context: DurableContext) => { - // Awaiting the testing sdk to implement this function - (context as any).lambdaContext = { - getRemainingTimeInMillis: vi.fn(() => 300000) // 5 minutes, - } - - const inner = makeIdempotent( - async (event, context: DurableContext) => { - try { - await context.wait('wait step', { seconds: 5 }); - return { statusCode: 200 }; - } catch (error) { - console.error(error) - return { statusCode: 400, message: error } - } - }, - mockIdempotencyOptions, - ) - return inner(event, context) - }) - - - const runner = new LocalDurableTestRunner({handlerFunction, skipTime: true}) - const payload = {"key":"value"} - const execution = await runner.run({payload}) - - const result = execution.getResult() - expect(result).toEqual({statusCode: 200}) + it("allows execution when the DurableMode is Replay and there is IN PROGRESS record", async ()=> { + // Arrange + const persistenceStore = new PersistenceLayerTestClass(); + + // Mock saveInProgress to simulate an existing IN_PROGRESS record + vi.spyOn(persistenceStore, 'saveInProgress') + .mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); + + const mockFunctionToMakeIdempotent = vi.fn(); + const mockFunctionPayloadToBeHashed = {}; + const mockIdempotencyOptions = { + persistenceStore, + config: new IdempotencyConfig({}), + }; + + const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [], + idempotencyConfig: mockIdempotencyOptions.config, + }); + + // Act + await idempotentHandler.handle({durableMode: "ReplayMode"}) + + // Assess + expect(mockFunctionToMakeIdempotent).toBeCalled() + + }) + it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { + + // Arrange + const persistenceStore = new PersistenceLayerTestClass(); + + // Mock saveInProgress to simulate an existing IN_PROGRESS record + vi.spyOn(persistenceStore, 'saveInProgress') + .mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); + + const mockFunctionToMakeIdempotent = vi.fn(); + const mockFunctionPayloadToBeHashed = {}; + const mockIdempotencyOptions = { + persistenceStore, + config: new IdempotencyConfig({}), + }; + + const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [], + idempotencyConfig: mockIdempotencyOptions.config, + }); + // Act & Assess + await expect(idempotentHandler.handle({ durableMode: "ExecutionMode" })).rejects.toThrow(IdempotencyAlreadyInProgressError); + }) + + + + it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { + + // Arrange + const persistenceStore = new PersistenceLayerTestClass(); + + vi.spyOn( + persistenceStore, + 'saveInProgress' + ).mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + + const stubRecord = new IdempotencyRecord({ + idempotencyKey: 'idempotencyKey', + expiryTimestamp: Date.now() + 10000, + inProgressExpiryTimestamp: 0, + responseData: { response: false }, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.COMPLETED, + }); + const getRecordSpy = vi + .spyOn(persistenceStore, 'getRecord') + .mockResolvedValue(stubRecord); + + const mockFunctionToMakeIdempotent = vi.fn(); + const mockFunctionPayloadToBeHashed = {}; + const mockIdempotencyOptions = { + persistenceStore, + config: new IdempotencyConfig({}), + }; + + const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [{"key":"value"}], + idempotencyConfig: mockIdempotencyOptions.config, + }); + + // Act + const result = await idempotentHandler.handle({durableMode: "ExecutionMode"}) + + // Assess + expect(result).toStrictEqual({ response: false }); + expect(getRecordSpy).toHaveBeenCalledTimes(1); + expect(getRecordSpy).toHaveBeenCalledWith(mockFunctionPayloadToBeHashed); + + }) + + // it("registers the lambda context when a durable context is passed to the makeIdempotent function", () => { + + // const registerLambdaContextSpy = vi.spyOn(IdempotencyConfig.prototype, "registerLambdaContext") + // const persistenceStore = new PersistenceLayerTestClass(); + // const inner = makeIdempotent(async (event, context) => { }, {persistenceStore}) + + // const handler = withDurableExecution(inner) + // handler({ DurableExecutionArn: "", CheckpointToken: "", InitialExecutionState: { Operations: [],NextMarker:""} }, {}) + + // expect(registerLambdaContextSpy).toHaveBeenCalledOnce() + + // }) }) From b50e641bf522220340db103ea37f8435f922aacf Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 20:54:29 +0000 Subject: [PATCH 11/17] Refactor tests --- .../idempotency/tests/unit/durable.test.ts | 88 +++++-------------- 1 file changed, 20 insertions(+), 68 deletions(-) diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts index feb28537ed..0094c15834 100644 --- a/packages/idempotency/tests/unit/durable.test.ts +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -9,18 +9,33 @@ import { describe, beforeAll, afterAll, it, expect, vi } from "vitest" import { IdempotencyHandler } from "src/IdempotencyHandler.js"; import { IdempotencyConfig } from "src/IdempotencyConfig.js"; +beforeAll(()=> { + vi.clearAllMocks(); + vi.restoreAllMocks(); + LocalDurableTestRunner.setupTestEnvironment()}) + +afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) + + +const persistenceStore = new PersistenceLayerTestClass(); +const mockFunctionToMakeIdempotent = vi.fn(); +const mockFunctionPayloadToBeHashed = {}; const mockIdempotencyOptions = { - persistenceStore: new PersistenceLayerTestClass(), + persistenceStore, + config: new IdempotencyConfig({}), }; -beforeAll(()=> LocalDurableTestRunner.setupTestEnvironment()) -afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) +const idempotentHandler = new IdempotencyHandler({ + functionToMakeIdempotent: mockFunctionToMakeIdempotent, + functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, + persistenceStore: mockIdempotencyOptions.persistenceStore, + functionArguments: [], + idempotencyConfig: mockIdempotencyOptions.config, +}); describe("Given a durable function using the idempotency utility", ()=> { it("allows execution when the DurableMode is Replay and there is IN PROGRESS record", async ()=> { // Arrange - const persistenceStore = new PersistenceLayerTestClass(); - // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') .mockRejectedValueOnce( @@ -34,21 +49,6 @@ describe("Given a durable function using the idempotency utility", ()=> { ) ); - const mockFunctionToMakeIdempotent = vi.fn(); - const mockFunctionPayloadToBeHashed = {}; - const mockIdempotencyOptions = { - persistenceStore, - config: new IdempotencyConfig({}), - }; - - const idempotentHandler = new IdempotencyHandler({ - functionToMakeIdempotent: mockFunctionToMakeIdempotent, - functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, - persistenceStore: mockIdempotencyOptions.persistenceStore, - functionArguments: [], - idempotencyConfig: mockIdempotencyOptions.config, - }); - // Act await idempotentHandler.handle({durableMode: "ReplayMode"}) @@ -61,8 +61,6 @@ describe("Given a durable function using the idempotency utility", ()=> { it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { // Arrange - const persistenceStore = new PersistenceLayerTestClass(); - // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') .mockRejectedValueOnce( @@ -76,31 +74,13 @@ describe("Given a durable function using the idempotency utility", ()=> { ) ); - const mockFunctionToMakeIdempotent = vi.fn(); - const mockFunctionPayloadToBeHashed = {}; - const mockIdempotencyOptions = { - persistenceStore, - config: new IdempotencyConfig({}), - }; - - const idempotentHandler = new IdempotencyHandler({ - functionToMakeIdempotent: mockFunctionToMakeIdempotent, - functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, - persistenceStore: mockIdempotencyOptions.persistenceStore, - functionArguments: [], - idempotencyConfig: mockIdempotencyOptions.config, - }); // Act & Assess await expect(idempotentHandler.handle({ durableMode: "ExecutionMode" })).rejects.toThrow(IdempotencyAlreadyInProgressError); }) - - it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { // Arrange - const persistenceStore = new PersistenceLayerTestClass(); - vi.spyOn( persistenceStore, 'saveInProgress' @@ -118,21 +98,6 @@ describe("Given a durable function using the idempotency utility", ()=> { .spyOn(persistenceStore, 'getRecord') .mockResolvedValue(stubRecord); - const mockFunctionToMakeIdempotent = vi.fn(); - const mockFunctionPayloadToBeHashed = {}; - const mockIdempotencyOptions = { - persistenceStore, - config: new IdempotencyConfig({}), - }; - - const idempotentHandler = new IdempotencyHandler({ - functionToMakeIdempotent: mockFunctionToMakeIdempotent, - functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, - persistenceStore: mockIdempotencyOptions.persistenceStore, - functionArguments: [{"key":"value"}], - idempotencyConfig: mockIdempotencyOptions.config, - }); - // Act const result = await idempotentHandler.handle({durableMode: "ExecutionMode"}) @@ -142,17 +107,4 @@ describe("Given a durable function using the idempotency utility", ()=> { expect(getRecordSpy).toHaveBeenCalledWith(mockFunctionPayloadToBeHashed); }) - - // it("registers the lambda context when a durable context is passed to the makeIdempotent function", () => { - - // const registerLambdaContextSpy = vi.spyOn(IdempotencyConfig.prototype, "registerLambdaContext") - // const persistenceStore = new PersistenceLayerTestClass(); - // const inner = makeIdempotent(async (event, context) => { }, {persistenceStore}) - - // const handler = withDurableExecution(inner) - // handler({ DurableExecutionArn: "", CheckpointToken: "", InitialExecutionState: { Operations: [],NextMarker:""} }, {}) - - // expect(registerLambdaContextSpy).toHaveBeenCalledOnce() - - // }) }) From 9d3348d7dce6364eb4fc3db54814b8827e3fd455 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 20:55:47 +0000 Subject: [PATCH 12/17] Remove unused test items --- packages/idempotency/tests/unit/durable.test.ts | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts index 0094c15834..544687ce8b 100644 --- a/packages/idempotency/tests/unit/durable.test.ts +++ b/packages/idempotency/tests/unit/durable.test.ts @@ -1,20 +1,17 @@ -import { DurableContext, withDurableExecution } from "@aws/durable-execution-sdk-js" -import { LocalDurableTestRunner } from "@aws/durable-execution-sdk-js-testing" -import { makeIdempotent } from "src/makeIdempotent.js" import { PersistenceLayerTestClass } from "tests/helpers/idempotencyUtils.js"; import { IdempotencyAlreadyInProgressError, IdempotencyItemAlreadyExistsError } from "../../src/errors.js"; import { IdempotencyRecord } from "../../src/persistence/index.js"; import { IdempotencyRecordStatus } from "../../src/constants.js"; -import { describe, beforeAll, afterAll, it, expect, vi } from "vitest" +import { describe, beforeAll, it, expect, vi } from "vitest" import { IdempotencyHandler } from "src/IdempotencyHandler.js"; import { IdempotencyConfig } from "src/IdempotencyConfig.js"; -beforeAll(()=> { +beforeAll(() => { vi.clearAllMocks(); vi.restoreAllMocks(); - LocalDurableTestRunner.setupTestEnvironment()}) +}) + -afterAll(()=> LocalDurableTestRunner.teardownTestEnvironment()) const persistenceStore = new PersistenceLayerTestClass(); @@ -59,7 +56,6 @@ describe("Given a durable function using the idempotency utility", ()=> { }) it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { - // Arrange // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') From 6f7ea86aee4f73bfb95de95cadd6d6ab62d26e72 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 21:01:03 +0000 Subject: [PATCH 13/17] Move tests to IdempotentHandler test file --- .../tests/unit/IdempotencyHandler.test.ts | 72 ++++++++++++ .../idempotency/tests/unit/durable.test.ts | 106 ------------------ 2 files changed, 72 insertions(+), 106 deletions(-) delete mode 100644 packages/idempotency/tests/unit/durable.test.ts diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 2481314dc1..d6527237c9 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -239,6 +239,78 @@ describe('Class IdempotencyHandler', () => { ); expect(mockProcessIdempotency).toHaveBeenCalledTimes(MAX_RETRIES + 1); }); + + it("allows execution when the DurableMode is Replay and there is IN PROGRESS record", async ()=> { + // Arrange + // Mock saveInProgress to simulate an existing IN_PROGRESS record + vi.spyOn(persistenceStore, 'saveInProgress') + .mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); + + // Act + await idempotentHandler.handle({durableMode: "ReplayMode"}) + + // Assess + expect(mockFunctionToMakeIdempotent).toBeCalled() + + + }) + + it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { + // Arrange + // Mock saveInProgress to simulate an existing IN_PROGRESS record + vi.spyOn(persistenceStore, 'saveInProgress') + .mockRejectedValueOnce( + new IdempotencyItemAlreadyExistsError( + 'Record exists', + new IdempotencyRecord({ + idempotencyKey: 'test-key', + status: IdempotencyRecordStatus.INPROGRESS, + expiryTimestamp: Date.now() + 10000, + }) + ) + ); + + // Act & Assess + await expect(idempotentHandler.handle({ durableMode: "ExecutionMode" })).rejects.toThrow(IdempotencyAlreadyInProgressError); + }) + + it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { + + // Arrange + vi.spyOn( + persistenceStore, + 'saveInProgress' + ).mockRejectedValue(new IdempotencyItemAlreadyExistsError()); + + const stubRecord = new IdempotencyRecord({ + idempotencyKey: 'idempotencyKey', + expiryTimestamp: Date.now() + 10000, + inProgressExpiryTimestamp: 0, + responseData: { response: false }, + payloadHash: 'payloadHash', + status: IdempotencyRecordStatus.COMPLETED, + }); + const getRecordSpy = vi + .spyOn(persistenceStore, 'getRecord') + .mockResolvedValue(stubRecord); + + // Act + const result = await idempotentHandler.handle({durableMode: "ExecutionMode"}) + + // Assess + expect(result).toStrictEqual({ response: false }); + expect(getRecordSpy).toHaveBeenCalledTimes(1); + expect(getRecordSpy).toHaveBeenCalledWith(mockFunctionPayloadToBeHashed); + }) }); describe('Method: getFunctionResult', () => { diff --git a/packages/idempotency/tests/unit/durable.test.ts b/packages/idempotency/tests/unit/durable.test.ts deleted file mode 100644 index 544687ce8b..0000000000 --- a/packages/idempotency/tests/unit/durable.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { PersistenceLayerTestClass } from "tests/helpers/idempotencyUtils.js"; -import { IdempotencyAlreadyInProgressError, IdempotencyItemAlreadyExistsError } from "../../src/errors.js"; -import { IdempotencyRecord } from "../../src/persistence/index.js"; -import { IdempotencyRecordStatus } from "../../src/constants.js"; -import { describe, beforeAll, it, expect, vi } from "vitest" -import { IdempotencyHandler } from "src/IdempotencyHandler.js"; -import { IdempotencyConfig } from "src/IdempotencyConfig.js"; - -beforeAll(() => { - vi.clearAllMocks(); - vi.restoreAllMocks(); -}) - - - - -const persistenceStore = new PersistenceLayerTestClass(); -const mockFunctionToMakeIdempotent = vi.fn(); -const mockFunctionPayloadToBeHashed = {}; -const mockIdempotencyOptions = { - persistenceStore, - config: new IdempotencyConfig({}), -}; - -const idempotentHandler = new IdempotencyHandler({ - functionToMakeIdempotent: mockFunctionToMakeIdempotent, - functionPayloadToBeHashed: mockFunctionPayloadToBeHashed, - persistenceStore: mockIdempotencyOptions.persistenceStore, - functionArguments: [], - idempotencyConfig: mockIdempotencyOptions.config, -}); - -describe("Given a durable function using the idempotency utility", ()=> { - it("allows execution when the DurableMode is Replay and there is IN PROGRESS record", async ()=> { - // Arrange - // Mock saveInProgress to simulate an existing IN_PROGRESS record - vi.spyOn(persistenceStore, 'saveInProgress') - .mockRejectedValueOnce( - new IdempotencyItemAlreadyExistsError( - 'Record exists', - new IdempotencyRecord({ - idempotencyKey: 'test-key', - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: Date.now() + 10000, - }) - ) - ); - - // Act - await idempotentHandler.handle({durableMode: "ReplayMode"}) - - // Assess - expect(mockFunctionToMakeIdempotent).toBeCalled() - - - }) - - it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { - // Arrange - // Mock saveInProgress to simulate an existing IN_PROGRESS record - vi.spyOn(persistenceStore, 'saveInProgress') - .mockRejectedValueOnce( - new IdempotencyItemAlreadyExistsError( - 'Record exists', - new IdempotencyRecord({ - idempotencyKey: 'test-key', - status: IdempotencyRecordStatus.INPROGRESS, - expiryTimestamp: Date.now() + 10000, - }) - ) - ); - - // Act & Assess - await expect(idempotentHandler.handle({ durableMode: "ExecutionMode" })).rejects.toThrow(IdempotencyAlreadyInProgressError); - }) - - it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { - - // Arrange - vi.spyOn( - persistenceStore, - 'saveInProgress' - ).mockRejectedValue(new IdempotencyItemAlreadyExistsError()); - - const stubRecord = new IdempotencyRecord({ - idempotencyKey: 'idempotencyKey', - expiryTimestamp: Date.now() + 10000, - inProgressExpiryTimestamp: 0, - responseData: { response: false }, - payloadHash: 'payloadHash', - status: IdempotencyRecordStatus.COMPLETED, - }); - const getRecordSpy = vi - .spyOn(persistenceStore, 'getRecord') - .mockResolvedValue(stubRecord); - - // Act - const result = await idempotentHandler.handle({durableMode: "ExecutionMode"}) - - // Assess - expect(result).toStrictEqual({ response: false }); - expect(getRecordSpy).toHaveBeenCalledTimes(1); - expect(getRecordSpy).toHaveBeenCalledWith(mockFunctionPayloadToBeHashed); - - }) -}) From f83d293d5f3dc8883eb48f911ba78af72b7effb9 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Wed, 19 Nov 2025 21:17:44 +0000 Subject: [PATCH 14/17] Add test for registering lambda context --- .../idempotency/tests/unit/makeIdempotent.test.ts | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/packages/idempotency/tests/unit/makeIdempotent.test.ts b/packages/idempotency/tests/unit/makeIdempotent.test.ts index bd4e39238e..6986c97c1d 100644 --- a/packages/idempotency/tests/unit/makeIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeIdempotent.test.ts @@ -597,4 +597,19 @@ describe('Function: makeIdempotent', () => { ); expect(getRecordSpy).toHaveBeenCalledTimes(0); }); + + it("registers the LambdaContext when provided a durable context", async ()=> { + + const registerLambdaContextSpy = vi.spyOn(IdempotencyConfig.prototype, "registerLambdaContext") + const fn = async (_event: any, _context: any) => { } + const handler = makeIdempotent(fn, mockIdempotencyOptions) + + const mockDurableContext = {step: vi.fn(), lambdaContext: context} + await handler(event, mockDurableContext) + + expect(registerLambdaContextSpy).toHaveBeenCalledOnce() + + + + }) }); From 65e968bac03dc9883982ddebb8a8cc9dc3b31278 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Mon, 24 Nov 2025 09:44:46 +0000 Subject: [PATCH 15/17] Refactor `durableMode` to `isReplay` --- packages/idempotency/src/IdempotencyHandler.ts | 13 ++++++------- packages/idempotency/src/makeIdempotent.ts | 4 ++-- .../idempotency/src/types/IdempotencyOptions.ts | 3 --- packages/idempotency/src/types/index.ts | 1 - .../tests/unit/IdempotencyHandler.test.ts | 10 +++++----- 5 files changed, 13 insertions(+), 18 deletions(-) diff --git a/packages/idempotency/src/IdempotencyHandler.ts b/packages/idempotency/src/IdempotencyHandler.ts index a1f71c9493..456b9ed0ca 100644 --- a/packages/idempotency/src/IdempotencyHandler.ts +++ b/packages/idempotency/src/IdempotencyHandler.ts @@ -17,7 +17,6 @@ import type { BasePersistenceLayer } from './persistence/BasePersistenceLayer.js import type { IdempotencyRecord } from './persistence/IdempotencyRecord.js'; import type { AnyFunction, - DurableMode, IdempotencyHandlerOptions, } from './types/IdempotencyOptions.js'; @@ -179,9 +178,9 @@ export class IdempotencyHandler { * cases we can safely retry the handling a few times. */ public async handle({ - durableMode, + isReplay, }: { - durableMode?: DurableMode; + isReplay?: boolean; } = {}): Promise> { // early return if we should skip idempotency completely if (this.shouldSkipIdempotency()) { @@ -195,7 +194,7 @@ export class IdempotencyHandler { for (let retryNo = 0; retryNo <= MAX_RETRIES; retryNo++) { try { const { isIdempotent, result } = - await this.#saveInProgressOrReturnExistingResult({ durableMode }); + await this.#saveInProgressOrReturnExistingResult({ isReplay }); if (isIdempotent) return result as ReturnType; return await this.getFunctionResult(); @@ -362,9 +361,9 @@ export class IdempotencyHandler { * and validate it to ensure that it is consistent with the payload to be hashed. */ readonly #saveInProgressOrReturnExistingResult = async ({ - durableMode, + isReplay, }: { - durableMode?: DurableMode; + isReplay?: boolean; } = {}): Promise<{ isIdempotent: boolean; result: JSONValue; @@ -390,7 +389,7 @@ export class IdempotencyHandler { { cause: error } ); if (error.name === 'IdempotencyItemAlreadyExistsError') { - if (durableMode === 'ReplayMode') { + if (isReplay) { return returnValue; } diff --git a/packages/idempotency/src/makeIdempotent.ts b/packages/idempotency/src/makeIdempotent.ts index 2d97aaa4db..09a319b8e5 100644 --- a/packages/idempotency/src/makeIdempotent.ts +++ b/packages/idempotency/src/makeIdempotent.ts @@ -146,7 +146,7 @@ function makeIdempotent( } } - const durableMode = args[1]?.durableExecutionMode; + const isReplay = args[1]?.durableExecutionMode === "REPLAY_MODE" return new IdempotencyHandler({ functionToMakeIdempotent: fn, @@ -156,7 +156,7 @@ function makeIdempotent( functionArguments: args, functionPayloadToBeHashed, thisArg: this, - }).handle({ durableMode }) as ReturnType; + }).handle({ isReplay }) as ReturnType }; } diff --git a/packages/idempotency/src/types/IdempotencyOptions.ts b/packages/idempotency/src/types/IdempotencyOptions.ts index c243293ca4..e586764646 100644 --- a/packages/idempotency/src/types/IdempotencyOptions.ts +++ b/packages/idempotency/src/types/IdempotencyOptions.ts @@ -206,8 +206,6 @@ type IdempotencyConfigOptions = { responseHook?: ResponseHook; }; -type DurableMode = 'ExecutionMode' | 'ReplayMode'; - export type { AnyFunction, IdempotencyConfigOptions, @@ -215,5 +213,4 @@ export type { IdempotencyLambdaHandlerOptions, IdempotencyHandlerOptions, ResponseHook, - DurableMode, }; diff --git a/packages/idempotency/src/types/index.ts b/packages/idempotency/src/types/index.ts index 26c192c8df..1d77754342 100644 --- a/packages/idempotency/src/types/index.ts +++ b/packages/idempotency/src/types/index.ts @@ -15,7 +15,6 @@ export type { } from './DynamoDBPersistence.js'; export type { AnyFunction, - DurableMode, IdempotencyConfigOptions, IdempotencyHandlerOptions, IdempotencyLambdaHandlerOptions, diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index d6527237c9..ca8b79602f 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -240,7 +240,7 @@ describe('Class IdempotencyHandler', () => { expect(mockProcessIdempotency).toHaveBeenCalledTimes(MAX_RETRIES + 1); }); - it("allows execution when the DurableMode is Replay and there is IN PROGRESS record", async ()=> { + it("allows execution when isReplay is true and there is IN PROGRESS record", async ()=> { // Arrange // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') @@ -256,7 +256,7 @@ describe('Class IdempotencyHandler', () => { ); // Act - await idempotentHandler.handle({durableMode: "ReplayMode"}) + await idempotentHandler.handle({isReplay: true}) // Assess expect(mockFunctionToMakeIdempotent).toBeCalled() @@ -264,7 +264,7 @@ describe('Class IdempotencyHandler', () => { }) - it("raises an IdempotencyAlreadyInProgressError error when the DurableMode is Execution and there is an IN PROGRESS record", async ()=> { + it("raises an IdempotencyAlreadyInProgressError error when isReplay is false and there is an IN PROGRESS record", async ()=> { // Arrange // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') @@ -280,7 +280,7 @@ describe('Class IdempotencyHandler', () => { ); // Act & Assess - await expect(idempotentHandler.handle({ durableMode: "ExecutionMode" })).rejects.toThrow(IdempotencyAlreadyInProgressError); + await expect(idempotentHandler.handle({ isReplay: false })).rejects.toThrow(IdempotencyAlreadyInProgressError); }) it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { @@ -304,7 +304,7 @@ describe('Class IdempotencyHandler', () => { .mockResolvedValue(stubRecord); // Act - const result = await idempotentHandler.handle({durableMode: "ExecutionMode"}) + const result = await idempotentHandler.handle({isReplay: false}) // Assess expect(result).toStrictEqual({ response: false }); From bb77da48a902b24068815089c5b4161274166568 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Mon, 1 Dec 2025 13:03:04 +0000 Subject: [PATCH 16/17] Fix test formatting --- .../idempotency/tests/unit/IdempotencyHandler.test.ts | 2 -- packages/idempotency/tests/unit/makeIdempotent.test.ts | 9 ++++----- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index ca8b79602f..169fbb69be 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -260,8 +260,6 @@ describe('Class IdempotencyHandler', () => { // Assess expect(mockFunctionToMakeIdempotent).toBeCalled() - - }) it("raises an IdempotencyAlreadyInProgressError error when isReplay is false and there is an IN PROGRESS record", async ()=> { diff --git a/packages/idempotency/tests/unit/makeIdempotent.test.ts b/packages/idempotency/tests/unit/makeIdempotent.test.ts index 6986c97c1d..38011a0770 100644 --- a/packages/idempotency/tests/unit/makeIdempotent.test.ts +++ b/packages/idempotency/tests/unit/makeIdempotent.test.ts @@ -599,17 +599,16 @@ describe('Function: makeIdempotent', () => { }); it("registers the LambdaContext when provided a durable context", async ()=> { - + // Prepare const registerLambdaContextSpy = vi.spyOn(IdempotencyConfig.prototype, "registerLambdaContext") const fn = async (_event: any, _context: any) => { } const handler = makeIdempotent(fn, mockIdempotencyOptions) - const mockDurableContext = {step: vi.fn(), lambdaContext: context} + + // Act await handler(event, mockDurableContext) + // Assess expect(registerLambdaContextSpy).toHaveBeenCalledOnce() - - - }) }); From 6b96436568f11d67a7138e740885361070ac5ea8 Mon Sep 17 00:00:00 2001 From: Connor Kirkpatrick Date: Tue, 2 Dec 2025 18:28:56 +0000 Subject: [PATCH 17/17] Update test comment --- packages/idempotency/tests/unit/IdempotencyHandler.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts index 169fbb69be..abe315ec5c 100644 --- a/packages/idempotency/tests/unit/IdempotencyHandler.test.ts +++ b/packages/idempotency/tests/unit/IdempotencyHandler.test.ts @@ -241,7 +241,7 @@ describe('Class IdempotencyHandler', () => { }); it("allows execution when isReplay is true and there is IN PROGRESS record", async ()=> { - // Arrange + // Prepare // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') .mockRejectedValueOnce( @@ -263,7 +263,7 @@ describe('Class IdempotencyHandler', () => { }) it("raises an IdempotencyAlreadyInProgressError error when isReplay is false and there is an IN PROGRESS record", async ()=> { - // Arrange + // Prepare // Mock saveInProgress to simulate an existing IN_PROGRESS record vi.spyOn(persistenceStore, 'saveInProgress') .mockRejectedValueOnce( @@ -283,7 +283,7 @@ describe('Class IdempotencyHandler', () => { it("returns the result of the original durable execution when another durable execution with the same payload is invoked", async () => { - // Arrange + // Prepare vi.spyOn( persistenceStore, 'saveInProgress'