tests/test.sh exit code (0 → resolved); the classification below is post-hoc and cannot change it.Check LocalStack health
curl -s http://localstack:4566/_localstack/health | head -100
{"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": "disabled", "ses": "disabled", "sns": "disabled", "sqs": "available", "ssm": "disabled", "stepfunctions": "disabled", "sts": "available", "support": "disabled", "swf": "disabled", "transcribe": "disabled"}, "edition": "community", "version": "4.13.1"}
[stdout]
{"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": "disabled", "ses": "disabled", "sns": "disabled", "sqs": "available", "ssm": "disabled", "stepfunctions": "disabled", "sts": "available", "support": "disabled", "swf": "disabled", "transcribe": "disabled"}, "edition": "community", "version": "4.13.1"}Verify tooling
aws --version && python3 --version && echo "region=$AWS_DEFAULT_REGION key=$AWS_ACCESS_KEY_ID"
aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 region=us-east-1 key=test [stdout] aws-cli/2.34.33 Python/3.14.4 Linux/6.10.14-linuxkit exe/aarch64.ubuntu.24 Python 3.12.3 region=us-east-1 key=test
List working directory
ls /app && pwd
/app [stdout] /app
Check sts identity
export AWS_ENDPOINT_URL=http://localstack:4566 aws sts get-caller-identity
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}
[stdout]
{
"UserId": "AKIAIOSFODNN7EXAMPLE",
"Account": "000000000000",
"Arn": "arn:aws:iam::000000000000:root"
}Write env helper
cat > /app/env.sh <<'EOF' export AWS_ENDPOINT_URL=http://localstack:4566 export AWS_DEFAULT_REGION=us-east-1 export ACCOUNT_ID=000000000000 export REGION=us-east-1 EOF echo "env written"
env written [stdout] env written
{
"Version": "2012-10-17",
"Id": "source-bucket-cmk",
"Statement": [
{
"Sid": "EnableRootAccountAdmin",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:root"},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "AllowS3ServiceUse",
"Effect": "Allow",
"Principal": {"Service": "s3.amazonaws.com"},
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "*"
},
{
"Sid": "AllowLambdaRoleDecrypt",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:role/image-ingest-lambda-role"},
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
{
"Version": "2012-10-17",
"Id": "sqs-queue-cmk",
"Statement": [
{
"Sid": "EnableRootAccountAdmin",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:root"},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "AllowS3ToPublishEncryptedSQS",
"Effect": "Allow",
"Principal": {"Service": "s3.amazonaws.com"},
"Action": [
"kms:GenerateDataKey",
"kms:Decrypt"
],
"Resource": "*"
},
{
"Sid": "AllowLambdaRoleConsumeEncryptedSQS",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:role/image-ingest-lambda-role"},
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
{
"Version": "2012-10-17",
"Id": "dest-bucket-cmk",
"Statement": [
{
"Sid": "EnableRootAccountAdmin",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:root"},
"Action": [
"kms:Create*",
"kms:Describe*",
"kms:Enable*",
"kms:List*",
"kms:Put*",
"kms:Update*",
"kms:Revoke*",
"kms:Disable*",
"kms:Get*",
"kms:Delete*",
"kms:TagResource",
"kms:UntagResource",
"kms:ScheduleKeyDeletion",
"kms:CancelKeyDeletion"
],
"Resource": "*"
},
{
"Sid": "AllowLambdaRoleEncryptForDest",
"Effect": "Allow",
"Principal": {"AWS": "arn:aws:iam::000000000000:role/image-ingest-lambda-role"},
"Action": [
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "*"
}
]
}
{
"query": "select:TodoWrite",
"max_results": 1
}
, output,
{"type": "tool_reference", "tool_name": "TodoWrite"}
Create KMS CMKs with aliases
source /app/env.sh set -e # Create 3 CMKs with policies, then aliases SRC_KEY_ID=$(aws kms create-key --description "source bucket CMK" --policy file:///app/kms-policy-source.json --query 'KeyMetadata.KeyId' --output text) SQS_KEY_ID=$(aws kms create-key --description "sqs queue CMK" --policy file:///app/kms-policy-sqs.json --query 'KeyMetadata.KeyId' --output text) DST_KEY_ID=$(aws kms create-key --description "dest bucket CMK" --policy file:///app/kms-policy-dest.json --query 'KeyMetadata.KeyId' --output text) aws kms create-alias --alias-name alias/source-bucket-cmk --target-key-id $SRC_KEY_ID aws kms create-alias --alias-name alias/sqs-queue-cmk --target-key-id $SQS_KEY_ID aws kms create-alias --alias-name alias/dest-bucket-cmk --target-key-id $DST_KEY_ID echo "SRC_KEY_ID=$SRC_KEY_ID" echo "SQS_KEY_ID=$SQS_KEY_ID" echo "DST_KEY_ID=$DST_KEY_ID" cat >> /app/env.sh <<EOF export SRC_KEY_ID=$SRC_KEY_ID export SQS_KEY_ID=$SQS_KEY_ID export DST_KEY_ID=$DST_KEY_ID export SRC_KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/$SRC_KEY_ID export SQS_KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/$SQS_KEY_ID export DST_KEY_ARN=arn:aws:kms:us-east-1:000000000000:key/$DST_KEY_ID EOF echo "env updated"
SRC_KEY_ID=161a7473-da40-49c6-a77d-ec49ee449b05 SQS_KEY_ID=4988fc63-5443-49ac-ba64-3c704e8759e2 DST_KEY_ID=f690d658-d425-42db-9241-8dc52bb2e812 env updated [stdout] SRC_KEY_ID=161a7473-da40-49c6-a77d-ec49ee449b05 SQS_KEY_ID=4988fc63-5443-49ac-ba64-3c704e8759e2 DST_KEY_ID=f690d658-d425-42db-9241-8dc52bb2e812 env updated
{
"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/image-ingest-consumer:*"
},
{
"Sid": "ReadSourceBucket",
"Effect": "Allow",
"Action": [
"s3:GetObject"
],
"Resource": "arn:aws:s3:::ingest-source-bucket/*"
},
{
"Sid": "WriteDestBucket",
"Effect": "Allow",
"Action": [
"s3:PutObject"
],
"Resource": "arn:aws:s3:::ingest-dest-bucket/*"
},
{
"Sid": "ConsumeMainQueue",
"Effect": "Allow",
"Action": [
"sqs:ReceiveMessage",
"sqs:DeleteMessage",
"sqs:GetQueueAttributes",
"sqs:ChangeMessageVisibility"
],
"Resource": "arn:aws:sqs:us-east-1:000000000000:ingest-events"
},
{
"Sid": "WriteDdb",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:000000000000:table/image-ingest-records"
},
{
"Sid": "DecryptSourceBucket",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:DescribeKey"
],
"Resource": "SRC_KEY_ARN_PLACEHOLDER"
},
{
"Sid": "DecryptSqsMessages",
"Effect": "Allow",
"Action": [
"kms:Decrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "SQS_KEY_ARN_PLACEHOLDER"
},
{
"Sid": "EncryptDestBucket",
"Effect": "Allow",
"Action": [
"kms:Encrypt",
"kms:GenerateDataKey",
"kms:DescribeKey"
],
"Resource": "DST_KEY_ARN_PLACEHOLDER"
}
]
}
Create IAM role and attach least-priv policy
source /app/env.sh
set -e
# Materialize policy with key ARNs
sed -e "s|SRC_KEY_ARN_PLACEHOLDER|$SRC_KEY_ARN|" \
-e "s|SQS_KEY_ARN_PLACEHOLDER|$SQS_KEY_ARN|" \
-e "s|DST_KEY_ARN_PLACEHOLDER|$DST_KEY_ARN|" \
/app/lambda-policy.json.tpl > /app/lambda-policy.json
python3 -c 'import json,sys; json.load(open("/app/lambda-policy.json"))' && echo "policy JSON ok"
aws iam create-role \
--role-name image-ingest-lambda-role \
--assume-role-policy-document file:///app/lambda-trust.json \
--query 'Role.Arn' --output text
aws iam put-role-policy \
--role-name image-ingest-lambda-role \
--policy-name image-ingest-inline \
--policy-document file:///app/lambda-policy.json
echo "ROLE_ARN=arn:aws:iam::000000000000:role/image-ingest-lambda-role"
cat >> /app/env.sh <<'EOF'
export LAMBDA_ROLE_ARN=arn:aws:iam::000000000000:role/image-ingest-lambda-role
EOFpolicy JSON ok arn:aws:iam::000000000000:role/image-ingest-lambda-role ROLE_ARN=arn:aws:iam::000000000000:role/image-ingest-lambda-role [stdout] policy JSON ok arn:aws:iam::000000000000:role/image-ingest-lambda-role ROLE_ARN=arn:aws:iam::000000000000:role/image-ingest-lambda-role
Create DynamoDB table
source /app/env.sh set -e aws dynamodb create-table \ --table-name image-ingest-records \ --attribute-definitions AttributeName=object_key,AttributeType=S \ --key-schema AttributeName=object_key,KeyType=HASH \ --billing-mode PAY_PER_REQUEST \ --query 'TableDescription.TableArn' --output text # wait until ACTIVE for i in 1 2 3 4 5; do st=$(aws dynamodb describe-table --table-name image-ingest-records --query 'Table.TableStatus' --output text) echo "ddb status: $st" [ "$st" = "ACTIVE" ] && break sleep 1 done
arn:aws:dynamodb:us-east-1:000000000000:table/image-ingest-records ddb status: ACTIVE [stdout] arn:aws:dynamodb:us-east-1:000000000000:table/image-ingest-records ddb status: ACTIVE
Create DLQ and main SQS queue with SSE-KMS and redrive
source /app/env.sh
set -e
# DLQ first (need its ARN for main queue redrive)
DLQ_URL=$(aws sqs create-queue \
--queue-name ingest-dlq \
--attributes "{\"KmsMasterKeyId\":\"$SQS_KEY_ARN\",\"MessageRetentionPeriod\":\"1209600\"}" \
--query 'QueueUrl' --output text)
echo "DLQ_URL=$DLQ_URL"
DLQ_ARN=$(aws sqs get-queue-attributes --queue-url "$DLQ_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo "DLQ_ARN=$DLQ_ARN"
# Main queue: SSE-KMS, visibility 90s (> lambda timeout 30s), redrive to DLQ
REDRIVE=$(python3 -c "import json,sys; print(json.dumps({'deadLetterTargetArn':'$DLQ_ARN','maxReceiveCount':5}))")
MAIN_URL=$(aws sqs create-queue \
--queue-name ingest-events \
--attributes "{\"KmsMasterKeyId\":\"$SQS_KEY_ARN\",\"VisibilityTimeout\":\"90\",\"MessageRetentionPeriod\":\"345600\",\"RedrivePolicy\":$(echo "$REDRIVE" | python3 -c 'import sys,json; print(json.dumps(sys.stdin.read().strip()))')}" \
--query 'QueueUrl' --output text)
echo "MAIN_URL=$MAIN_URL"
MAIN_ARN=$(aws sqs get-queue-attributes --queue-url "$MAIN_URL" --attribute-names QueueArn --query 'Attributes.QueueArn' --output text)
echo "MAIN_ARN=$MAIN_ARN"
cat >> /app/env.sh <<EOF
export DLQ_URL=$DLQ_URL
export DLQ_ARN=$DLQ_ARN
export MAIN_URL=$MAIN_URL
export MAIN_ARN=$MAIN_ARN
EOF
# Verify redrive and visibility
aws sqs get-queue-attributes --queue-url "$MAIN_URL" --attribute-names All --query 'Attributes' --output jsonDLQ_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/ingest-dlq
DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:ingest-dlq
MAIN_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/ingest-events
MAIN_ARN=arn:aws:sqs:us-east-1:000000000000:ingest-events
{
"CreatedTimestamp": "1776851521",
"DelaySeconds": "0",
"LastModifiedTimestamp": "1776851521",
"MaximumMessageSize": "1048576",
"MessageRetentionPeriod": "345600",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"ReceiveMessageWaitTimeSeconds": "0",
"VisibilityTimeout": "90",
"SqsManagedSseEnabled": "true",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2",
"RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:000000000000:ingest-dlq\", \"maxReceiveCount\": 5}",
"ApproximateNumberOfMessages": "0",
"ApproximateNumberOfMessagesDelayed": "0",
"ApproximateNumberOfMessagesNotVisible": "0"
}
[stdout]
DLQ_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/ingest-dlq
DLQ_ARN=arn:aws:sqs:us-east-1:000000000000:ingest-dlq
MAIN_URL=http://sqs.us-east-1.localhost.localstack.cloud:4566/000000000000/ingest-events
MAIN_ARN=arn:aws:sqs:us-east-1:000000000000:ingest-events
{
"CreatedTimestamp": "1776851521",
"DelaySeconds": "0",
"LastModifiedTimestamp": "1776851521",
"MaximumMessageSize": "1048576",
"MessageRetentionPeriod": "345600",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"ReceiveMessageWaitTimeSeconds": "0",
"VisibilityTimeout": "90",
"SqsManagedSseEnabled": "true",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2",
"RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:000000000000:ingest-dlq\", \"maxReceiveCount\": 5}",
"ApproximateNumberOfMessages": "0",
"ApproximateNumberOfMessagesDelayed": "0",
"ApproximateNumberOfMessagesNotVisible": "0"
}{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSourceBucketToSend",
"Effect": "Allow",
"Principal": {"Service": "s3.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Condition": {
"ArnLike": {"aws:SourceArn": "arn:aws:s3:::ingest-source-bucket"},
"StringEquals": {"aws:SourceAccount": "000000000000"}
}
}
]
}
Attach queue policy allowing S3 to SendMessage
source /app/env.sh
set -e
POLICY_JSON=$(python3 -c 'import json; print(json.dumps(open("/app/main-queue-policy.json").read()))')
aws sqs set-queue-attributes \
--queue-url "$MAIN_URL" \
--attributes "{\"Policy\":$POLICY_JSON}"
aws sqs get-queue-attributes --queue-url "$MAIN_URL" --attribute-names Policy --query 'Attributes.Policy' --output text{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSourceBucketToSend",
"Effect": "Allow",
"Principal": {"Service": "s3.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Condition": {
"ArnLike": {"aws:SourceArn": "arn:aws:s3:::ingest-source-bucket"},
"StringEquals": {"aws:SourceAccount": "000000000000"}
}
}
]
}
[stdout]
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowSourceBucketToSend",
"Effect": "Allow",
"Principal": {"Service": "s3.amazonaws.com"},
"Action": "sqs:SendMessage",
"Resource": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Condition": {
"ArnLike": {"aws:SourceArn": "arn:aws:s3:::ingest-source-bucket"},
"StringEquals": {"aws:SourceAccount": "000000000000"}
}
}
]
}Create S3 buckets with SSE-KMS defaults
source /app/env.sh
set -e
aws s3api create-bucket --bucket ingest-source-bucket >/dev/null
aws s3api create-bucket --bucket ingest-dest-bucket >/dev/null
# SSE-KMS default encryption with CMK
aws s3api put-bucket-encryption --bucket ingest-source-bucket \
--server-side-encryption-configuration "{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"$SRC_KEY_ARN\"}}]}"
aws s3api put-bucket-encryption --bucket ingest-dest-bucket \
--server-side-encryption-configuration "{\"Rules\":[{\"ApplyServerSideEncryptionByDefault\":{\"SSEAlgorithm\":\"aws:kms\",\"KMSMasterKeyID\":\"$DST_KEY_ARN\"}}]}"
aws s3api get-bucket-encryption --bucket ingest-source-bucket --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault' --output json
aws s3api get-bucket-encryption --bucket ingest-dest-bucket --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault' --output json{
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/161a7473-da40-49c6-a77d-ec49ee449b05"
}
{
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/f690d658-d425-42db-9241-8dc52bb2e812"
}
[stdout]
{
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/161a7473-da40-49c6-a77d-ec49ee449b05"
}
{
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/f690d658-d425-42db-9241-8dc52bb2e812"
}Configure S3 event notification to SQS
source /app/env.sh
set -e
cat > /app/notify.json <<EOF
{
"QueueConfigurations": [
{
"Id": "source-put-to-sqs",
"QueueArn": "$MAIN_ARN",
"Events": ["s3:ObjectCreated:*"]
}
]
}
EOF
aws s3api put-bucket-notification-configuration --bucket ingest-source-bucket --notification-configuration file:///app/notify.json
aws s3api get-bucket-notification-configuration --bucket ingest-source-bucket{
"QueueConfigurations": [
{
"Id": "source-put-to-sqs",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Events": [
"s3:ObjectCreated:*"
]
}
]
}
[stdout]
{
"QueueConfigurations": [
{
"Id": "source-put-to-sqs",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Events": [
"s3:ObjectCreated:*"
]
}
]
}import json
import os
import urllib.parse
from datetime import datetime, timezone
import boto3
SOURCE_BUCKET = os.environ["SOURCE_BUCKET"]
DEST_BUCKET = os.environ["DEST_BUCKET"]
DDB_TABLE = os.environ["DDB_TABLE"]
DEST_KMS_KEY_ARN = os.environ["DEST_KMS_KEY_ARN"]
_endpoint = os.environ.get("AWS_ENDPOINT_URL") or None
s3 = boto3.client("s3", endpoint_url=_endpoint)
ddb = boto3.client("dynamodb", endpoint_url=_endpoint)
def _process_s3_record(bucket, raw_key):
key = urllib.parse.unquote_plus(raw_key)
if bucket != SOURCE_BUCKET:
raise ValueError(f"unexpected bucket {bucket}")
obj = s3.get_object(Bucket=SOURCE_BUCKET, Key=key)
body = obj["Body"].read()
size = len(body)
dest_key = f"processed/{key}"
s3.put_object(
Bucket=DEST_BUCKET,
Key=dest_key,
Body=body,
ServerSideEncryption="aws:kms",
SSEKMSKeyId=DEST_KMS_KEY_ARN,
)
ddb.put_item(
TableName=DDB_TABLE,
Item={
"object_key": {"S": key},
"source_size": {"N": str(size)},
"processed_at": {"S": datetime.now(timezone.utc).isoformat()},
"status": {"S": "OK"},
},
)
def lambda_handler(event, context):
failures = []
for record in event.get("Records", []):
message_id = record.get("messageId", "unknown")
try:
body = json.loads(record["body"])
s3_records = body.get("Records")
if not s3_records:
continue
for s3_rec in s3_records:
bucket = s3_rec["s3"]["bucket"]["name"]
raw_key = s3_rec["s3"]["object"]["key"]
_process_s3_record(bucket, raw_key)
except Exception as exc:
print(f"record {message_id} failed: {type(exc).__name__}: {exc}")
failures.append({"itemIdentifier": message_id})
return {"batchItemFailures": failures}
Package lambda zip
cd /app/lambda && zip -q ../handler.zip handler.py && ls -la /app/handler.zip
-rw-r--r-- 1 root root 998 Apr 22 09:53 /app/handler.zip [stdout] -rw-r--r-- 1 root root 998 Apr 22 09:53 /app/handler.zip
Create Lambda function
source /app/env.sh
set -e
LAMBDA_ARN=$(aws lambda create-function \
--function-name image-ingest-consumer \
--runtime python3.11 \
--role "$LAMBDA_ROLE_ARN" \
--handler handler.lambda_handler \
--zip-file fileb:///app/handler.zip \
--timeout 30 \
--memory-size 256 \
--environment "Variables={SOURCE_BUCKET=ingest-source-bucket,DEST_BUCKET=ingest-dest-bucket,DDB_TABLE=image-ingest-records,DEST_KMS_KEY_ARN=$DST_KEY_ARN,AWS_ENDPOINT_URL=http://localstack:4566}" \
--query 'FunctionArn' --output text)
echo "LAMBDA_ARN=$LAMBDA_ARN"
cat >> /app/env.sh <<EOF
export LAMBDA_ARN=$LAMBDA_ARN
EOF
# Wait until active
for i in 1 2 3 4 5 6 7 8 9 10; do
st=$(aws lambda get-function --function-name image-ingest-consumer --query 'Configuration.State' --output text)
echo "lambda state: $st"
[ "$st" = "Active" ] && break
sleep 2
doneLAMBDA_ARN=arn:aws:lambda:us-east-1:000000000000:function:image-ingest-consumer lambda state: Pending lambda state: Pending lambda state: Active [stdout] LAMBDA_ARN=arn:aws:lambda:us-east-1:000000000000:function:image-ingest-consumer lambda state: Pending lambda state: Pending lambda state: Active
Create ESM with ReportBatchItemFailures
source /app/env.sh
set -e
ESM_UUID=$(aws lambda create-event-source-mapping \
--function-name image-ingest-consumer \
--event-source-arn "$MAIN_ARN" \
--batch-size 10 \
--maximum-batching-window-in-seconds 1 \
--function-response-types ReportBatchItemFailures \
--query 'UUID' --output text)
echo "ESM_UUID=$ESM_UUID"
cat >> /app/env.sh <<EOF
export ESM_UUID=$ESM_UUID
EOF
# confirm response type
aws lambda get-event-source-mapping --uuid "$ESM_UUID" --query '{State:State,RT:FunctionResponseTypes,Src:EventSourceArn}' --output json
# wait for Enabled
for i in 1 2 3 4 5 6 7 8 9 10; do
st=$(aws lambda get-event-source-mapping --uuid "$ESM_UUID" --query 'State' --output text)
echo "esm state: $st"
[ "$st" = "Enabled" ] && break
sleep 2
doneESM_UUID=19da0bba-76e5-4931-984a-1ecad75311a6
{
"State": "Enabled",
"RT": [
"ReportBatchItemFailures"
],
"Src": "arn:aws:sqs:us-east-1:000000000000:ingest-events"
}
esm state: Enabled
[stdout]
ESM_UUID=19da0bba-76e5-4931-984a-1ecad75311a6
{
"State": "Enabled",
"RT": [
"ReportBatchItemFailures"
],
"Src": "arn:aws:sqs:us-east-1:000000000000:ingest-events"
}
esm state: EnabledE2E test: upload two files and verify dest + ddb
source /app/env.sh set -e # Happy path: simple key echo "hello world payload A" > /tmp/a.txt aws s3api put-object --bucket ingest-source-bucket --key a.txt --body /tmp/a.txt >/dev/null echo "uploaded a.txt" # Spaced key echo "spaced key payload" > /tmp/b.txt aws s3api put-object --bucket ingest-source-bucket --key "hello world.txt" --body /tmp/b.txt >/dev/null echo "uploaded 'hello world.txt'" # Wait up to 60s for both to land for i in $(seq 1 30); do sleep 2 a_ok=$(aws s3api head-object --bucket ingest-dest-bucket --key "processed/a.txt" >/dev/null 2>&1 && echo yes || echo no) b_ok=$(aws s3api head-object --bucket ingest-dest-bucket --key "processed/hello world.txt" >/dev/null 2>&1 && echo yes || echo no) echo "$i a=$a_ok b=$b_ok" if [ "$a_ok" = "yes" ] && [ "$b_ok" = "yes" ]; then break; fi done echo "---- dest listing ----" aws s3api list-objects-v2 --bucket ingest-dest-bucket --query 'Contents[].Key' --output json echo "---- ddb scan ----" aws dynamodb scan --table-name image-ingest-records --query 'Items' --output json echo "---- DLQ depth ----" aws sqs get-queue-attributes --queue-url "$DLQ_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text
uploaded a.txt
uploaded 'hello world.txt'
1 a=no b=no
2 a=no b=no
3 a=no b=no
4 a=yes b=yes
---- dest listing ----
[
"processed/a.txt",
"processed/hello world.txt"
]
---- ddb scan ----
[
{
"source_size": {
"N": "22"
},
"processed_at": {
"S": "2026-04-22T09:54:06.402713+00:00"
},
"object_key": {
"S": "a.txt"
},
"status": {
"S": "OK"
}
},
{
"source_size": {
"N": "19"
},
"processed_at": {
"S": "2026-04-22T09:54:06.637234+00:00"
},
"object_key": {
"S": "hello world.txt"
},
"status": {
"S": "OK"
}
}
]
---- DLQ depth ----
0
[stdout]
uploaded a.txt
uploaded 'hello world.txt'
1 a=no b=no
2 a=no b=no
3 a=no b=no
4 a=yes b=yes
---- dest listing ----
[
"processed/a.txt",
"processed/hello world.txt"
]
---- ddb scan ----
[
{
"source_size": {
"N": "22"
},
"processed_at": {
"S": "2026-04-22T09:54:06.402713+00:00"
},
"object_key": {
"S": "a.txt"
},
"status": {
"S": "OK"
}
},
{
"source_size": {
"N": "19"
},
"processed_at": {
"S": "2026-04-22T09:54:06.637234+00:00"
},
"object_key": {
"S": "hello world.txt"
},
"status": {
"S": "OK"
}
}
]
---- DLQ depth ----
0Invoke lambda with mixed good/bad batch and verify response
source /app/env.sh
set -e
# Upload a fresh source object for the good record (don't fire a notification , we'll synthesize an event)
echo "partial batch good payload" > /tmp/good.txt
aws s3api put-object --bucket ingest-source-bucket --key "partial-good.txt" --body /tmp/good.txt >/dev/null
# Wait for the auto-pipeline to NOT interfere , give it time to settle, then delete the resulting dest object + ddb row so we can re-verify the synthesized invocation.
sleep 6
aws s3api delete-object --bucket ingest-dest-bucket --key "processed/partial-good.txt" >/dev/null || true
aws dynamodb delete-item --table-name image-ingest-records --key '{"object_key":{"S":"partial-good.txt"}}' >/dev/null || true
# Build a synthetic SQS batch event: 1 good (valid s3 envelope), 1 bad (unparseable body)
python3 - > /tmp/batch-event.json <<'PY'
import json
good_body = {
"Records": [{
"eventSource": "aws:s3",
"s3": {
"bucket": {"name": "ingest-source-bucket"},
"object": {"key": "partial-good.txt"}
}
}]
}
event = {
"Records": [
{
"messageId": "msg-good-1",
"receiptHandle": "rh-good",
"body": json.dumps(good_body),
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"awsRegion": "us-east-1"
},
{
"messageId": "msg-bad-1",
"receiptHandle": "rh-bad",
"body": "this is not json {",
"eventSource": "aws:sqs",
"eventSourceARN": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"awsRegion": "us-east-1"
}
]
}
print(json.dumps(event))
PY
aws lambda invoke \
--function-name image-ingest-consumer \
--payload fileb:///tmp/batch-event.json \
--cli-binary-format raw-in-base64-out \
/tmp/invoke-out.json >/tmp/invoke-meta.json
echo "---- invoke meta ----"
cat /tmp/invoke-meta.json
echo
echo "---- handler response ----"
cat /tmp/invoke-out.json
echo
echo "---- verify good record side-effects ----"
aws s3api head-object --bucket ingest-dest-bucket --key "processed/partial-good.txt" --query 'ContentLength' --output text
aws dynamodb get-item --table-name image-ingest-records --key '{"object_key":{"S":"partial-good.txt"}}' --query 'Item' --output json---- invoke meta ----
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
---- handler response ----
{"batchItemFailures": [{"itemIdentifier": "msg-bad-1"}]}
---- verify good record side-effects ----
27
{
"source_size": {
"N": "27"
},
"processed_at": {
"S": "2026-04-22T09:54:37.531020+00:00"
},
"object_key": {
"S": "partial-good.txt"
},
"status": {
"S": "OK"
}
}
[stdout]
---- invoke meta ----
{
"StatusCode": 200,
"ExecutedVersion": "$LATEST"
}
---- handler response ----
{"batchItemFailures": [{"itemIdentifier": "msg-bad-1"}]}
---- verify good record side-effects ----
27
{
"source_size": {
"N": "27"
},
"processed_at": {
"S": "2026-04-22T09:54:37.531020+00:00"
},
"object_key": {
"S": "partial-good.txt"
},
"status": {
"S": "OK"
}
}Final config audit
source /app/env.sh
set -e
echo "=== KMS aliases ==="
aws kms list-aliases --query 'Aliases[?contains(AliasName,`-cmk`)].{Alias:AliasName,KeyId:TargetKeyId}' --output table
echo "=== Buckets and encryption ==="
for b in ingest-source-bucket ingest-dest-bucket; do
enc=$(aws s3api get-bucket-encryption --bucket "$b" --query 'ServerSideEncryptionConfiguration.Rules[0].ApplyServerSideEncryptionByDefault' --output json)
echo "$b -> $enc"
done
echo "=== SQS attrs ==="
aws sqs get-queue-attributes --queue-url "$MAIN_URL" --attribute-names QueueArn KmsMasterKeyId VisibilityTimeout RedrivePolicy --query 'Attributes' --output json
aws sqs get-queue-attributes --queue-url "$DLQ_URL" --attribute-names QueueArn KmsMasterKeyId --query 'Attributes' --output json
echo "=== S3 notifications ==="
aws s3api get-bucket-notification-configuration --bucket ingest-source-bucket
echo "=== ESM ==="
aws lambda get-event-source-mapping --uuid "$ESM_UUID" --query '{State:State,RT:FunctionResponseTypes,BatchSize:BatchSize}' --output json
echo "=== DDB ==="
aws dynamodb describe-table --table-name image-ingest-records --query 'Table.{Keys:KeySchema,Status:TableStatus}' --output json
echo "=== Queue depths ==="
echo -n "main: "; aws sqs get-queue-attributes --queue-url "$MAIN_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text
echo -n "dlq : "; aws sqs get-queue-attributes --queue-url "$DLQ_URL" --attribute-names ApproximateNumberOfMessages --query 'Attributes.ApproximateNumberOfMessages' --output text
echo "=== Dest objects ==="
aws s3api list-objects-v2 --bucket ingest-dest-bucket --query 'Contents[].Key' --output json
echo "=== DDB items ==="
aws dynamodb scan --table-name image-ingest-records --query 'Items[].object_key.S' --output json=== KMS aliases ===
---------------------------------------------------------------------
| ListAliases |
+--------------------------+----------------------------------------+
| Alias | KeyId |
+--------------------------+----------------------------------------+
| alias/source-bucket-cmk | 161a7473-da40-49c6-a77d-ec49ee449b05 |
| alias/sqs-queue-cmk | 4988fc63-5443-49ac-ba64-3c704e8759e2 |
| alias/dest-bucket-cmk | f690d658-d425-42db-9241-8dc52bb2e812 |
+--------------------------+----------------------------------------+
=== Buckets and encryption ===
ingest-source-bucket -> {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/161a7473-da40-49c6-a77d-ec49ee449b05"
}
ingest-dest-bucket -> {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/f690d658-d425-42db-9241-8dc52bb2e812"
}
=== SQS attrs ===
{
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2",
"VisibilityTimeout": "90",
"RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:000000000000:ingest-dlq\", \"maxReceiveCount\": 5}"
}
{
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-dlq",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2"
}
=== S3 notifications ===
{
"QueueConfigurations": [
{
"Id": "source-put-to-sqs",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Events": [
"s3:ObjectCreated:*"
]
}
]
}
=== ESM ===
{
"State": "Enabled",
"RT": [
"ReportBatchItemFailures"
],
"BatchSize": 10
}
=== DDB ===
{
"Keys": [
{
"AttributeName": "object_key",
"KeyType": "HASH"
}
],
"Status": "ACTIVE"
}
=== Queue depths ===
main: 0
dlq : 0
=== Dest objects ===
[
"processed/a.txt",
"processed/hello world.txt",
"processed/partial-good.txt"
]
=== DDB items ===
[
"a.txt",
"hello world.txt",
"partial-good.txt"
]
[stdout]
=== KMS aliases ===
---------------------------------------------------------------------
| ListAliases |
+--------------------------+----------------------------------------+
| Alias | KeyId |
+--------------------------+----------------------------------------+
| alias/source-bucket-cmk | 161a7473-da40-49c6-a77d-ec49ee449b05 |
| alias/sqs-queue-cmk | 4988fc63-5443-49ac-ba64-3c704e8759e2 |
| alias/dest-bucket-cmk | f690d658-d425-42db-9241-8dc52bb2e812 |
+--------------------------+----------------------------------------+
=== Buckets and encryption ===
ingest-source-bucket -> {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/161a7473-da40-49c6-a77d-ec49ee449b05"
}
ingest-dest-bucket -> {
"SSEAlgorithm": "aws:kms",
"KMSMasterKeyID": "arn:aws:kms:us-east-1:000000000000:key/f690d658-d425-42db-9241-8dc52bb2e812"
}
=== SQS attrs ===
{
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2",
"VisibilityTimeout": "90",
"RedrivePolicy": "{\"deadLetterTargetArn\": \"arn:aws:sqs:us-east-1:000000000000:ingest-dlq\", \"maxReceiveCount\": 5}"
}
{
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-dlq",
"KmsMasterKeyId": "arn:aws:kms:us-east-1:000000000000:key/4988fc63-5443-49ac-ba64-3c704e8759e2"
}
=== S3 notifications ===
{
"QueueConfigurations": [
{
"Id": "source-put-to-sqs",
"QueueArn": "arn:aws:sqs:us-east-1:000000000000:ingest-events",
"Events": [
"s3:ObjectCreated:*"
]
}
]
}
=== ESM ===
{
"State": "Enabled",
"RT": [
"ReportBatchItemFailures"
],
"BatchSize": 10
}
=== DDB ===
{
"Keys": [
{
"AttributeName": "object_key",
"KeyType": "HASH"
}
],
"Status": "ACTIVE"
}
=== Queue depths ===
main: 0
dlq : 0
=== Dest objects ===
[
"processed/a.txt",
"processed/hello world.txt",
"processed/partial-good.txt"
]
=== DDB items ===
[
"a.txt",
"hello world.txt",
"partial-good.txt"
]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/5kwjkmpM3xeFQ2AVkOQFo/bin/python
cachedir: .pytest_cache
rootdir: /tests
plugins: json-ctrf-0.3.5
collecting ... collected 18 items
../tests/test_outputs.py::test_aws_cli_binary_is_real PASSED [ 5%]
../tests/test_outputs.py::test_localstack_reachable PASSED [ 11%]
../tests/test_outputs.py::test_source_and_dest_buckets_use_customer_cmks PASSED [ 16%]
../tests/test_outputs.py::test_main_queue_and_dlq_exist_and_are_encrypted PASSED [ 22%]
../tests/test_outputs.py::test_lambda_exists_with_expected_handler PASSED [ 27%]
../tests/test_outputs.py::test_ddb_table_exists PASSED [ 33%]
../tests/test_outputs.py::test_source_bucket_notification_targets_main_queue PASSED [ 38%]
../tests/test_outputs.py::test_main_queue_policy_allows_s3_service PASSED [ 44%]
../tests/test_outputs.py::test_sqs_cmk_allows_s3_service PASSED [ 50%]
../tests/test_outputs.py::test_source_cmk_allows_lambda_role_decrypt PASSED [ 55%]
../tests/test_outputs.py::test_dest_cmk_allows_lambda_role_encrypt PASSED [ 61%]
../tests/test_outputs.py::test_lambda_role_has_kms_decrypt_on_sqs_cmk PASSED [ 66%]
../tests/test_outputs.py::test_main_queue_visibility_timeout_covers_lambda_timeout PASSED [ 72%]
../tests/test_outputs.py::test_event_source_mapping_declares_report_batch_item_failures PASSED [ 77%]
../tests/test_outputs.py::test_lambda_handler_returns_correct_response_shape FAILED [ 83%]
../tests/test_outputs.py::test_end_to_end_preserves_object_size PASSED [ 88%]
../tests/test_outputs.py::test_end_to_end_key_with_spaces_is_decoded PASSED [ 94%]
../tests/test_outputs.py::test_end_to_end_upload_propagates_to_dest_and_ddb PASSED [100%]
=================================== FAILURES ===================================
______________ test_lambda_handler_returns_correct_response_shape ______________
lmb = <botocore.client.Lambda object at 0xffff962a6b10>
sqs = <botocore.client.SQS object at 0xffff965d3dd0>
s3 = <botocore.client.S3 object at 0xffff968a2f90>
def test_lambda_handler_returns_correct_response_shape(lmb, sqs, s3):
"""Partial batch response shape."""
probe_key = f"probe/verifier-{uuid.uuid4().hex[:8]}.bin"
s3.put_object(Bucket=SRC_BUCKET, Key=probe_key, Body=b"x" * 32)
main_arn = _queue_attrs(
sqs, _queue_url(sqs, MAIN_QUEUE), attrs=("QueueArn",)
)["QueueArn"]
good_body = {
"Records": [
{
"s3": {
"bucket": {"name": SRC_BUCKET},
"object": {"key": probe_key},
}
}
]
}
bad_body = {"some-other-shape": "not-an-s3-event"}
good_mid = "msg-good-" + uuid.uuid4().hex[:8]
bad_mid = "msg-bad-" + uuid.uuid4().hex[:8]
event = {
"Records": [
{
"messageId": good_mid,
"body": json.dumps(good_body),
"eventSource": "aws:sqs",
"eventSourceARN": main_arn,
"awsRegion": REGION,
},
{
"messageId": bad_mid,
"body": json.dumps(bad_body),
"eventSource": "aws:sqs",
"eventSourceARN": main_arn,
"awsRegion": REGION,
},
]
}
resp = lmb.invoke(
FunctionName=LAMBDA_FUNC,
InvocationType="RequestResponse",
Payload=json.dumps(event).encode(),
)
func_err = resp.get("FunctionError")
payload = resp["Payload"].read()
assert not func_err, (
f"Lambda invocation raised FunctionError={func_err}. "
f"Payload: {payload!r}"
)
result = json.loads(payload) if payload else None
assert isinstance(result, dict), (
f"Lambda returned non-object payload: {result!r}. Expected "
f"{{'batchItemFailures': [...]}}."
)
assert "batchItemFailures" in result, (
f"Lambda response is missing the required `batchItemFailures` "
f"key. AWS silently ignores any other key (e.g. "
f"`failedBatchItems`) when FunctionResponseTypes includes "
f"ReportBatchItemFailures. Got: {result}"
)
failures = result["batchItemFailures"]
assert isinstance(failures, list), (
f"batchItemFailures must be a list; got {type(failures).__name__}"
)
ids = [f.get("itemIdentifier") for f in failures]
> assert bad_mid in ids, (
f"Expected the bad record {bad_mid} in batchItemFailures; "
f"got ids={ids}. The handler must catch per-record exceptions "
f"and return the failed messageIds."
)
E AssertionError: Expected the bad record msg-bad-338aadc3 in batchItemFailures; got ids=[]. The handler must catch per-record exceptions and return the failed messageIds.
E assert 'msg-bad-338aadc3' in []
/tests/test_outputs.py:522: AssertionError
=============================== warnings summary ===============================
test_outputs.py: 59 warnings
/root/.cache/uv/archive-v0/5kwjkmpM3xeFQ2AVkOQFo/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_aws_cli_binary_is_real
PASSED ../tests/test_outputs.py::test_localstack_reachable
PASSED ../tests/test_outputs.py::test_source_and_dest_buckets_use_customer_cmks
PASSED ../tests/test_outputs.py::test_main_queue_and_dlq_exist_and_are_encrypted
PASSED ../tests/test_outputs.py::test_lambda_exists_with_expected_handler
PASSED ../tests/test_outputs.py::test_ddb_table_exists
PASSED ../tests/test_outputs.py::test_source_bucket_notification_targets_main_queue
PASSED ../tests/test_outputs.py::test_main_queue_policy_allows_s3_service
PASSED ../tests/test_outputs.py::test_sqs_cmk_allows_s3_service
PASSED ../tests/test_outputs.py::test_source_cmk_allows_lambda_role_decrypt
PASSED ../tests/test_outputs.py::test_dest_cmk_allows_lambda_role_encrypt
PASSED ../tests/test_outputs.py::test_lambda_role_has_kms_decrypt_on_sqs_cmk
PASSED ../tests/test_outputs.py::test_main_queue_visibility_timeout_covers_lambda_timeout
PASSED ../tests/test_outputs.py::test_event_source_mapping_declares_report_batch_item_failures
PASSED ../tests/test_outputs.py::test_end_to_end_preserves_object_size
PASSED ../tests/test_outputs.py::test_end_to_end_key_with_spaces_is_decoded
PASSED ../tests/test_outputs.py::test_end_to_end_upload_propagates_to_dest_and_ddb
FAILED ../tests/test_outputs.py::test_lambda_handler_returns_correct_response_shape
================== 1 failed, 17 passed, 59 warnings in 10.27s ==================Reproduce this trial: git checkout 2f94510 && PYTHONPATH=src python3 scripts/build_site.py , then open trial/trial_9ee88cb704074357. Re-running the agent live requires EVAL_PLATFORM_ENABLE_OAUTH_SMOKE=1 and is non-deterministic.
Trial trial_9ee88cb704074357 · verifier authoritative; classifier explanatory.