t3-code-android-nightly/.repos/alchemy-effect/packages/alchemy/test/AWS/Lambda/VpcFunctionDestroy.test.ts
Julius Marminge 6f9cea00ae
chore(refs): sync Effect and Alchemy references to 4.0.1 and beta.80 (#16170)
Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-10-05 13:22:30 -07:00

112 lines
4 KiB
TypeScript

import * as AWS from "@/AWS";
import * as Test from "@/Test/Alchemy";
import * as EC2 from "@distilled.cloud/aws/ec2";
import * as Lambda from "@distilled.cloud/aws/lambda";
import { expect } from "alchemy-test";
import * as Effect from "effect/Effect";
import * as Schedule from "effect/Schedule";
import { fileURLToPath } from "node:url";
const { test } = Test.make({ providers: AWS.providers() });
const handlerPath = fileURLToPath(
new URL("./timeout-handler.ts", import.meta.url),
);
// Repro for https://github.com/xofromthemoon/alchemy-aws-demo — a VPC-attached
// Lambda leaves Hyperplane ENIs in its subnets/security group after
// DeleteFunction. AWS releases them asynchronously (up to ~20 minutes), which
// used to outlive the Subnet/SecurityGroup DependencyViolation retry budgets
// and fail the destroy, forcing users to run destroy multiple times. The
// delete paths now reap detached Lambda ENIs explicitly so a single destroy
// converges. Slow (minutes of AWS-side ENI release), so FAST-gated.
test.provider.skipIf(!!process.env.FAST)(
"destroy converges in one pass with a VPC-attached function",
(stack) =>
Effect.gen(function* () {
yield* stack.destroy();
const program = Effect.gen(function* () {
const network = yield* AWS.EC2.Network("Network", {
cidrBlock: "10.42.0.0/16",
availabilityZones: 2,
nat: "none",
});
const sg = yield* AWS.EC2.SecurityGroup("FunctionSecurityGroup", {
vpcId: network.vpcId,
description: "VPC lambda destroy repro",
ingress: [
{
ipProtocol: "tcp",
fromPort: 443,
toPort: 443,
cidrIpv4: "10.42.0.0/16",
description: "intra-vpc https",
},
],
});
const fn = yield* AWS.Lambda.Function("VpcFn", {
main: handlerPath,
handler: "handler",
isExternal: true,
functionUrl: false,
vpc: {
subnetIds: network.privateSubnetIds,
securityGroupIds: [sg.groupId],
},
});
return {
vpcId: network.vpcId,
privateSubnetIds: network.privateSubnetIds,
groupId: sg.groupId,
functionName: fn.functionName,
};
});
const deployed = yield* stack.deploy(program);
const configuration = yield* Lambda.getFunctionConfiguration({
FunctionName: deployed.functionName,
});
expect(configuration.State).toBe("Active");
expect(configuration.LastUpdateStatus).toBe("Successful");
expect(configuration.VpcConfig?.VpcId).toBe(deployed.vpcId);
// The function's Hyperplane ENI(s) materialize in the private subnets
// shortly after the function goes Active — poll (describe is
// eventually consistent right after deploy). Note: attached Hyperplane
// ENIs report InterfaceType "lambda"; once detached they flip to plain
// "interface" and only the description identifies them.
const isLambdaEni = (eni: EC2.NetworkInterface): boolean =>
eni.InterfaceType === "lambda" ||
(eni.Description?.startsWith("AWS Lambda VPC ENI") ?? false);
const before = yield* EC2.describeNetworkInterfaces({
Filters: [{ Name: "vpc-id", Values: [deployed.vpcId] }],
}).pipe(
Effect.repeat({
schedule: Schedule.spaced("5 seconds"),
until: (page) => (page.NetworkInterfaces ?? []).some(isLambdaEni),
times: 36,
}),
);
expect((before.NetworkInterfaces ?? []).some(isLambdaEni)).toBe(true);
// One destroy must fully tear down: function, SG, subnets, VPC.
yield* stack.destroy();
const vpcs = yield* EC2.describeVpcs({
VpcIds: [deployed.vpcId],
}).pipe(
Effect.catchTag("InvalidVpcID.NotFound", () =>
Effect.succeed({ Vpcs: [] }),
),
);
expect(vpcs.Vpcs ?? []).toHaveLength(0);
}),
{
tags: ["provider:aws", "provider:aws:ec2", "provider:aws:lambda", "live"],
timeout: 30 * 60 * 1000,
},
);