1import * as AWS from "@/AWS";
2import { Role } from "@/AWS/IAM/Role.ts";
3import { Endpoint, EndpointConfig, Model } from "@/AWS/SageMaker";
4import * as Test from "@/Test/Alchemy";
5import * as sagemaker from "@distilled.cloud/aws/sagemaker";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe: prove the distilled patch carves EndpointNotFound
12// out of the overloaded ValidationException. Runs in every CI pass.
13test.provider(
14 "describeEndpoint on a nonexistent endpoint fails with EndpointNotFound",
15 () =>
16 Effect.gen(function* () {
17 const error = yield* Effect.flip(
18 sagemaker.describeEndpoint({
19 EndpointName: "alchemy-nonexistent-sagemaker-endpoint-probe",
20 }),
21 );
22 expect(error._tag).toBe("EndpointNotFound");
23 }),
24 { tags: ["provider:aws", "provider:aws:sagemaker", "live"] },
25);
26
27const findEndpoint = (name: string) =>
28 sagemaker
29 .describeEndpoint({ EndpointName: name })
30 .pipe(Effect.catchTag("EndpointNotFound", () => Effect.succeed(undefined)));
31
32// A live endpoint deploys real serving capacity: it takes ~3-10 minutes to
33// reach InService and REQUIRES a working inference container. Gated behind:
34// AWS_TEST_SAGEMAKER_ENDPOINT=1
35// AWS_TEST_SAGEMAKER_IMAGE=<ECR URI of a serving container>
36// AWS_TEST_SAGEMAKER_MODEL_DATA=<s3://... model.tar.gz> (optional)
37test.provider.skipIf(
38 !process.env.AWS_TEST_SAGEMAKER_ENDPOINT ||
39 !process.env.AWS_TEST_SAGEMAKER_IMAGE,
40)(
41 "create serverless endpoint, wait InService, destroy",
42 (stack) =>
43 Effect.gen(function* () {
44 yield* stack.destroy();
45
46 const image = process.env.AWS_TEST_SAGEMAKER_IMAGE!;
47 const modelData = process.env.AWS_TEST_SAGEMAKER_MODEL_DATA;
48
49 const { endpoint } = yield* stack.deploy(
50 Effect.gen(function* () {
51 const role = yield* Role("SageMakerEndpointRole", {
52 assumeRolePolicyDocument: {
53 Version: "2012-10-17",
54 Statement: [
55 {
56 Effect: "Allow",
57 Principal: { Service: "sagemaker.amazonaws.com" },
58 Action: ["sts:AssumeRole"],
59 },
60 ],
61 },
62 managedPolicyArns: [
63 "arn:aws:iam::aws:policy/AmazonSageMakerFullAccess",
64 ],
65 });
66 const model = yield* Model("EndpointTestModel", {
67 executionRoleArn: role.roleArn,
68 primaryContainer: {
69 Image: image,
70 ...(modelData ? { ModelDataUrl: modelData } : {}),
71 },
72 });
73 const config = yield* EndpointConfig("EndpointTestConfig", {
74 productionVariants: [
75 {
76 VariantName: "AllTraffic",
77 ModelName: model.modelName,
78 ServerlessConfig: { MemorySizeInMB: 2048, MaxConcurrency: 1 },
79 },
80 ],
81 });
82 const endpoint = yield* Endpoint("TestEndpoint", {
83 endpointConfigName: config.endpointConfigName,
84 });
85 return { endpoint };
86 }),
87 );
88
89 expect(endpoint.endpointArn).toContain(":endpoint/");
90 expect(endpoint.endpointStatus).toBe("InService");
91
92 // out-of-band verification via distilled
93 const observed = yield* findEndpoint(endpoint.endpointName);
94 expect(observed?.EndpointStatus).toBe("InService");
95
96 // delete waits until the endpoint is fully gone
97 yield* stack.destroy();
98 expect(yield* findEndpoint(endpoint.endpointName)).toBeUndefined();
99 }),
100 {
101 tags: [
102 "provider:aws",
103 "provider:aws:iam",
104 "provider:aws:sagemaker",
105 "live",
106 ],
107 timeout: 1_500_000,
108 },
109);