tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Inspect app directory and AWS env
ls /app && echo "---" && env | grep -E "AWS|LOCALSTACK" | sort
--- AWS_ACCESS_KEY_ID=test AWS_DEFAULT_REGION=us-east-1 AWS_ENDPOINT_URL=http://localstack:4566 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test [stdout] --- AWS_ACCESS_KEY_ID=test AWS_DEFAULT_REGION=us-east-1 AWS_ENDPOINT_URL=http://localstack:4566 AWS_PAGER= AWS_SECRET_ACCESS_KEY=test
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Check working dir and tools
ls -la /app/ 2>&1; which aws python3 zip jq; aws --version; python3 --version
total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 23 04:25 .. /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 [stdout] total 8 drwxr-xr-x 2 root root 4096 Apr 21 20:05 . drwxr-xr-x 1 root root 4096 Apr 23 04:25 .. /usr/local/bin/aws /opt/venv/bin/python3 /usr/bin/zip /usr/bin/jq aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3
Verify localstack reachability
aws --endpoint-url=$AWS_ENDPOINT_URL sts get-caller-identity; echo "---"; curl -s http://localstack:4566/_localstack/health | jq .
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "available",
"ses": "disabled",
"sns": "disabled",
"sqs": "available",
"ssm": "disabled",
"stepfunctions": "available",
"sts": "running",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled"
},
"edition": "community",
"version": "4.13.1"
}
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
---
{
"services": {
"acm": "disabled",
"apigateway": "disabled",
"cloudformation": "disabled",
"cloudwatch": "disabled",
"config": "disabled",
"dynamodb": "available",
"dynamodbstreams": "available",
"ec2": "disabled",
"es": "disabled",
"events": "available",
"firehose": "disabled",
"iam": "available",
"kinesis": "available",
"kms": "available",
"lambda": "available",
"logs": "available",
"opensearch": "disabled",
"redshift": "disabled",
"resource-groups": "disabled",
"resourcegroupstaggingapi": "disabled",
"route53": "disabled",
"route53resolver": "disabled",
"s3": "available",
"s3control": "disabled",
"scheduler": "disabled",
"secretsmanager": "available",
"ses": "disabled",
"sns": "disabled",
"sqs": "available",
"ssm": "disabled",
"stepfunctions": "available",
"sts": "running",
"support": "disabled",
"swf": "disabled",
"transcribe": "disabled"
},
"edition": "community",
"version": "4.13.1"
}Create work directories
mkdir -p /app/lambdas /app/policies /app/sm /app/build
(Bash completed with no output)
import os, json, time, boto3
from botocore.exceptions import ClientError
ddb = boto3.resource("dynamodb")
inv = ddb.Table("inventory")
idem = ddb.Table("saga-idempotency")
def _idem_key(event):
return f"{event['idempotency_key']}:reserve"
def handler(event, _ctx):
key = _idem_key(event)
try:
idem.put_item(
Item={"idempotency_key": key, "ttl": int(time.time()) + 86400, "status": "DONE"},
ConditionExpression="attribute_not_exists(idempotency_key)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "reserve": {"already_done": True}}
raise
inv.update_item(
Key={"sku": event["sku"]},
UpdateExpression="SET reserved_qty = if_not_exists(reserved_qty, :z) + :q",
ExpressionAttributeValues={":q": event["qty"], ":z": 0},
)
return {**event, "reserve": {"sku": event["sku"], "qty": event["qty"]}}
import os, json, time, boto3
from botocore.exceptions import ClientError
ddb = boto3.resource("dynamodb")
payments = ddb.Table("payments")
idem = ddb.Table("saga-idempotency")
sm = boto3.client("secretsmanager")
def _idem_key(event):
return f"{event['idempotency_key']}:charge"
def handler(event, _ctx):
resp = sm.get_secret_value(SecretId="payment/gateway-apikey")
api_key = resp["SecretString"]
print(f"charge-payment: loaded gateway api key len={len(api_key)}")
if event.get("force_failure"):
raise RuntimeError("forced payment failure")
key = _idem_key(event)
try:
idem.put_item(
Item={"idempotency_key": key, "ttl": int(time.time()) + 86400, "status": "DONE"},
ConditionExpression="attribute_not_exists(idempotency_key)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "charge": {"already_done": True, "payment_id": f"pay-{event['order_id']}"}}
raise
payment_id = f"pay-{event['order_id']}"
payments.put_item(
Item={
"payment_id": payment_id,
"order_id": event["order_id"],
"amount": event["amount"],
"status": "CAPTURED",
}
)
return {**event, "charge": {"payment_id": payment_id, "status": "CAPTURED"}}
import os, json, time, boto3
from botocore.exceptions import ClientError
ddb = boto3.resource("dynamodb")
shipments = ddb.Table("shipments")
idem = ddb.Table("saga-idempotency")
def _idem_key(event):
return f"{event['idempotency_key']}:ship"
def handler(event, _ctx):
key = _idem_key(event)
try:
idem.put_item(
Item={"idempotency_key": key, "ttl": int(time.time()) + 86400, "status": "DONE"},
ConditionExpression="attribute_not_exists(idempotency_key)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "ship": {"already_done": True, "shipment_id": f"ship-{event['order_id']}"}}
raise
shipment_id = f"ship-{event['order_id']}"
shipments.put_item(
Item={
"shipment_id": shipment_id,
"order_id": event["order_id"],
"sku": event["sku"],
"qty": event["qty"],
"status": "CREATED",
}
)
return {**event, "ship": {"shipment_id": shipment_id, "status": "CREATED"}}
import os, json, time, boto3
from botocore.exceptions import ClientError
ddb = boto3.resource("dynamodb")
inv = ddb.Table("inventory")
idem = ddb.Table("saga-idempotency")
def _idem_key(event):
return f"{event['idempotency_key']}:release"
def handler(event, _ctx):
key = _idem_key(event)
try:
idem.put_item(
Item={"idempotency_key": key, "ttl": int(time.time()) + 86400, "status": "DONE"},
ConditionExpression="attribute_not_exists(idempotency_key)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "release": {"already_done": True}}
raise
if not event.get("reserve") or event.get("reserve", {}).get("already_done"):
pass
inv.update_item(
Key={"sku": event["sku"]},
UpdateExpression="SET reserved_qty = if_not_exists(reserved_qty, :z) - :q",
ExpressionAttributeValues={":q": event["qty"], ":z": 0},
)
return {**event, "release": {"sku": event["sku"], "qty": event["qty"]}}
import os, json, time, boto3
from botocore.exceptions import ClientError
ddb = boto3.resource("dynamodb")
payments = ddb.Table("payments")
idem = ddb.Table("saga-idempotency")
def _idem_key(event):
return f"{event['idempotency_key']}:refund"
def handler(event, _ctx):
key = _idem_key(event)
try:
idem.put_item(
Item={"idempotency_key": key, "ttl": int(time.time()) + 86400, "status": "DONE"},
ConditionExpression="attribute_not_exists(idempotency_key)",
)
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "refund": {"already_done": True}}
raise
payment_id = f"pay-{event['order_id']}"
try:
payments.update_item(
Key={"payment_id": payment_id},
UpdateExpression="SET #s = :r",
ConditionExpression="attribute_exists(payment_id)",
ExpressionAttributeNames={"#s": "status"},
ExpressionAttributeValues={":r": "REFUNDED"},
)
return {**event, "refund": {"payment_id": payment_id, "status": "REFUNDED"}}
except ClientError as e:
if e.response["Error"]["Code"] == "ConditionalCheckFailedException":
return {**event, "refund": {"payment_id": payment_id, "noop": True}}
raise
import json
def handler(event, _ctx):
detail_type = event.get("detail-type", "Unknown")
print(f"saga-terminal:{detail_type} detail={json.dumps(event.get('detail', {}))}")
return {"ok": True}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/reserve-inventory",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/reserve-inventory:*"
]
},
{
"Sid": "InventoryTable",
"Effect": "Allow",
"Action": ["dynamodb:UpdateItem", "dynamodb:GetItem", "dynamodb:PutItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/inventory"
},
{
"Sid": "IdempotencyTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/charge-payment",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/charge-payment:*"
]
},
{
"Sid": "PaymentsTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem", "dynamodb:UpdateItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/payments"
},
{
"Sid": "IdempotencyTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
},
{
"Sid": "ReadGatewaySecret",
"Effect": "Allow",
"Action": ["secretsmanager:GetSecretValue", "secretsmanager:DescribeSecret"],
"Resource": "{{SECRET_ARN}}"
},
{
"Sid": "DecryptSecretWithCmk",
"Effect": "Allow",
"Action": ["kms:Decrypt"],
"Resource": "{{KMS_ARN}}"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/create-shipment",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/create-shipment:*"
]
},
{
"Sid": "ShipmentsTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/shipments"
},
{
"Sid": "IdempotencyTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/release-inventory",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/release-inventory:*"
]
},
{
"Sid": "InventoryTable",
"Effect": "Allow",
"Action": ["dynamodb:UpdateItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/inventory"
},
{
"Sid": "IdempotencyTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/refund-payment",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/refund-payment:*"
]
},
{
"Sid": "PaymentsTable",
"Effect": "Allow",
"Action": ["dynamodb:UpdateItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/payments"
},
{
"Sid": "IdempotencyTable",
"Effect": "Allow",
"Action": ["dynamodb:PutItem", "dynamodb:GetItem"],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/saga-idempotency"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "Logs",
"Effect": "Allow",
"Action": ["logs:CreateLogGroup", "logs:CreateLogStream", "logs:PutLogEvents"],
"Resource": [
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/saga-notifier",
"arn:aws:logs:us-east-1:000000000000:log-group:/aws/lambda/saga-notifier:*"
]
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "InvokeSagaLambdas",
"Effect": "Allow",
"Action": "lambda:InvokeFunction",
"Resource": [
"arn:aws:lambda:us-east-1:000000000000:function:reserve-inventory",
"arn:aws:lambda:us-east-1:000000000000:function:charge-payment",
"arn:aws:lambda:us-east-1:000000000000:function:create-shipment",
"arn:aws:lambda:us-east-1:000000000000:function:release-inventory",
"arn:aws:lambda:us-east-1:000000000000:function:refund-payment"
]
},
{
"Sid": "PublishToSagaBus",
"Effect": "Allow",
"Action": "events:PutEvents",
"Resource": "arn:aws:events:us-east-1:000000000000:event-bus/saga-events"
}
]
}
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {"Service": "states.amazonaws.com"},
"Action": "sts:AssumeRole"
}
]
}
{
"Comment": "Order saga: reserve -> charge -> ship. On failure, compensate in reverse.",
"StartAt": "ReserveInventory",
"States": {
"ReserveInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:reserve-inventory",
"ResultPath": "$.reserve_result",
"OutputPath": "$.reserve_result",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"],
"IntervalSeconds": 1,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "PublishFailed"
}
],
"Next": "ChargePayment"
},
"ChargePayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:charge-payment",
"ResultPath": "$.charge_result",
"OutputPath": "$.charge_result",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"],
"IntervalSeconds": 1,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "ReleaseInventory"
}
],
"Next": "CreateShipment"
},
"CreateShipment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:create-shipment",
"ResultPath": "$.ship_result",
"OutputPath": "$.ship_result",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.AWSLambdaException", "Lambda.SdkClientException"],
"IntervalSeconds": 1,
"MaxAttempts": 2,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "RefundPayment"
}
],
"Next": "PublishCompleted"
},
"RefundPayment": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:refund-payment",
"ResultPath": "$.refund_result",
"OutputPath": "$.refund_result",
"Next": "ReleaseInventory"
},
"ReleaseInventory": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:000000000000:function:release-inventory",
"ResultPath": "$.release_result",
"OutputPath": "$.release_result",
"Next": "PublishFailed"
},
"PublishCompleted": {
"Type": "Task",
"Resource": "arn:aws:states:::events:putEvents",
"Parameters": {
"Entries": [
{
"EventBusName": "saga-events",
"Source": "order.saga",
"DetailType": "OrderCompleted",
"Detail": {
"order_id.$": "$.order_id",
"outcome": "completed"
}
}
]
},
"End": true
},
"PublishFailed": {
"Type": "Task",
"Resource": "arn:aws:states:::events:putEvents",
"Parameters": {
"Entries": [
{
"EventBusName": "saga-events",
"Source": "order.saga",
"DetailType": "OrderFailed",
"Detail": {
"order_id.$": "$.order_id",
"outcome": "failed-compensated"
}
}
]
},
"End": true
}
}
}
#!/usr/bin/env bash
set -euo pipefail
AWSX="aws --endpoint-url=${AWS_ENDPOINT_URL}"
REGION="us-east-1"
ACCOUNT="000000000000"
ARN_PREFIX_LAMBDA="arn:aws:lambda:${REGION}:${ACCOUNT}:function"
BUS_NAME="saga-events"
BUS_ARN="arn:aws:events:${REGION}:${ACCOUNT}:event-bus/${BUS_NAME}"
cd /app
mkdir -p build/policies build/zips
echo "== KMS key + alias =="
if ! $AWSX kms describe-key --key-id alias/saga-cmk >/dev/null 2>&1; then
KMS_KEY_ID=$($AWSX kms create-key --description "saga customer managed key" --query 'KeyMetadata.KeyId' --output text)
$AWSX kms create-alias --alias-name alias/saga-cmk --target-key-id "$KMS_KEY_ID"
else
KMS_KEY_ID=$($AWSX kms describe-key --key-id alias/saga-cmk --query 'KeyMetadata.KeyId' --output text)
fi
KMS_ARN="arn:aws:kms:${REGION}:${ACCOUNT}:key/${KMS_KEY_ID}"
echo "kms key: $KMS_ARN"
echo "== Secret =="
if ! $AWSX secretsmanager describe-secret --secret-id payment/gateway-apikey >/dev/null 2>&1; then
$AWSX secretsmanager create-secret \
--name payment/gateway-apikey \
--kms-key-id "$KMS_ARN" \
--secret-string "sk_test_FAKEGATEWAYKEY_ABC123" >/dev/null
fi
SECRET_ARN=$($AWSX secretsmanager describe-secret --secret-id payment/gateway-apikey --query 'ARN' --output text)
echo "secret: $SECRET_ARN"
echo "== DynamoDB tables =="
create_table() {
local name="$1" pk="$2"
if ! $AWSX dynamodb describe-table --table-name "$name" >/dev/null 2>&1; then
$AWSX dynamodb create-table \
--table-name "$name" \
--attribute-definitions AttributeName=${pk},AttributeType=S \
--key-schema AttributeName=${pk},KeyType=HASH \
--billing-mode PAY_PER_REQUEST >/dev/null
fi
}
create_table inventory sku
create_table payments payment_id
create_table shipments shipment_id
create_table saga-idempotency idempotency_key
$AWSX dynamodb update-time-to-live \
--table-name saga-idempotency \
--time-to-live-specification "Enabled=true, AttributeName=ttl" >/dev/null 2>&1 || true
echo "== Event bus =="
$AWSX events create-event-bus --name "$BUS_NAME" >/dev/null 2>&1 || true
echo "== Substitute policy templates =="
for f in policies/*.json; do
out="build/$(basename "$f" .json).json"
# skip trust policies
case "$(basename "$f")" in
lambda-trust.json|states-trust.json)
cp "$f" "build/$(basename "$f")"; continue;;
esac
sed -e "s|{{KMS_ARN}}|${KMS_ARN}|g" -e "s|{{SECRET_ARN}}|${SECRET_ARN}|g" "$f" > "build/policies/$(basename "$f")"
done
cp policies/lambda-trust.json build/lambda-trust.json
cp policies/states-trust.json build/states-trust.json
echo "== IAM roles =="
create_role() {
local role="$1" trust="$2" policy_doc="$3" policy_name="${role}-policy"
if ! $AWSX iam get-role --role-name "$role" >/dev/null 2>&1; then
$AWSX iam create-role --role-name "$role" --assume-role-policy-document "file://$trust" >/dev/null
fi
$AWSX iam put-role-policy --role-name "$role" --policy-name "$policy_name" --policy-document "file://$policy_doc" >/dev/null
}
create_role reserve-inventory-role build/lambda-trust.json build/policies/reserve-inventory-role.json
create_role charge-payment-role build/lambda-trust.json build/policies/charge-payment-role.json
create_role create-shipment-role build/lambda-trust.json build/policies/create-shipment-role.json
create_role release-inventory-role build/lambda-trust.json build/policies/release-inventory-role.json
create_role refund-payment-role build/lambda-trust.json build/policies/refund-payment-role.json
create_role saga-notifier-role build/lambda-trust.json build/policies/saga-notifier-role.json
create_role order-saga-role build/states-trust.json build/policies/order-saga-role.json
role_arn() { echo "arn:aws:iam::${ACCOUNT}:role/$1"; }
echo "== Package lambdas =="
zip_lambda() {
local src="$1" zipname="$2"
(cd lambdas && zip -q -j "/app/build/zips/${zipname}.zip" "${src}")
}
zip_lambda reserve_inventory.py reserve-inventory
zip_lambda charge_payment.py charge-payment
zip_lambda create_shipment.py create-shipment
zip_lambda release_inventory.py release-inventory
zip_lambda refund_payment.py refund-payment
zip_lambda saga_notifier.py saga-notifier
echo "== Create lambdas =="
upsert_lambda() {
local name="$1" handler="$2" role="$3" zip="$4"
if $AWSX lambda get-function --function-name "$name" >/dev/null 2>&1; then
$AWSX lambda update-function-code --function-name "$name" --zip-file "fileb://$zip" >/dev/null
else
$AWSX lambda create-function \
--function-name "$name" \
--runtime python3.11 \
--role "$(role_arn "$role")" \
--handler "$handler" \
--zip-file "fileb://$zip" \
--timeout 30 >/dev/null
fi
# wait until Active
for i in 1 2 3 4 5 6 7 8 9 10; do
state=$($AWSX lambda get-function --function-name "$name" --query 'Configuration.State' --output text 2>/dev/null || echo Pending)
[ "$state" = "Active" ] && break
sleep 1
done
}
upsert_lambda reserve-inventory reserve_inventory.handler reserve-inventory-role build/zips/reserve-inventory.zip
upsert_lambda charge-payment charge_payment.handler charge-payment-role build/zips/charge-payment.zip
upsert_lambda create-shipment create_shipment.handler create-shipment-role build/zips/create-shipment.zip
upsert_lambda release-inventory release_inventory.handler release-inventory-role build/zips/release-inventory.zip
upsert_lambda refund-payment refund_payment.handler refund-payment-role build/zips/refund-payment.zip
upsert_lambda saga-notifier saga_notifier.handler saga-notifier-role build/zips/saga-notifier.zip
echo "== EventBridge rule -> notifier =="
$AWSX events put-rule \
--name on-saga-terminal \
--event-bus-name "$BUS_NAME" \
--event-pattern '{"source":["order.saga"],"detail-type":["OrderCompleted","OrderFailed"]}' >/dev/null
$AWSX lambda add-permission \
--function-name saga-notifier \
--statement-id allow-eventbridge-invoke \
--action lambda:InvokeFunction \
--principal events.amazonaws.com \
--source-arn "arn:aws:events:${REGION}:${ACCOUNT}:rule/${BUS_NAME}/on-saga-terminal" >/dev/null 2>&1 || true
$AWSX events put-targets \
--rule on-saga-terminal \
--event-bus-name "$BUS_NAME" \
--targets "Id=notifier,Arn=${ARN_PREFIX_LAMBDA}:saga-notifier" >/dev/null
echo "== State machine =="
SM_NAME="order-saga"
SM_ROLE_ARN=$(role_arn order-saga-role)
SM_DEF=$(cat sm/order-saga.json)
EXISTING_SM_ARN=$($AWSX stepfunctions list-state-machines --query "stateMachines[?name=='${SM_NAME}'].stateMachineArn | [0]" --output text)
if [ "$EXISTING_SM_ARN" = "None" ] || [ -z "$EXISTING_SM_ARN" ]; then
SM_ARN=$($AWSX stepfunctions create-state-machine \
--name "$SM_NAME" \
--role-arn "$SM_ROLE_ARN" \
--definition "$SM_DEF" \
--query 'stateMachineArn' --output text)
else
SM_ARN="$EXISTING_SM_ARN"
$AWSX stepfunctions update-state-machine \
--state-machine-arn "$SM_ARN" \
--definition "$SM_DEF" \
--role-arn "$SM_ROLE_ARN" >/dev/null
fi
echo "state machine: $SM_ARN"
printf '%s\n' "$SM_ARN" > build/state_machine_arn.txt
printf '%s\n' "$KMS_ARN" > build/kms_arn.txt
printf '%s\n' "$SECRET_ARN" > build/secret_arn.txt
echo
echo "OK , provisioned"
Provision everything in localstack
chmod +x /app/setup.sh && /app/setup.sh 2>&1
Exit code 1 == KMS key + alias == kms key: arn:aws:kms:us-east-1:000000000000:key/da3b36bb-8a96-4d96-8f47-a9cf6673581a == Secret == secret: arn:aws:secretsmanager:us-east-1:000000000000:secret:payment/gateway-apikey-fbyQrQ == DynamoDB tables == == Event bus == == Substitute policy templates == == IAM roles == /app/setup.sh: line 71: role: unbound variable [error] tool reported failure
/app/setup.sh
create_role() {
local role="$1" trust="$2" policy_doc="$3" policy_name="${role}-policy"
if ! $AWSX iam get-role --role-name "$role" >/dev/null 2>&1; thencreate_role() {
local role="$1"
local trust="$2"
local policy_doc="$3"
local policy_name="${role}-policy"
if ! $AWSX iam get-role --role-name "$role" >/dev/null 2>&1; thenRe-run setup
/app/setup.sh 2>&1
== KMS key + alias == kms key: arn:aws:kms:us-east-1:000000000000:key/da3b36bb-8a96-4d96-8f47-a9cf6673581a == Secret == secret: arn:aws:secretsmanager:us-east-1:000000000000:secret:payment/gateway-apikey-fbyQrQ == DynamoDB tables == == Event bus == == Substitute policy templates == == IAM roles == == Package lambdas == == Create lambdas == == EventBridge rule -> notifier == == State machine == state machine: arn:aws:states:us-east-1:000000000000:stateMachine:order-saga OK , provisioned [stdout] == KMS key + alias == kms key: arn:aws:kms:us-east-1:000000000000:key/da3b36bb-8a96-4d96-8f47-a9cf6673581a == Secret == secret: arn:aws:secretsmanager:us-east-1:000000000000:secret:payment/gateway-apikey-fbyQrQ == DynamoDB tables == == Event bus == == Substitute policy templates == == IAM roles == == Package lambdas == == Create lambdas == == EventBridge rule -> notifier == == State machine == state machine: arn:aws:states:us-east-1:000000000000:stateMachine:order-saga OK , provisioned
Run happy path execution
AWSX="aws --endpoint-url=$AWS_ENDPOINT_URL"
SM_ARN=$(cat /app/build/state_machine_arn.txt)
EXEC1=$($AWSX stepfunctions start-execution \
--state-machine-arn "$SM_ARN" \
--name "happy-$(date +%s)" \
--input '{"order_id":"o-1","sku":"x","qty":2,"amount":100,"idempotency_key":"k-1"}' \
--query 'executionArn' --output text)
echo "exec: $EXEC1"
for i in $(seq 1 30); do
STATUS=$($AWSX stepfunctions describe-execution --execution-arn "$EXEC1" --query 'status' --output text)
[ "$STATUS" != "RUNNING" ] && break
sleep 1
done
echo "status: $STATUS"
$AWSX stepfunctions describe-execution --execution-arn "$EXEC1" --query '{status: status, output: output, error: error, cause: cause}'exec: arn:aws:states:us-east-1:000000000000:execution:order-saga:happy-1776918670
status: SUCCEEDED
{
"status": "SUCCEEDED",
"output": "{\"FailedEntryCount\":0,\"Entries\":[{\"EventId\":\"8929e6d7-81eb-41f8-96a8-0973fff4e9bc\"}]}",
"error": null,
"cause": null
}
[stdout]
exec: arn:aws:states:us-east-1:000000000000:execution:order-saga:happy-1776918670
status: SUCCEEDED
{
"status": "SUCCEEDED",
"output": "{\"FailedEntryCount\":0,\"Entries\":[{\"EventId\":\"8929e6d7-81eb-41f8-96a8-0973fff4e9bc\"}]}",
"error": null,
"cause": null
}Check happy path side-effects
AWSX="aws --endpoint-url=$AWS_ENDPOINT_URL"
echo "--- inventory[x] ---"
$AWSX dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}'
echo "--- payments[pay-o-1] ---"
$AWSX dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-1"}}'
echo "--- shipments[ship-o-1] ---"
$AWSX dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-1"}}'
echo "--- notifier logs ---"
LG="/aws/lambda/saga-notifier"
STREAMS=$($AWSX logs describe-log-streams --log-group-name "$LG" --order-by LastEventTime --descending --max-items 3 --query 'logStreams[].logStreamName' --output text 2>/dev/null || true)
for s in $STREAMS; do
$AWSX logs get-log-events --log-group-name "$LG" --log-stream-name "$s" --query 'events[].message' --output text 2>/dev/null | tr '\t' '\n'
done | grep -a "saga-terminal:" || echo "NO saga-terminal log found"--- inventory[x] ---
{
"Item": {
"sku": {
"S": "x"
},
"reserved_qty": {
"N": "2"
}
}
}
--- payments[pay-o-1] ---
{
"Item": {
"amount": {
"N": "100"
},
"order_id": {
"S": "o-1"
},
"payment_id": {
"S": "pay-o-1"
},
"status": {
"S": "CAPTURED"
}
}
}
--- shipments[ship-o-1] ---
{
"Item": {
"shipment_id": {
"S": "ship-o-1"
},
"sku": {
"S": "x"
},
"order_id": {
"S": "o-1"
},
"qty": {
"N": "2"
},
"status": {
"S": "CREATED"
}
}
}
--- notifier logs ---
saga-terminal:OrderCompleted detail={"order_id": "o-1", "outcome": "completed"}
[stdout]
--- inventory[x] ---
{
"Item": {
"sku": {
"S": "x"
},
"reserved_qty": {
"N": "2"
}
}
}
--- payments[pay-o-1] ---
{
"Item": {
"amount": {
"N": "100"
},
"order_id": {
"S": "o-1"
},
"payment_id": {
"S": "pay-o-1"
},
"status": {
"S": "CAPTURED"
}
}
}
--- shipments[ship-o-1] ---
{
"Item": {
"shipment_id": {
"S": "ship-o-1"
},
"sku": {
"S": "x"
},
"order_id": {
"S": "o-1"
},
"qty": {
"N": "2"
},
"status": {
"S": "CREATED"
}
}
}
--- notifier logs ---
saga-terminal:OrderCompleted detail={"order_id": "o-1", "outcome": "completed"}Run failure path execution
AWSX="aws --endpoint-url=$AWS_ENDPOINT_URL"
SM_ARN=$(cat /app/build/state_machine_arn.txt)
EXEC2=$($AWSX stepfunctions start-execution \
--state-machine-arn "$SM_ARN" \
--name "fail-$(date +%s)" \
--input '{"order_id":"o-2","sku":"x","qty":2,"amount":100,"idempotency_key":"k-2","force_failure":true}' \
--query 'executionArn' --output text)
echo "exec: $EXEC2"
for i in $(seq 1 60); do
STATUS=$($AWSX stepfunctions describe-execution --execution-arn "$EXEC2" --query 'status' --output text)
[ "$STATUS" != "RUNNING" ] && break
sleep 1
done
echo "status: $STATUS"
$AWSX stepfunctions get-execution-history --execution-arn "$EXEC2" --query 'events[].{t:type, n:stateEnteredEventDetails.name}' --output table 2>/dev/null | head -60exec: arn:aws:states:us-east-1:000000000000:execution:order-saga:fail-1776918692 status: SUCCEEDED ------------------------------------------------- | GetExecutionHistory | +-------------------+---------------------------+ | n | t | +-------------------+---------------------------+ | None | ExecutionStarted | | ReserveInventory | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionSucceeded | | None | TaskStateExited | | ChargePayment | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionFailed | | None | TaskStateExited | | ReleaseInventory | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionSucceeded | | None | TaskStateExited | | PublishFailed | TaskStateEntered | | None | TaskScheduled | | None | TaskStarted | | None | TaskSucceeded | | None | TaskStateExited | | None | ExecutionSucceeded | +-------------------+---------------------------+ [stdout] exec: arn:aws:states:us-east-1:000000000000:execution:order-saga:fail-1776918692 status: SUCCEEDED ------------------------------------------------- | GetExecutionHistory | +-------------------+---------------------------+ | n | t | +-------------------+---------------------------+ | None | ExecutionStarted | | ReserveInventory | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionSucceeded | | None | TaskStateExited | | ChargePayment | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionFailed | | None | TaskStateExited | | ReleaseInventory | TaskStateEntered | | None | LambdaFunctionScheduled | | None | LambdaFunctionStarted | | None | LambdaFunctionSucceeded | | None | TaskStateExited | | PublishFailed | TaskStateEntered | | None | TaskScheduled | | None | TaskStarted | | None | TaskSucceeded | | None | TaskStateExited | | None | ExecutionSucceeded | +-------------------+---------------------------+
Verify failure path end state
AWSX="aws --endpoint-url=$AWS_ENDPOINT_URL"
echo "--- inventory[x] ---"
$AWSX dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}'
echo "--- payments[pay-o-2] ---"
$AWSX dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-2"}}'
echo "(empty = absent, expected)"
echo "--- shipments[ship-o-2] ---"
$AWSX dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-2"}}'
echo "(empty = absent, expected)"
echo "--- notifier latest log ---"
LG="/aws/lambda/saga-notifier"
STREAMS=$($AWSX logs describe-log-streams --log-group-name "$LG" --order-by LastEventTime --descending --max-items 5 --query 'logStreams[].logStreamName' --output text 2>/dev/null || true)
for s in $STREAMS; do
$AWSX logs get-log-events --log-group-name "$LG" --log-stream-name "$s" --query 'events[].message' --output text 2>/dev/null | tr '\t' '\n'
done | grep -a "saga-terminal:"--- inventory[x] ---
{
"Item": {
"sku": {
"S": "x"
},
"reserved_qty": {
"N": "2"
}
}
}
--- payments[pay-o-2] ---
(empty = absent, expected)
--- shipments[ship-o-2] ---
(empty = absent, expected)
--- notifier latest log ---
saga-terminal:OrderFailed detail={"order_id": "o-2", "outcome": "failed-compensated"}
saga-terminal:OrderCompleted detail={"order_id": "o-1", "outcome": "completed"}
[stdout]
--- inventory[x] ---
{
"Item": {
"sku": {
"S": "x"
},
"reserved_qty": {
"N": "2"
}
}
}
--- payments[pay-o-2] ---
(empty = absent, expected)
--- shipments[ship-o-2] ---
(empty = absent, expected)
--- notifier latest log ---
saga-terminal:OrderFailed detail={"order_id": "o-2", "outcome": "failed-compensated"}
saga-terminal:OrderCompleted detail={"order_id": "o-1", "outcome": "completed"}#!/usr/bin/env bash
set -euo pipefail
AWSX="aws --endpoint-url=${AWS_ENDPOINT_URL}"
SM_ARN=$(cat /app/build/state_machine_arn.txt)
pass() { echo "PASS: $1"; }
fail() { echo "FAIL: $1"; exit 1; }
reset_tables() {
for t in inventory payments shipments saga-idempotency; do
pk=$($AWSX dynamodb describe-table --table-name "$t" --query 'Table.KeySchema[0].AttributeName' --output text)
items=$($AWSX dynamodb scan --table-name "$t" --projection-expression "#k" \
--expression-attribute-names "{\"#k\":\"$pk\"}" --query 'Items[].'"$pk"'.S' --output text 2>/dev/null || true)
for v in $items; do
$AWSX dynamodb delete-item --table-name "$t" --key "{\"$pk\":{\"S\":\"$v\"}}" >/dev/null
done
done
}
run_sm() {
local name="$1" input="$2"
local arn
arn=$($AWSX stepfunctions start-execution --state-machine-arn "$SM_ARN" --name "$name" --input "$input" --query 'executionArn' --output text)
for _ in $(seq 1 60); do
st=$($AWSX stepfunctions describe-execution --execution-arn "$arn" --query 'status' --output text)
[ "$st" != "RUNNING" ] && break
sleep 1
done
echo "$st"
}
notifier_has() {
local needle="$1"
local lg="/aws/lambda/saga-notifier"
local streams
streams=$($AWSX logs describe-log-streams --log-group-name "$lg" --order-by LastEventTime --descending --max-items 10 --query 'logStreams[].logStreamName' --output text 2>/dev/null || true)
for s in $streams; do
if $AWSX logs get-log-events --log-group-name "$lg" --log-stream-name "$s" --query 'events[].message' --output text 2>/dev/null | tr '\t' '\n' | grep -qa -- "$needle"; then
return 0
fi
done
return 1
}
echo "== Policy docs: no wildcards on KMS/Lambda invoke =="
# charge-payment kms:Decrypt must not be on "*"
if jq -e '.Statement[] | select(.Action | tostring | contains("kms:")) | select(.Resource=="*")' \
/app/build/policies/charge-payment-role.json >/dev/null 2>&1; then
fail "charge-payment kms has wildcard resource"
fi
pass "charge-payment kms resource is not *"
if jq -e '.Statement[] | select(.Action=="lambda:InvokeFunction") | select(.Resource=="*")' \
/app/build/policies/order-saga-role.json >/dev/null 2>&1; then
fail "saga role has wildcard on lambda invoke"
fi
pass "saga role lambda:InvokeFunction is per-function, not *"
# Only payment role has secrets read
for p in reserve-inventory release-inventory create-shipment refund-payment saga-notifier; do
if grep -q 'secretsmanager:GetSecretValue' /app/build/policies/${p}-role.json; then
fail "$p role has secret read permission"
fi
done
pass "only charge-payment role reads the secret"
echo
echo "== Happy path =="
reset_tables
st=$(run_sm "v-happy-$(date +%s)" '{"order_id":"o-1","sku":"x","qty":2,"amount":100,"idempotency_key":"k-1"}')
[ "$st" = "SUCCEEDED" ] || fail "happy path status=$st"
pass "state machine SUCCEEDED"
rq=$($AWSX dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}' --query 'Item.reserved_qty.N' --output text)
[ "$rq" = "2" ] || fail "expected reserved_qty=2, got $rq"
pass "inventory reserved_qty=2"
ps=$($AWSX dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-1"}}' --query 'Item.status.S' --output text)
[ "$ps" = "CAPTURED" ] || fail "expected payment CAPTURED, got $ps"
pass "payment row CAPTURED"
sid=$($AWSX dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-1"}}' --query 'Item.shipment_id.S' --output text)
[ "$sid" = "ship-o-1" ] || fail "shipment row missing"
pass "shipment row exists"
sleep 2
notifier_has "saga-terminal:OrderCompleted" || fail "no saga-terminal:OrderCompleted log"
pass "notifier logged saga-terminal:OrderCompleted"
echo
echo "== Failure path =="
reset_tables
st=$(run_sm "v-fail-$(date +%s)" '{"order_id":"o-2","sku":"x","qty":2,"amount":100,"idempotency_key":"k-2","force_failure":true}')
[ "$st" = "SUCCEEDED" ] || fail "failure path status=$st"
pass "state machine SUCCEEDED (saga handled gracefully)"
rq=$($AWSX dynamodb get-item --table-name inventory --key '{"sku":{"S":"x"}}' --query 'Item.reserved_qty.N' --output text)
[ "$rq" = "0" ] || fail "expected reserved_qty=0 after release, got $rq"
pass "inventory reserved_qty=0 (release ran)"
pexists=$($AWSX dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-2"}}' --query 'Item.payment_id.S' --output text 2>/dev/null)
if [ -n "$pexists" ] && [ "$pexists" != "None" ]; then
pstatus=$($AWSX dynamodb get-item --table-name payments --key '{"payment_id":{"S":"pay-o-2"}}' --query 'Item.status.S' --output text)
[ "$pstatus" = "REFUNDED" ] || fail "payment row present with status=$pstatus (want absent or REFUNDED)"
pass "payment row REFUNDED"
else
pass "payment row absent (nothing to refund)"
fi
sexists=$($AWSX dynamodb get-item --table-name shipments --key '{"shipment_id":{"S":"ship-o-2"}}' --query 'Item.shipment_id.S' --output text 2>/dev/null)
if [ -n "$sexists" ] && [ "$sexists" != "None" ]; then
fail "shipment row should not exist"
fi
pass "shipment row absent"
sleep 2
notifier_has "saga-terminal:OrderFailed" || fail "no saga-terminal:OrderFailed log"
pass "notifier logged saga-terminal:OrderFailed"
echo
echo "ALL CHECKS PASSED"
Run end-to-end verifier
chmod +x /app/verify.sh && /app/verify.sh 2>&1
== Policy docs: no wildcards on KMS/Lambda invoke == PASS: charge-payment kms resource is not * PASS: saga role lambda:InvokeFunction is per-function, not * PASS: only charge-payment role reads the secret == Happy path == PASS: state machine SUCCEEDED PASS: inventory reserved_qty=2 PASS: payment row CAPTURED PASS: shipment row exists PASS: notifier logged saga-terminal:OrderCompleted == Failure path == PASS: state machine SUCCEEDED (saga handled gracefully) PASS: inventory reserved_qty=0 (release ran) PASS: payment row absent (nothing to refund) PASS: shipment row absent PASS: notifier logged saga-terminal:OrderFailed ALL CHECKS PASSED [stdout] == Policy docs: no wildcards on KMS/Lambda invoke == PASS: charge-payment kms resource is not * PASS: saga role lambda:InvokeFunction is per-function, not * PASS: only charge-payment role reads the secret == Happy path == PASS: state machine SUCCEEDED PASS: inventory reserved_qty=2 PASS: payment row CAPTURED PASS: shipment row exists PASS: notifier logged saga-terminal:OrderCompleted == Failure path == PASS: state machine SUCCEEDED (saga handled gracefully) PASS: inventory reserved_qty=0 (release ran) PASS: payment row absent (nothing to refund) PASS: shipment row absent PASS: notifier logged saga-terminal:OrderFailed ALL CHECKS PASSED
No file changes, the agent left the workspace untouched.
============================= test session starts ==============================
platform linux -- Python 3.12.3, pytest-8.4.1, pluggy-1.6.0 -- /root/.cache/uv/archive-v0/Ru4UoKprAMY8zl44qZ4tw/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 20 items
../tests/test_outputs.py::test_01_cmk_alias_and_secret_exist PASSED [ 5%]
../tests/test_outputs.py::test_02_all_four_tables_exist PASSED [ 10%]
../tests/test_outputs.py::test_03_all_six_lambdas_exist PASSED [ 15%]
../tests/test_outputs.py::test_04_state_machine_and_bus_exist PASSED [ 20%]
../tests/test_outputs.py::test_05_terminal_rule_exists_on_saga_bus PASSED [ 25%]
../tests/test_outputs.py::test_06_every_forward_state_has_catch_to_compensation PASSED [ 30%]
../tests/test_outputs.py::test_07_compensations_run_in_lifo_order PASSED [ 35%]
../tests/test_outputs.py::test_08_compensation_states_preserve_error_via_result_path PASSED [ 40%]
../tests/test_outputs.py::test_09_idempotency_table_has_ttl_and_correct_pk PASSED [ 45%]
../tests/test_outputs.py::test_10_each_lambda_role_scoped_to_its_own_table PASSED [ 50%]
../tests/test_outputs.py::test_11_only_charge_role_reads_the_payment_secret PASSED [ 55%]
../tests/test_outputs.py::test_12_saga_role_lambda_invoke_not_wildcarded PASSED [ 60%]
../tests/test_outputs.py::test_13_rule_pattern_matches_both_terminal_types_with_array_wrap PASSED [ 65%]
../tests/test_outputs.py::test_14_charge_role_kms_decrypt_scoped_not_wildcard PASSED [ 70%]
../tests/test_outputs.py::test_15_rule_targets_notifier_lambda PASSED [ 75%]
../tests/test_outputs.py::test_16_notifier_role_is_least_privilege_no_ddb_kms_secrets PASSED [ 80%]
../tests/test_outputs.py::test_17_notifier_lambda_resource_policy_allows_events_with_source_arn PASSED [ 85%]
../tests/test_outputs.py::test_18_happy_path_completes_and_writes_all_three_tables PASSED [ 90%]
../tests/test_outputs.py::test_19_failure_path_triggers_compensation_and_rolls_back_inventory PASSED [ 95%]
../tests/test_outputs.py::test_20_idempotency_prevents_double_reservation PASSED [100%]
=============================== warnings summary ===============================
test_outputs.py: 137 warnings
/root/.cache/uv/archive-v0/Ru4UoKprAMY8zl44qZ4tw/lib/python3.12/site-packages/botocore/auth.py:424: DeprecationWarning: datetime.datetime.utcnow() is deprecated and scheduled for removal in a future version. Use timezone-aware objects to represent datetimes in UTC: datetime.datetime.now(datetime.UTC).
datetime_now = datetime.datetime.utcnow()
-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
=========================== short test summary info ============================
PASSED ../tests/test_outputs.py::test_01_cmk_alias_and_secret_exist
PASSED ../tests/test_outputs.py::test_02_all_four_tables_exist
PASSED ../tests/test_outputs.py::test_03_all_six_lambdas_exist
PASSED ../tests/test_outputs.py::test_04_state_machine_and_bus_exist
PASSED ../tests/test_outputs.py::test_05_terminal_rule_exists_on_saga_bus
PASSED ../tests/test_outputs.py::test_06_every_forward_state_has_catch_to_compensation
PASSED ../tests/test_outputs.py::test_07_compensations_run_in_lifo_order
PASSED ../tests/test_outputs.py::test_08_compensation_states_preserve_error_via_result_path
PASSED ../tests/test_outputs.py::test_09_idempotency_table_has_ttl_and_correct_pk
PASSED ../tests/test_outputs.py::test_10_each_lambda_role_scoped_to_its_own_table
PASSED ../tests/test_outputs.py::test_11_only_charge_role_reads_the_payment_secret
PASSED ../tests/test_outputs.py::test_12_saga_role_lambda_invoke_not_wildcarded
PASSED ../tests/test_outputs.py::test_13_rule_pattern_matches_both_terminal_types_with_array_wrap
PASSED ../tests/test_outputs.py::test_14_charge_role_kms_decrypt_scoped_not_wildcard
PASSED ../tests/test_outputs.py::test_15_rule_targets_notifier_lambda
PASSED ../tests/test_outputs.py::test_16_notifier_role_is_least_privilege_no_ddb_kms_secrets
PASSED ../tests/test_outputs.py::test_17_notifier_lambda_resource_policy_allows_events_with_source_arn
PASSED ../tests/test_outputs.py::test_18_happy_path_completes_and_writes_all_three_tables
PASSED ../tests/test_outputs.py::test_19_failure_path_triggers_compensation_and_rolls_back_inventory
PASSED ../tests/test_outputs.py::test_20_idempotency_prevents_double_reservation
====================== 20 passed, 137 warnings in 23.94s =======================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_183215d893744136. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_183215d893744136 · verifier authoritative; classifier explanatory.