| 1 | import * as AWS from "@/AWS"; |
| 2 | import { amazonLinux2023, Instance, Subnet, Vpc } from "@/AWS/EC2"; |
| 3 | import * as Test from "./VpcTest.ts"; |
| 4 | import { expect } from "alchemy-test"; |
| 5 | import * as Effect from "effect/Effect"; |
| 6 | import * as FileSystem from "effect/FileSystem"; |
| 7 | import * as Path from "effect/Path"; |
| 8 | import { assertInstanceTerminated, assertVpcGone } from "./Gone.ts"; |
| 9 | |
| 10 | const { test } = Test.make({ providers: AWS.providers() }); |
| 11 | |
| 12 | // A change confined to the hosted runtime program must participate in |
| 13 | // planning (#1025): before the fix, `code.hash` was only computed during |
| 14 | // reconcile, so a code-only edit planned `noop` and the stale bundle stayed |
| 15 | // live. The program is `isExternal` (a plain script) so the test can rewrite |
| 16 | // the source between plans. |
| 17 | test.provider.skipIf(!!process.env.FAST)( |
| 18 | "code-only change to the hosted program plans an in-place update", |
| 19 | (stack) => |
| 20 | Effect.gen(function* () { |
| 21 | yield* stack.destroy(); |
| 22 | const fs = yield* FileSystem.FileSystem; |
| 23 | const path = yield* Path.Path; |
| 24 | |
| 25 | const imageId = amazonLinux2023(); |
| 26 | const dir = yield* fs.makeTempDirectory({ prefix: "alchemy-ec2-code-" }); |
| 27 | const mainPath = path.join(dir, "main.ts"); |
| 28 | // A marker-bearing long-running program; only its bundle hash matters |
| 29 | // to this test, never its runtime behavior. |
| 30 | const writeProgram = (marker: string) => |
| 31 | fs.writeFileString( |
| 32 | mainPath, |
| 33 | `setInterval(() => console.log(${JSON.stringify(marker)}), 60_000);\n`, |
| 34 | ); |
| 35 | yield* writeProgram("generation-one"); |
| 36 | |
| 37 | const makeStack = Effect.gen(function* () { |
| 38 | const vpc = yield* Vpc("CodeInstanceVpc", { |
| 39 | cidrBlock: "10.0.0.0/16", |
| 40 | }); |
| 41 | const subnet = yield* Subnet("CodeInstanceSubnet", { |
| 42 | vpcId: vpc.vpcId, |
| 43 | cidrBlock: "10.0.1.0/24", |
| 44 | }); |
| 45 | const instance = yield* Instance("CodeInstance", { |
| 46 | imageId, |
| 47 | instanceType: "t3.micro", |
| 48 | subnetId: subnet.subnetId, |
| 49 | main: mainPath, |
| 50 | isExternal: true, |
| 51 | }); |
| 52 | return { vpc, instance }; |
| 53 | }); |
| 54 | |
| 55 | const { vpc, instance } = yield* stack.deploy(makeStack); |
| 56 | |
| 57 | // Unchanged source replans as noop (bundling is deterministic)... |
| 58 | const before = yield* stack.plan(makeStack); |
| 59 | expect(before.resources.CodeInstance).toMatchObject({ action: "noop" }); |
| 60 | |
| 61 | // ...and a code-only edit plans an in-place update. |
| 62 | yield* writeProgram("generation-two"); |
| 63 | const after = yield* stack.plan(makeStack); |
| 64 | expect(after.resources.CodeInstance).toMatchObject({ action: "update" }); |
| 65 | |
| 66 | yield* stack.destroy(); |
| 67 | |
| 68 | // Zero-orphan proof. |
| 69 | yield* assertInstanceTerminated(instance.instanceId); |
| 70 | yield* assertVpcGone(vpc.vpcId); |
| 71 | }), |
| 72 | { tags: ["provider:aws", "provider:aws:ec2", "live"], timeout: 600_000 }, |
| 73 | ); |