Test a compensation handler
This test checks the SDK contract with a business adapter that releases a reservation: identifier, original receipt, idempotency key and result. A local adapter holds a reservation in memory. See the compensation guide for runtime-governed compensation.
Install the SDK
Terminal
touba sdk install javascript
Define the handler
release.mjs
import {createToolHandler, createToolResult} from '@touba/sdk';
export function releaseHandler(releaseReservation) {
return createToolHandler({
toolRef:'tool:reservations/ReleaseReservation',
inputSignature:[{name:'reservation_id',type_ref:'builtin:Text'}],
outputType:'builtin:Text',
run:async ({request:reservation_id, context}) => {
const response = await releaseReservation({
reservationId:reservation_id,
originalReceipt:context.compensation.original_effect_receipt,
idempotencyKey:context.effect.effect_id,
});
return createToolResult(response.message, {
effectReceipt:context.createEffectReceipt({outcome:'applied',receipt_ref:response.receiptRef}),
compensationReceipt:context.createCompensationReceipt({result:'completed'}),
});
},
});
}
Check data and business idempotency
The execution metadata below are test fixtures. In execution, the runtime establishes context and checks receipts, authority and budgets.
exercise.mjs
import assert from 'node:assert/strict';
import {releaseHandler} from './release.mjs';
const reservations = new Set(['RES-42']);
const completed = new Map();
const original = {outcome:'applied', receipt_ref:'reservation:RES-42'};
let writes = 0;
const handler = releaseHandler(async ({reservationId, originalReceipt, idempotencyKey}) => {
assert.equal(reservationId,'RES-42');
assert.deepEqual(originalReceipt,original);
assert.equal(idempotencyKey,'effect:release-42');
if (!completed.has(idempotencyKey)) {
assert.equal(reservations.delete(reservationId),true); writes++;
completed.set(idempotencyKey,{message:'Released',receiptRef:'release:RES-42'});
}
return completed.get(idempotencyKey);
});
const request = {arguments:{reservation_id:'RES-42'},env:{},
compensation:{original_effect_receipt:original}, effect:{effect_id:'effect:release-42'},
createEffectReceipt:value=>value, createCompensationReceipt:value=>value};
await handler(request);
await handler(request);
assert.equal(writes,1); assert.equal(reservations.size,0);
await assert.rejects(handler({...request,arguments:{reservation_id:42}}));
assert.equal(writes,1);
console.log('COMPENSATION_HANDLER_CONTRACT_OK');
Terminal
node exercise.mjs
Expected result: COMPENSATION_HANDLER_CONTRACT_OK. Replace the adapter with your provider's implementation and persist its idempotency keys. This callback test complements runtime compensation tests.