1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as HttpClient from "effect/http/HttpClient";
8import SageMakerEndpointTestFunctionLive, {
9 SageMakerEndpointTestFunction,
10} from "./endpoint-handler";
11
12// A live endpoint deploys real serving capacity (~3-10 min to InService) and
13// requires a working inference container. Same gate as Endpoint.test.ts:
14// AWS_TEST_SAGEMAKER_ENDPOINT=1
15// AWS_TEST_SAGEMAKER_IMAGE=<ECR URI of a serving container>
16const gated =
17 !process.env.AWS_TEST_SAGEMAKER_ENDPOINT ||
18 !process.env.AWS_TEST_SAGEMAKER_IMAGE;
19
20const testOptions = { providers: AWS.providers() };
21const { test, beforeAll, afterAll } = Test.make(testOptions);
22const sharedStack = Core.scratchStack(testOptions, "SageMakerEndpointBindings");
23
24let baseUrl: string;
25
26describe(
27 "SageMaker Endpoint Bindings",
28 {
29 tags: [
30 "provider:aws",
31 "provider:aws:iam",
32 "provider:aws:lambda",
33 "provider:aws:sagemaker",
34 "live",
35 ],
36 },
37 () => {
38 beforeAll(
39 Effect.gen(function* () {
40 if (gated) return;
41
42 yield* sharedStack.destroy();
43 const attrs = yield* sharedStack.deploy(
44 Effect.gen(function* () {
45 return yield* SageMakerEndpointTestFunction;
46 }).pipe(Effect.provide(SageMakerEndpointTestFunctionLive)),
47 );
48
49 expect(attrs.functionUrl).toBeTruthy();
50 baseUrl = attrs.functionUrl!.replace(/\/+$/, "");
51
52 yield* HttpClient.get(`${baseUrl}/health`).pipe(
53 Effect.flatMap((response) =>
54 response.status === 200
55 ? Effect.succeed(response)
56 : Effect.fail(
57 new Error(`Function not ready: ${response.status}`),
58 ),
59 ),
60 Effect.retry({
61 schedule: Schedule.max([
62 Schedule.fixed("2 seconds"),
63 Schedule.recurs(75),
64 ]),
65 }),
66 );
67 }),
68 { timeout: 1_500_000 },
69 );
70
71 afterAll.skipIf(gated)(sharedStack.destroy(), { timeout: 1_200_000 });
72
73 describe("DescribeEndpoint", () => {
74 test.provider.skipIf(gated)(
75 "reads the bound endpoint's live status (injected endpoint name)",
76 () =>
77 Effect.gen(function* () {
78 const client = yield* HttpClient.HttpClient;
79 const response = yield* client.get(`${baseUrl}/describe-endpoint`);
80 expect(response.status).toBe(200);
81 const body = (yield* response.json) as {
82 endpointName: string;
83 status: string;
84 variants: string[];
85 };
86 expect(body.status).toBe("InService");
87 expect(body.variants).toContain("AllTraffic");
88 }),
89 { timeout: 120_000 },
90 );
91 });
92
93 describe("UpdateEndpointWeightsAndCapacities", () => {
94 test.provider.skipIf(gated)(
95 "the binding registers on the runtime (IAM grant proven by deploy)",
96 () =>
97 Effect.gen(function* () {
98 const client = yield* HttpClient.HttpClient;
99 const response = yield* client.get(`${baseUrl}/bindings`);
100 const body = (yield* response.json) as { bound: string[] };
101 expect(body.bound).toContain("updateWeights");
102 }),
103 { timeout: 60_000 },
104 );
105 });
106 },
107);