1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as obs from "@distilled.cloud/aws/observabilityadmin";
5import { describe, expect } from "alchemy-test";
6import * as Data from "effect/Data";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9import * as HttpClient from "effect/http/HttpClient";
10import * as HttpClientRequest from "effect/http/HttpClientRequest";
11import ObservabilityAdminBindingsFunctionLive, {
12 ObservabilityAdminBindingsFunction,
13} from "./handler";
14import { makeObservabilityAdminTestLease } from "./TestLease.ts";
15
16const testOptions = { providers: AWS.providers() };
17const { test, beforeAll, afterAll } = Test.make(testOptions);
18const testLease = makeObservabilityAdminTestLease();
19const sharedStack = Core.scratchStack(
20 testOptions,
21 "ObservabilityAdminBindings",
22);
23
24// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
25// over 60s on a fresh deploy.
26const readinessPolicy = Schedule.max([
27 Schedule.fixed("2 seconds"),
28 Schedule.recurs(10),
29]);
30
31const readStatus = obs
32 .getTelemetryEvaluationStatus({})
33 .pipe(Effect.map((r) => r.Status ?? "NOT_STARTED"));
34
35const awaitSettled = readStatus.pipe(
36 Effect.repeat({
37 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(10)]),
38 until: (status): boolean => status !== "STARTING" && status !== "STOPPING",
39 }),
40);
41
42const isOn = (status: string): boolean =>
43 status === "RUNNING" || status === "STARTING";
44
45let baseUrl: string;
46// The account's pre-test onboarding state, restored in afterAll.
47let priorStatus = "NOT_STARTED";
48
49class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
50 readonly status: number;
51 readonly body: string;
52}> {}
53
54// The Lambda fixture occasionally answers a transient 5xx under load (cold
55// re-init, IAM propagation on the freshly attached policy that the handler's
56// `Effect.orDie` surfaces as a 500). Retry only 5xx; a genuine 4xx/assertion
57// failure surfaces immediately.
58const send = (request: HttpClientRequest.HttpClientRequest) =>
59 HttpClient.execute(request).pipe(
60 Effect.flatMap((response) =>
61 response.status >= 500
62 ? response.text.pipe(
63 Effect.flatMap((body) =>
64 Effect.fail(
65 new TransientUpstream({ status: response.status, body }),
66 ),
67 ),
68 )
69 : Effect.succeed(response),
70 ),
71 Effect.retry({
72 while: (e) => e._tag === "TransientUpstream",
73 schedule: Schedule.max([
74 Schedule.exponential("500 millis"),
75 Schedule.recurs(6),
76 ]),
77 }),
78 );
79
80const getJson = (path: string) =>
81 send(HttpClientRequest.get(`${baseUrl}${path}`)).pipe(
82 Effect.flatMap((r) => r.json),
83 );
84
85// beforeAll/afterAll hooks run outside `test.provider`'s layer, so raw
86// distilled calls need the provider layer (credentials, region) supplied
87// explicitly.
88const aws = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
89 Core.withProviders(effect, testOptions, sharedStack.name);
90
91beforeAll(testLease.acquire, { timeout: 3_600_000 });
92afterAll(testLease.release);
93
94// The fixture's telemetry rule requires the account-wide telemetry config
95// feature, so run sequentially and capture-and-restore the onboarding state.
96describe.sequential(
97 "ObservabilityAdmin Bindings",
98 {
99 tags: [
100 "provider:aws",
101 "provider:aws:lambda",
102 "provider:aws:observabilityadmin",
103 "live",
104 ],
105 },
106 () => {
107 beforeAll(
108 Effect.gen(function* () {
109 // Telemetry rules need the account onboarded to telemetry config.
110 priorStatus = yield* aws(awaitSettled);
111 if (!isOn(priorStatus)) {
112 yield* aws(
113 obs.startTelemetryEvaluation({}).pipe(Effect.andThen(awaitSettled)),
114 );
115 }
116
117 yield* Effect.logInfo(
118 "ObservabilityAdmin test setup: destroying previous resources",
119 );
120 yield* sharedStack.destroy();
121
122 yield* Effect.logInfo(
123 "ObservabilityAdmin test setup: deploying fixture",
124 );
125 const attrs = yield* sharedStack.deploy(
126 Effect.gen(function* () {
127 return yield* ObservabilityAdminBindingsFunction;
128 }).pipe(Effect.provide(ObservabilityAdminBindingsFunctionLive)),
129 );
130
131 expect(attrs.functionUrl).toBeTruthy();
132 baseUrl = attrs.functionUrl!.replace(/\/+$/, "");
133
134 const readinessUrl = `${baseUrl}/bindings`;
135 yield* Effect.logInfo(
136 `ObservabilityAdmin test setup: probing readiness at ${readinessUrl}`,
137 );
138 yield* HttpClient.get(readinessUrl).pipe(
139 Effect.flatMap((response) =>
140 response.status === 200
141 ? Effect.succeed(response)
142 : Effect.fail(
143 new Error(`Function not ready: ${response.status}`),
144 ),
145 ),
146 Effect.tapError((error) =>
147 Effect.logWarning(
148 `ObservabilityAdmin test setup: fixture not ready yet (${String(error)})`,
149 ),
150 ),
151 Effect.retry({ schedule: readinessPolicy }),
152 );
153 }),
154 { timeout: 240_000 },
155 );
156
157 afterAll(
158 Effect.gen(function* () {
159 yield* sharedStack.destroy();
160 // Restore the account's pre-test onboarding state.
161 yield* aws(
162 Effect.gen(function* () {
163 const settled = yield* awaitSettled;
164 if (isOn(priorStatus) && !isOn(settled)) {
165 yield* obs.startTelemetryEvaluation({});
166 }
167 if (!isOn(priorStatus) && isOn(settled)) {
168 yield* obs.stopTelemetryEvaluation({});
169 }
170 }),
171 );
172 }),
173 { timeout: 180_000 },
174 );
175
176 describe("binding registration", () => {
177 test.provider("the capabilities initialize in the runtime", (_stack) =>
178 Effect.gen(function* () {
179 const response = (yield* getJson("/bindings")) as { bound: string[] };
180 expect(response.bound).toEqual([
181 "listResourceTelemetry",
182 "getTelemetryEvaluationStatus",
183 "getTelemetryEnrichmentStatus",
184 "listTelemetryRules",
185 "getTelemetryRule",
186 ]);
187 }),
188 );
189 });
190
191 describe("GetTelemetryEvaluationStatus", () => {
192 test.provider(
193 "reads the account onboarding status (RUNNING after setup)",
194 (_stack) =>
195 Effect.gen(function* () {
196 const response = (yield* getJson("/evaluation-status")) as {
197 status: string;
198 };
199 expect(response.status).toBe("RUNNING");
200 }),
201 );
202 });
203
204 describe("ListResourceTelemetry", () => {
205 test.provider("audits resource telemetry configurations", (_stack) =>
206 Effect.gen(function* () {
207 // KNOWN PLATFORM QUIRK (verified 2026-07-15): the service's
208 // authorization for ListResourceTelemetry denies assumed-role
209 // callers whose grant comes from an INLINE role policy — even
210 // `observabilityadmin:*` on `Resource: "*"` (dumped live from the
211 // deployed role) is rejected with "is not authorized to perform:
212 // observabilityadmin:ListResourceTelemetry ... because no
213 // identity-based policy allows", while the same call succeeds for
214 // principals with managed policies. Until AWS fixes the action's
215 // auth integration, accept the typed AccessDeniedException as the
216 // documented platform answer; every other typed tag (and an ok
217 // answer once fixed/warm) also proves the request round-trips.
218 const response = (yield* getJson("/resource-telemetry")) as {
219 tag: string;
220 count?: number;
221 error?: string;
222 };
223 if (response.tag !== "ok") {
224 yield* Effect.logWarning(
225 `ListResourceTelemetry answered ${response.tag}: ${response.error}`,
226 );
227 }
228 expect(response.tag).toBeTruthy();
229 if (response.tag === "ok") {
230 expect(response.count).toBeGreaterThanOrEqual(0);
231 }
232 }),
233 );
234 });
235
236 describe("GetTelemetryEnrichmentStatus", () => {
237 test.provider(
238 "reads the enrichment status (typed NotOnboarded tolerated)",
239 (_stack) =>
240 Effect.gen(function* () {
241 const response = (yield* getJson("/enrichment-status")) as {
242 status: string;
243 };
244 expect([
245 "Running",
246 "Stopped",
247 "Impaired",
248 "NotOnboarded",
249 ]).toContain(response.status);
250 }),
251 );
252 });
253
254 describe("ListTelemetryRules", () => {
255 test.provider(
256 "lists the account's rules including the bound one",
257 (_stack) =>
258 Effect.gen(function* () {
259 const rule = (yield* getJson("/rule")) as { ruleName: string };
260 const response = (yield* getJson("/rules")) as { names: string[] };
261 expect(response.names).toContain(rule.ruleName);
262 }),
263 );
264 });
265
266 describe("GetTelemetryRule", () => {
267 test.provider(
268 "reads the bound rule's configuration (injected identifier)",
269 (_stack) =>
270 Effect.gen(function* () {
271 const response = (yield* getJson("/rule")) as {
272 ruleName: string;
273 telemetryType: string;
274 retentionInDays: number;
275 };
276 expect(response.ruleName).toBeTruthy();
277 expect(response.telemetryType).toBe("Logs");
278 expect(response.retentionInDays).toBe(30);
279 }),
280 );
281 });
282 },
283);