1import * as AWS from "@/AWS";
2import { AWSEnvironment } from "@/AWS/Environment.ts";
3import { Role } from "@/AWS/IAM/Role.ts";
4import { EndpointConfig, Model } from "@/AWS/SageMaker";
5import * as Test from "@/Test/Alchemy";
6import * as sagemaker from "@distilled.cloud/aws/sagemaker";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import { sklearnImage } from "./images.ts";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// Ungated typed-error probe: prove the distilled patch carves
14// EndpointConfigNotFound out of the overloaded ValidationException.
15test.provider(
16 "describeEndpointConfig on a nonexistent config fails with EndpointConfigNotFound",
17 () =>
18 Effect.gen(function* () {
19 const error = yield* Effect.flip(
20 sagemaker.describeEndpointConfig({
21 EndpointConfigName: "alchemy-nonexistent-sagemaker-config-probe",
22 }),
23 );
24 expect(error._tag).toBe("EndpointConfigNotFound");
25 }),
26 { tags: ["provider:aws", "provider:aws:sagemaker", "live"] },
27);
28
29const findConfig = (name: string) =>
30 sagemaker
31 .describeEndpointConfig({ EndpointConfigName: name })
32 .pipe(
33 Effect.catchTag("EndpointConfigNotFound", () =>
34 Effect.succeed(undefined),
35 ),
36 );
37
38test.provider(
39 "create serverless endpoint config, verify out-of-band, destroy",
40 (stack) =>
41 Effect.gen(function* () {
42 yield* stack.destroy();
43
44 const { region } = yield* AWSEnvironment.current;
45 const image = sklearnImage(region);
46
47 const { model, config } = yield* stack.deploy(
48 Effect.gen(function* () {
49 const role = yield* Role("SageMakerConfigRole", {
50 assumeRolePolicyDocument: {
51 Version: "2012-10-17",
52 Statement: [
53 {
54 Effect: "Allow",
55 Principal: { Service: "sagemaker.amazonaws.com" },
56 Action: ["sts:AssumeRole"],
57 },
58 ],
59 },
60 managedPolicyArns: [
61 "arn:aws:iam::aws:policy/AmazonEC2ContainerRegistryReadOnly",
62 ],
63 });
64 const model = yield* Model("ConfigTestModel", {
65 executionRoleArn: role.roleArn,
66 primaryContainer: { Image: image },
67 });
68 const config = yield* EndpointConfig("TestConfig", {
69 productionVariants: [
70 {
71 VariantName: "AllTraffic",
72 ModelName: model.modelName,
73 ServerlessConfig: { MemorySizeInMB: 1024, MaxConcurrency: 1 },
74 },
75 ],
76 tags: { purpose: "alchemy-test" },
77 });
78 return { model, config };
79 }),
80 );
81
82 expect(config.endpointConfigName).toBeDefined();
83 expect(config.endpointConfigArn).toContain(":endpoint-config/");
84
85 // out-of-band verification via distilled
86 const observed = yield* findConfig(config.endpointConfigName);
87 expect(observed?.EndpointConfigArn).toBe(config.endpointConfigArn);
88 const variant = observed?.ProductionVariants?.[0];
89 expect(variant?.ModelName).toBe(model.modelName);
90 expect(variant?.ServerlessConfig?.MemorySizeInMB).toBe(1024);
91
92 // internal ownership tags applied
93 const tags = yield* sagemaker
94 .listTags({ ResourceArn: config.endpointConfigArn })
95 .pipe(Effect.map((r) => r.Tags ?? []));
96 const tagMap = Object.fromEntries(tags.map((t) => [t.Key, t.Value]));
97 expect(tagMap["alchemy::id"]).toBe("TestConfig");
98 expect(tagMap.purpose).toBe("alchemy-test");
99
100 yield* stack.destroy();
101 expect(yield* findConfig(config.endpointConfigName)).toBeUndefined();
102 }),
103 {
104 tags: [
105 "provider:aws",
106 "provider:aws:iam",
107 "provider:aws:sagemaker",
108 "live",
109 ],
110 timeout: 240_000,
111 },
112);