1import * as AWS from "@/AWS";
2import { AWSEnvironment } from "@/AWS/Environment";
3import * as Core from "@/Test/Core";
4import * as Test from "@/Test/Alchemy";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import ForecastTestFunctionLive, { ForecastTestFunction } from "./handler";
10
11const testOptions = { providers: AWS.providers() };
12const { test, beforeAll, afterAll } = Test.make(testOptions);
13const sharedStack = Core.scratchStack(testOptions, "ForecastBindings");
14
15// Lambda function URL cold-start (DNS, IAM propagation, init) can take well
16// over 60s on a fresh deploy.
17const readinessPolicy = Schedule.max([
18 Schedule.fixed("2 seconds"),
19 Schedule.recurs(75),
20]);
21
22let baseUrl: string;
23
24// Well-formed-but-nonexistent ARNs the probe routes are driven against.
25// Computed inside test bodies (test.provider provides AWSEnvironment).
26const probeArns = Effect.gen(function* () {
27 const { accountId, region } = yield* AWSEnvironment.current;
28 return {
29 importJob: `arn:aws:forecast:${region}:${accountId}:dataset-import-job/alchemy_probe/alchemy_probe`,
30 predictor: `arn:aws:forecast:${region}:${accountId}:predictor/alchemy_probe`,
31 forecast: `arn:aws:forecast:${region}:${accountId}:forecast/alchemy_probe`,
32 forecastExport: `arn:aws:forecast:${region}:${accountId}:forecast-export-job/alchemy_probe/alchemy_probe`,
33 whatIfAnalysis: `arn:aws:forecast:${region}:${accountId}:what-if-analysis/alchemy_probe`,
34 whatIfForecast: `arn:aws:forecast:${region}:${accountId}:what-if-forecast/alchemy_probe`,
35 whatIfForecastExport: `arn:aws:forecast:${region}:${accountId}:what-if-forecast-export/alchemy_probe/alchemy_probe`,
36 };
37});
38
39// Amazon Forecast is closed to new customers: a grandfathered account
40// answers a nonexistent ARN with ResourceNotFoundException, while a blocked
41// account rejects the call with AccessDeniedException ("Amazon Forecast is
42// no longer available to new customers"). Both are typed tags decoded inside
43// the Lambda — an IAM gap or schema break would surface a different tag and
44// fail the assertion.
45const EXPECTED_PROBE_TAGS = [
46 "ResourceNotFoundException",
47 "AccessDeniedException",
48 "InvalidInputException",
49];
50
51const getJson = (path: string) =>
52 HttpClient.get(`${baseUrl}${path}`).pipe(
53 Effect.flatMap((response) =>
54 response.status >= 500
55 ? Effect.fail(new Error(`transient upstream ${response.status}`))
56 : Effect.succeed(response),
57 ),
58 Effect.retry({
59 schedule: Schedule.max([
60 Schedule.exponential("500 millis"),
61 Schedule.recurs(6),
62 ]),
63 }),
64 Effect.flatMap((r) => r.json),
65 );
66
67const probeTag = (path: string, arn: string) =>
68 getJson(`${path}?arn=${encodeURIComponent(arn)}`).pipe(
69 Effect.map((response) => (response as { tag: string }).tag),
70 );
71
72describe.sequential(
73 "Forecast Bindings",
74 {
75 tags: [
76 "provider:aws",
77 "provider:aws:forecast",
78 "provider:aws:lambda",
79 "live",
80 ],
81 },
82 () => {
83 beforeAll(
84 Effect.gen(function* () {
85 yield* Effect.logInfo(
86 "Forecast test setup: destroying previous resources",
87 );
88 yield* sharedStack.destroy();
89
90 yield* Effect.logInfo("Forecast test setup: deploying fixture");
91 const { functionUrl } = yield* sharedStack.deploy(
92 Effect.gen(function* () {
93 return yield* ForecastTestFunction;
94 }).pipe(Effect.provide(ForecastTestFunctionLive)),
95 );
96
97 expect(functionUrl).toBeTruthy();
98 baseUrl = functionUrl!.replace(/\/+$/, "");
99
100 const readinessUrl = `${baseUrl}/bindings`;
101 yield* HttpClient.get(readinessUrl).pipe(
102 Effect.flatMap((response) =>
103 response.status === 200
104 ? Effect.succeed(response)
105 : Effect.fail(
106 new Error(`Function not ready: ${response.status}`),
107 ),
108 ),
109 Effect.retry({ schedule: readinessPolicy }),
110 );
111 }),
112 { timeout: 240_000 },
113 );
114
115 afterAll(sharedStack.destroy(), { timeout: 120_000 });
116
117 describe("binding registration", () => {
118 test.provider("all twenty capabilities initialize in the runtime", () =>
119 Effect.gen(function* () {
120 const response = (yield* getJson("/bindings")) as { bound: string[] };
121 expect(response.bound).toHaveLength(20);
122 }),
123 );
124 });
125
126 describe("DescribeDatasetImportJob", () => {
127 test.provider("surfaces a typed tag for a nonexistent import job", () =>
128 Effect.gen(function* () {
129 const arns = yield* probeArns;
130 const tag = yield* probeTag("/import-probe", arns.importJob);
131 expect(EXPECTED_PROBE_TAGS).toContain(tag);
132 }),
133 );
134 });
135
136 describe("DescribeAutoPredictor", () => {
137 test.provider("surfaces a typed tag for a nonexistent predictor", () =>
138 Effect.gen(function* () {
139 const arns = yield* probeArns;
140 const tag = yield* probeTag("/predictor-probe", arns.predictor);
141 expect(EXPECTED_PROBE_TAGS).toContain(tag);
142 }),
143 );
144 });
145
146 describe("GetAccuracyMetrics", () => {
147 test.provider("surfaces a typed tag for a nonexistent predictor", () =>
148 Effect.gen(function* () {
149 const arns = yield* probeArns;
150 const tag = yield* probeTag("/accuracy-probe", arns.predictor);
151 expect(EXPECTED_PROBE_TAGS).toContain(tag);
152 }),
153 );
154 });
155
156 describe("DescribeForecast", () => {
157 test.provider("surfaces a typed tag for a nonexistent forecast", () =>
158 Effect.gen(function* () {
159 const arns = yield* probeArns;
160 const tag = yield* probeTag("/forecast-probe", arns.forecast);
161 expect(EXPECTED_PROBE_TAGS).toContain(tag);
162 }),
163 );
164 });
165
166 describe("CreateForecast", () => {
167 test.provider("rejects a nonexistent predictor with a typed tag", () =>
168 Effect.gen(function* () {
169 const arns = yield* probeArns;
170 const tag = yield* probeTag("/create-forecast-probe", arns.predictor);
171 expect(EXPECTED_PROBE_TAGS).toContain(tag);
172 }),
173 );
174 });
175
176 describe("StopResource", () => {
177 test.provider("surfaces a typed tag for a nonexistent job", () =>
178 Effect.gen(function* () {
179 const arns = yield* probeArns;
180 const tag = yield* probeTag("/stop-probe", arns.predictor);
181 expect(EXPECTED_PROBE_TAGS).toContain(tag);
182 }),
183 );
184 });
185
186 describe("ResumeResource", () => {
187 test.provider("surfaces a typed tag for a nonexistent monitor", () =>
188 Effect.gen(function* () {
189 const arns = yield* probeArns;
190 const tag = yield* probeTag("/resume-probe", arns.predictor);
191 expect(EXPECTED_PROBE_TAGS).toContain(tag);
192 }),
193 );
194 });
195
196 describe("QueryForecast", () => {
197 test.provider("surfaces a typed tag for a nonexistent forecast", () =>
198 Effect.gen(function* () {
199 const arns = yield* probeArns;
200 const tag = yield* probeTag("/query-probe", arns.forecast);
201 expect(EXPECTED_PROBE_TAGS).toContain(tag);
202 }),
203 );
204 });
205
206 describe("QueryWhatIfForecast", () => {
207 test.provider("surfaces a typed tag for a nonexistent forecast", () =>
208 Effect.gen(function* () {
209 const arns = yield* probeArns;
210 const tag = yield* probeTag(
211 "/whatif-query-probe",
212 arns.whatIfForecast,
213 );
214 expect(EXPECTED_PROBE_TAGS).toContain(tag);
215 }),
216 );
217 });
218
219 describe("CreateForecastExportJob", () => {
220 test.provider("rejects a nonexistent forecast with a typed tag", () =>
221 Effect.gen(function* () {
222 const arns = yield* probeArns;
223 const tag = yield* probeTag("/export-create-probe", arns.forecast);
224 expect(EXPECTED_PROBE_TAGS).toContain(tag);
225 }),
226 );
227 });
228
229 describe("DescribeForecastExportJob", () => {
230 test.provider("surfaces a typed tag for a nonexistent export job", () =>
231 Effect.gen(function* () {
232 const arns = yield* probeArns;
233 const tag = yield* probeTag("/export-probe", arns.forecastExport);
234 expect(EXPECTED_PROBE_TAGS).toContain(tag);
235 }),
236 );
237 });
238
239 describe("CreateWhatIfAnalysis", () => {
240 test.provider("rejects a nonexistent forecast with a typed tag", () =>
241 Effect.gen(function* () {
242 const arns = yield* probeArns;
243 const tag = yield* probeTag(
244 "/whatif-analysis-create-probe",
245 arns.forecast,
246 );
247 expect(EXPECTED_PROBE_TAGS).toContain(tag);
248 }),
249 );
250 });
251
252 describe("DescribeWhatIfAnalysis", () => {
253 test.provider("surfaces a typed tag for a nonexistent analysis", () =>
254 Effect.gen(function* () {
255 const arns = yield* probeArns;
256 const tag = yield* probeTag(
257 "/whatif-analysis-probe",
258 arns.whatIfAnalysis,
259 );
260 expect(EXPECTED_PROBE_TAGS).toContain(tag);
261 }),
262 );
263 });
264
265 describe("CreateWhatIfForecast", () => {
266 test.provider("rejects a nonexistent analysis with a typed tag", () =>
267 Effect.gen(function* () {
268 const arns = yield* probeArns;
269 const tag = yield* probeTag(
270 "/whatif-create-probe",
271 arns.whatIfAnalysis,
272 );
273 expect(EXPECTED_PROBE_TAGS).toContain(tag);
274 }),
275 );
276 });
277
278 describe("DescribeWhatIfForecast", () => {
279 test.provider(
280 "surfaces a typed tag for a nonexistent scenario forecast",
281 () =>
282 Effect.gen(function* () {
283 const arns = yield* probeArns;
284 const tag = yield* probeTag(
285 "/whatif-forecast-probe",
286 arns.whatIfForecast,
287 );
288 expect(EXPECTED_PROBE_TAGS).toContain(tag);
289 }),
290 );
291 });
292
293 describe("CreateWhatIfForecastExport", () => {
294 test.provider(
295 "rejects a nonexistent scenario forecast with a typed tag",
296 () =>
297 Effect.gen(function* () {
298 const arns = yield* probeArns;
299 const tag = yield* probeTag(
300 "/whatif-export-create-probe",
301 arns.whatIfForecast,
302 );
303 expect(EXPECTED_PROBE_TAGS).toContain(tag);
304 }),
305 );
306 });
307
308 describe("DescribeWhatIfForecastExport", () => {
309 test.provider("surfaces a typed tag for a nonexistent export", () =>
310 Effect.gen(function* () {
311 const arns = yield* probeArns;
312 const tag = yield* probeTag(
313 "/whatif-export-probe",
314 arns.whatIfForecastExport,
315 );
316 expect(EXPECTED_PROBE_TAGS).toContain(tag);
317 }),
318 );
319 });
320
321 describe("DeleteResourceTree", () => {
322 test.provider("surfaces a typed tag for a nonexistent parent", () =>
323 Effect.gen(function* () {
324 const arns = yield* probeArns;
325 const tag = yield* probeTag("/delete-tree-probe", arns.predictor);
326 expect(EXPECTED_PROBE_TAGS).toContain(tag);
327 }),
328 );
329 });
330 },
331);