1/**
2 * `AWS.Lambda.MicrovmImage` under `alchemy dev`: the dev provider builds the
3 * image with a plain HOST-side `docker build` (BuildKit layer cache against
4 * the user's real sources) and hands the emulator a pre-built
5 * `docker://<tag>` artifact, which the alchemy floci fork runs as-is —
6 * nothing is zipped, uploaded, or built inside the emulator.
7 *
8 * Proof structure:
9 * - deploy an effectful image: attrs carry the dummy account AND a
10 * `docker://alchemy-dev/microvm-…` artifact uri — the live cloud can
11 * never mint either, and the uri is proof the host-build path ran;
12 * - `RunMicrovm` out-of-band reaches RUNNING — the emulator actually
13 * BOOTED a VM from the host-built reference (an invalid or unbuilt ref
14 * fails the run);
15 * - re-deploy with NO change: a true noop (dev diff policy — content is
16 * the watch loop's job, and nothing changed);
17 * - rewrite the program's marker in the clone WITHOUT a deploy: the
18 * sidecar watch loop rebuilds (docker layer cache — only the COPY layer
19 * re-runs) and re-reconciles; the emulator's `latestActiveImageVersion`
20 * advances within seconds, and a fresh `RunMicrovm` boots the new
21 * version;
22 * - destroy removes the image from the emulator.
23 *
24 * Requires Docker (floci runs as a container, MicroVMs run as containers);
25 * skipped when the daemon is unavailable.
26 */
27import * as AWS from "@/AWS";
28import * as Endpoint from "@/AWS/Endpoint.ts";
29import * as Region from "@/AWS/Region.ts";
30import * as Test from "@/Test/Alchemy";
31import { Credentials } from "@distilled.cloud/aws/Credentials";
32import type { RegionName } from "@distilled.cloud/aws/Region";
33import * as microvms from "@distilled.cloud/aws/lambda-microvms";
34import { expect } from "alchemy-test";
35import * as Effect from "effect/Effect";
36import * as FileSystem from "effect/FileSystem";
37import * as Layer from "effect/Layer";
38import * as Path from "effect/Path";
39import * as Redacted from "effect/Redacted";
40import * as Schedule from "effect/Schedule";
41import { fileURLToPath } from "node:url";
42import { cloneFixture } from "../../Cloudflare/Utils/Fixture.ts";
43import { dockerAvailable, FLOCI_ENDPOINT } from "../Local/fixtures/raw.ts";
44
45const { test } = Test.make({ providers: AWS.providers(), dev: true });
46
47const fixtureDir = fileURLToPath(
48 new URL("./fixtures/microvm-dev", import.meta.url),
49);
50
51/** Floci-scoped context for the raw distilled calls the test makes itself. */
52const flociContext = Layer.mergeAll(
53 Endpoint.of(FLOCI_ENDPOINT),
54 Region.of("us-east-1"),
55 Layer.succeed(
56 Credentials,
57 Effect.succeed({
58 accessKeyId: Redacted.make("test"),
59 secretAccessKey: Redacted.make("test"),
60 sessionToken: undefined,
61 region: "us-east-1" as RegionName,
62 }),
63 ),
64);
65
66/** Boot a VM from the image and prove it reaches RUNNING, then clean up. */
67const bootsToRunning = (imageArn: string, imageVersion?: string) =>
68 Effect.gen(function* () {
69 const vm = yield* microvms.runMicrovm({
70 imageIdentifier: imageArn,
71 ...(imageVersion !== undefined ? { imageVersion } : {}),
72 });
73 const state = yield* microvms
74 .getMicrovm({ microvmIdentifier: vm.microvmId })
75 .pipe(
76 Effect.map((m) => m.state),
77 Effect.repeat({
78 schedule: Schedule.spaced("1 second"),
79 until: (s): boolean => s !== "PENDING",
80 times: 60,
81 }),
82 Effect.ensuring(
83 microvms
84 .terminateMicrovm({ microvmIdentifier: vm.microvmId })
85 .pipe(Effect.ignore),
86 ),
87 );
88 return { state, imageVersion: vm.imageVersion };
89 }).pipe(Effect.provide(flociContext));
90
91test.provider.skipIf(!dockerAvailable)(
92 "dev builds the image host-side (docker://), boots it, and a content edit hot-rebuilds it within seconds",
93 (stack) =>
94 Effect.gen(function* () {
95 const fs = yield* FileSystem.FileSystem;
96 const path = yield* Path.Path;
97
98 yield* stack.destroy();
99
100 const clone = yield* cloneFixture(fixtureDir, {
101 prefix: "floci-dev-microvm-",
102 });
103 const mainPath = path.join(clone, "program.ts");
104
105 const declaration = Effect.gen(function* () {
106 const buildRole = yield* AWS.IAM.Role("DevMicrovmBuildRole");
107 return yield* AWS.Lambda.MicrovmImage("DevMicrovm", {
108 main: mainPath,
109 buildRole,
110 runtime: "bun",
111 resources: [{ minimumMemoryInMiB: 512 }],
112 cpuConfigurations: [{ architecture: "ARM_64" }],
113 });
114 });
115
116 const first = yield* stack.deploy(declaration);
117 // Emulator identity: the live cloud never mints the dummy account.
118 expect(first.imageArn).toContain(":000000000000:");
119 // The host-build path: a pre-built local docker reference, not an
120 // uploaded-zip S3 uri.
121 expect(first.codeArtifact?.uri).toMatch(
122 /^docker:\/\/alchemy-dev\/microvm-/,
123 );
124 expect(first.codeArtifact?.hash).toBeDefined();
125 const v1 = first.latestActiveImageVersion;
126 expect(v1).toBeDefined();
127
128 // The emulator can BOOT the host-built reference: RunMicrovm reaches
129 // RUNNING (a missing or broken image ref fails the run).
130 const booted = yield* bootsToRunning(first.imageArn);
131 expect(booted.state).toBe("RUNNING");
132 expect(booted.imageVersion).toBe(v1);
133
134 // Same declaration, same source — a true noop: nothing is rebuilt.
135 const unchanged = yield* stack.deploy(declaration);
136 expect(unchanged.codeArtifact?.hash).toBe(first.codeArtifact!.hash);
137 expect(unchanged.latestActiveImageVersion).toBe(v1);
138
139 // Content-only edit, NO deploy: the sidecar watch loop rebuilds
140 // (cached docker build) and re-reconciles; the emulator's active
141 // version advances. This is the latency the whole design exists for.
142 const source = yield* fs.readFileString(mainPath);
143 const editStartedAt = Date.now();
144 yield* fs.writeFileString(
145 mainPath,
146 source.replace(`"microvm-marker-v1"`, `"microvm-marker-v2"`),
147 );
148 const rebuilt = yield* microvms
149 .getMicrovmImage({ imageIdentifier: first.imageArn })
150 .pipe(
151 Effect.provide(flociContext),
152 Effect.repeat({
153 schedule: Schedule.spaced("500 millis"),
154 until: (image): boolean =>
155 image.latestActiveImageVersion !== undefined &&
156 image.latestActiveImageVersion !== v1,
157 times: 120,
158 }),
159 );
160 const rebuildMs = Date.now() - editStartedAt;
161 // eslint-disable-next-line no-console
162 console.log(
163 `microvm content edit -> new active version in ${rebuildMs}ms`,
164 );
165 expect(rebuilt.latestActiveImageVersion).not.toBe(v1);
166 // The point of the docker:// path: a content edit is a cached docker
167 // build, not a zip-upload-extract-rebuild. Well under a minute even
168 // on a cold-ish daemon; typically a few seconds.
169 expect(rebuildMs).toBeLessThan(30_000);
170
171 // A fresh VM boots the NEW active version.
172 const rebooted = yield* bootsToRunning(first.imageArn);
173 expect(rebooted.state).toBe("RUNNING");
174 expect(rebooted.imageVersion).toBe(rebuilt.latestActiveImageVersion);
175
176 // Destroy: the image must be gone from the emulator.
177 yield* stack.destroy();
178 const gone = yield* microvms
179 .getMicrovmImage({ imageIdentifier: first.imageArn })
180 .pipe(
181 Effect.map(() => false),
182 Effect.catchTag("ResourceNotFoundException", () =>
183 Effect.succeed(true),
184 ),
185 Effect.provide(flociContext),
186 Effect.repeat({
187 schedule: Schedule.spaced("1 second"),
188 until: (isGone): boolean => isGone,
189 times: 20,
190 }),
191 );
192 expect(gone).toBe(true);
193 }),
194 {
195 tags: ["provider:aws", "provider:aws:iam", "provider:aws:lambda", "local"],
196 timeout: 600_000,
197 },
198);