1import * as AWS from "@/AWS";
2import { amazonLinux2023, Instance, Subnet, Vpc } from "@/AWS/EC2";
3import * as Test from "./VpcTest.ts";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as FileSystem from "effect/FileSystem";
7import * as Path from "effect/Path";
8import { assertInstanceTerminated, assertVpcGone } from "./Gone.ts";
9
10const { 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.
17test.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);