1import * as AWS from "@/AWS";
2import { AutoScalingConfiguration } from "@/AWS/AppRunner";
3import * as Test from "@/Test/Alchemy";
4import * as apprunner from "@distilled.cloud/aws/apprunner";
5import * as sts from "@distilled.cloud/aws/sts";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Ungated typed-error probe: proves the distilled error union carries the
13// not-found tag this provider's read/delete paths depend on.
14test.provider(
15 "describeAutoScalingConfiguration on a nonexistent ARN fails with ResourceNotFoundException",
16 () =>
17 Effect.gen(function* () {
18 const { Account } = yield* sts.getCallerIdentity({});
19 const error = yield* Effect.flip(
20 apprunner.describeAutoScalingConfiguration({
21 AutoScalingConfigurationArn: `arn:aws:apprunner:us-west-2:${Account}:autoscalingconfiguration/alchemy-nonexistent-probe/1/00000000000000000000000000000000`,
22 }),
23 );
24 expect(error._tag).toBe("ResourceNotFoundException");
25 }),
26 { tags: ["provider:aws", "provider:aws:apprunner", "live"] },
27);
28
29// No ACTIVE revision of the named configuration remains (deleted configs
30// linger as INACTIVE for observability).
31const assertConfigGone = (name: string) =>
32 Effect.gen(function* () {
33 const page = yield* apprunner.listAutoScalingConfigurations({
34 AutoScalingConfigurationName: name,
35 });
36 // App Runner returns lowercase statuses ("active") despite documenting
37 // uppercase — compare case-insensitively.
38 const active = (page.AutoScalingConfigurationSummaryList ?? []).filter(
39 (s) =>
40 s.AutoScalingConfigurationName === name &&
41 s.Status?.toUpperCase() === "ACTIVE",
42 );
43 if (active.length > 0) {
44 return yield* Effect.fail(
45 new Error(
46 `Auto scaling configuration '${name}' still has ${active.length} ACTIVE revision(s)`,
47 ),
48 );
49 }
50 }).pipe(
51 Effect.retry({
52 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
53 }),
54 );
55
56// Auto scaling configurations are immutable revisions — fast and free, so
57// the full lifecycle (create, no-op, revise, replace-by-name, destroy)
58// runs ungated.
59test.provider(
60 "create, revise, replace, and destroy an auto scaling configuration",
61 (stack) =>
62 Effect.gen(function* () {
63 // Clean slate in case a previous run died mid-flight.
64 yield* stack.destroy();
65
66 const props = {
67 autoScalingConfigurationName: "alchemy-test-asc",
68 maxConcurrency: 50,
69 minSize: 1,
70 maxSize: 3,
71 };
72
73 // Create.
74 const created = yield* stack.deploy(
75 AutoScalingConfiguration("Asc", props),
76 );
77 expect(created.autoScalingConfigurationName).toBe("alchemy-test-asc");
78 expect(created.autoScalingConfigurationArn).toContain(
79 ":autoscalingconfiguration/alchemy-test-asc/",
80 );
81 expect(created.maxConcurrency).toBe(50);
82 expect(created.maxSize).toBe(3);
83
84 // Out-of-band verification via distilled.
85 const described = yield* apprunner.describeAutoScalingConfiguration({
86 AutoScalingConfigurationArn: created.autoScalingConfigurationArn,
87 });
88 expect(described.AutoScalingConfiguration.Status?.toUpperCase()).toBe(
89 "ACTIVE",
90 );
91 expect(described.AutoScalingConfiguration.MaxConcurrency).toBe(50);
92 expect(described.AutoScalingConfiguration.MinSize).toBe(1);
93 expect(described.AutoScalingConfiguration.MaxSize).toBe(3);
94
95 // No-op redeploy must not create a new revision.
96 const noop = yield* stack.deploy(AutoScalingConfiguration("Asc", props));
97 expect(noop.autoScalingConfigurationRevision).toBe(
98 created.autoScalingConfigurationRevision,
99 );
100
101 // Settings change → new revision under the same name.
102 const revised = yield* stack.deploy(
103 AutoScalingConfiguration("Asc", { ...props, maxConcurrency: 80 }),
104 );
105 expect(revised.autoScalingConfigurationName).toBe("alchemy-test-asc");
106 expect(revised.autoScalingConfigurationRevision).toBe(
107 created.autoScalingConfigurationRevision + 1,
108 );
109 expect(revised.autoScalingConfigurationArn).not.toBe(
110 created.autoScalingConfigurationArn,
111 );
112 expect(revised.maxConcurrency).toBe(80);
113
114 // Name change → replace; every revision of the old name is deleted.
115 const replaced = yield* stack.deploy(
116 AutoScalingConfiguration("Asc", {
117 ...props,
118 autoScalingConfigurationName: "alchemy-test-asc-b",
119 }),
120 );
121 expect(replaced.autoScalingConfigurationName).toBe("alchemy-test-asc-b");
122 yield* assertConfigGone("alchemy-test-asc");
123
124 // Destroy and verify deletion out-of-band.
125 yield* stack.destroy();
126 yield* assertConfigGone("alchemy-test-asc-b");
127 }),
128 {
129 tags: ["provider:aws", "provider:aws:apprunner", "live"],
130 timeout: 120_000,
131 },
132);