1import * as AWS from "@/AWS";
2import { ObservabilityConfiguration } 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 "describeObservabilityConfiguration 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.describeObservabilityConfiguration({
21 ObservabilityConfigurationArn: `arn:aws:apprunner:us-west-2:${Account}:observabilityconfiguration/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. Unlike auto
30// scaling configuration summaries, observability configuration summaries
31// carry no Status field — the list only returns active revisions.
32const assertConfigGone = (name: string) =>
33 Effect.gen(function* () {
34 const page = yield* apprunner.listObservabilityConfigurations({
35 ObservabilityConfigurationName: name,
36 });
37 const active = (page.ObservabilityConfigurationSummaryList ?? []).filter(
38 (s) => s.ObservabilityConfigurationName === name,
39 );
40 if (active.length > 0) {
41 return yield* Effect.fail(
42 new Error(
43 `Observability configuration '${name}' still has ${active.length} ACTIVE revision(s)`,
44 ),
45 );
46 }
47 }).pipe(
48 Effect.retry({
49 schedule: Schedule.max([Schedule.fixed("3 seconds"), Schedule.recurs(8)]),
50 }),
51 );
52
53// Observability configurations are immutable revisions — fast and free, so
54// the full lifecycle (create, no-op, replace-by-name, destroy) runs ungated.
55test.provider(
56 "create, no-op, replace, and destroy an observability configuration",
57 (stack) =>
58 Effect.gen(function* () {
59 // Clean slate in case a previous run died mid-flight.
60 yield* stack.destroy();
61
62 const props = {
63 observabilityConfigurationName: "alchemy-test-obs",
64 traceConfiguration: { vendor: "AWSXRAY" as const },
65 };
66
67 // Create.
68 const created = yield* stack.deploy(
69 ObservabilityConfiguration("Obs", props),
70 );
71 expect(created.observabilityConfigurationName).toBe("alchemy-test-obs");
72 expect(created.observabilityConfigurationArn).toContain(
73 ":observabilityconfiguration/alchemy-test-obs/",
74 );
75 expect(created.traceVendor).toBe("AWSXRAY");
76 expect(created.observabilityConfigurationRevision).toBeGreaterThanOrEqual(
77 1,
78 );
79
80 // Out-of-band verification via distilled.
81 const described = yield* apprunner.describeObservabilityConfiguration({
82 ObservabilityConfigurationArn: created.observabilityConfigurationArn,
83 });
84 expect(described.ObservabilityConfiguration.Status?.toUpperCase()).toBe(
85 "ACTIVE",
86 );
87 expect(
88 described.ObservabilityConfiguration.TraceConfiguration?.Vendor,
89 ).toBe("AWSXRAY");
90
91 // No-op redeploy must not create a new revision.
92 const noop = yield* stack.deploy(
93 ObservabilityConfiguration("Obs", props),
94 );
95 expect(noop.observabilityConfigurationRevision).toBe(
96 created.observabilityConfigurationRevision,
97 );
98
99 // Name change → replace; every revision of the old name is deleted.
100 const replaced = yield* stack.deploy(
101 ObservabilityConfiguration("Obs", {
102 ...props,
103 observabilityConfigurationName: "alchemy-test-obs-b",
104 }),
105 );
106 expect(replaced.observabilityConfigurationName).toBe(
107 "alchemy-test-obs-b",
108 );
109 yield* assertConfigGone("alchemy-test-obs");
110
111 // Destroy and verify deletion out-of-band.
112 yield* stack.destroy();
113 yield* assertConfigGone("alchemy-test-obs-b");
114 }),
115 {
116 tags: ["provider:aws", "provider:aws:apprunner", "live"],
117 timeout: 120_000,
118 },
119);