1import * as AWS from "@/AWS";
2import * as Test from "@/Test/Alchemy";
3import * as EC2 from "@distilled.cloud/aws/ec2";
4import * as fsx from "@distilled.cloud/aws/fsx";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import { getDefaultVpc } from "../DefaultVpc.ts";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// FSx surfaces a missing file system as the typed FileSystemNotFound.
13test.provider(
14 "typed FileSystemNotFound on a nonexistent file system",
15 () =>
16 Effect.gen(function* () {
17 const result = yield* fsx
18 .describeFileSystems({ FileSystemIds: ["fs-00000000000000000"] })
19 .pipe(Effect.result);
20 expect(result._tag).toBe("Failure");
21 if (result._tag === "Failure") {
22 const err = result.failure as { _tag: string };
23 expect(err._tag).toBe("FileSystemNotFound");
24 }
25 }),
26 { tags: ["provider:aws", "provider:aws:fsx", "live"], timeout: 60_000 },
27);
28
29const describeById = (fileSystemId: string) =>
30 fsx.describeFileSystems({ FileSystemIds: [fileSystemId] }).pipe(
31 Effect.map((r) => r.FileSystems?.[0]),
32 Effect.catchTag("FileSystemNotFound", () => Effect.succeed(undefined)),
33 );
34
35// FSx create → AVAILABLE takes ~5 minutes; slow polling is allowed only
36// because the whole lifecycle is gated behind AWS_TEST_SLOW=1.
37const waitUntilAvailable = (fileSystemId: string) =>
38 describeById(fileSystemId).pipe(
39 Effect.repeat({
40 schedule: Schedule.spaced("15 seconds"),
41 until: (fs) =>
42 fs?.Lifecycle === "AVAILABLE" || fs?.Lifecycle === "FAILED",
43 times: 60,
44 }),
45 );
46
47const waitUntilGone = (fileSystemId: string) =>
48 describeById(fileSystemId).pipe(
49 Effect.repeat({
50 schedule: Schedule.spaced("15 seconds"),
51 until: (fs) => fs === undefined,
52 times: 60,
53 }),
54 );
55
56const fsxTags = (tags: readonly fsx.Tag[] | undefined) =>
57 Object.fromEntries((tags ?? []).map((t) => [t.Key, t.Value]));
58
59// A Lustre SCRATCH_2 file system takes several minutes to provision and a few
60// more to delete (and bills hourly while up), so the live lifecycle is gated
61// behind AWS_TEST_SLOW=1.
62test.provider.skipIf(!process.env.AWS_TEST_SLOW)(
63 "create Lustre scratch file system, update tags, delete",
64 (stack) =>
65 Effect.gen(function* () {
66 yield* stack.destroy();
67
68 const vpc = yield* getDefaultVpc;
69 const subnets = yield* EC2.describeSubnets({
70 Filters: [
71 { Name: "vpc-id", Values: [vpc.vpcId] },
72 { Name: "default-for-az", Values: ["true"] },
73 ],
74 });
75 const subnetId = subnets.Subnets![0]!.SubnetId!;
76
77 const build = (tags: Record<string, string>) =>
78 Effect.gen(function* () {
79 const scratch = yield* AWS.FSx.FileSystem("Scratch", {
80 fileSystemType: "LUSTRE",
81 storageCapacity: 1200,
82 subnetIds: [subnetId],
83 lustreConfiguration: { DeploymentType: "SCRATCH_2" },
84 tags,
85 });
86 return { scratch };
87 });
88
89 // --- create ---
90 const created = yield* stack.deploy(
91 build({ purpose: "alchemy-fsx-test" }),
92 );
93 expect(created.scratch.fileSystemId).toContain("fs-");
94 expect(created.scratch.fileSystemArn).toContain(":file-system/fs-");
95 expect(created.scratch.fileSystemType).toBe("LUSTRE");
96 const fileSystemId = created.scratch.fileSystemId;
97
98 const available = yield* waitUntilAvailable(fileSystemId);
99 expect(available?.Lifecycle).toBe("AVAILABLE");
100 expect(available?.FileSystemType).toBe("LUSTRE");
101 expect(available?.StorageCapacity).toBe(1200);
102 const tags = fsxTags(available?.Tags);
103 expect(tags.purpose).toBe("alchemy-fsx-test");
104 expect(tags["alchemy::id"]).toBe("Scratch");
105
106 // --- update: tags sync in place, identity preserved ---
107 const updated = yield* stack.deploy(
108 build({ purpose: "alchemy-fsx-test", stage: "prod" }),
109 );
110 expect(updated.scratch.fileSystemId).toBe(fileSystemId);
111 const observed = yield* describeById(fileSystemId);
112 expect(fsxTags(observed?.Tags).stage).toBe("prod");
113
114 // --- delete ---
115 yield* stack.destroy();
116 const gone = yield* waitUntilGone(fileSystemId);
117 expect(gone).toBeUndefined();
118 }),
119 {
120 tags: ["provider:aws", "provider:aws:ec2", "provider:aws:fsx", "live"],
121 timeout: 1_500_000,
122 },
123);