1import * as AWS from "@/AWS";
2import { ClusterSchedulerConfig } from "@/AWS/SageMaker";
3import * as Test from "@/Test/Alchemy";
4import * as sagemaker from "@distilled.cloud/aws/sagemaker";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7
8const { test } = Test.make({ providers: AWS.providers() });
9
10// Ungated typed-error probe: prove describeClusterSchedulerConfig returns
11// the typed ResourceNotFound for a nonexistent policy id.
12test.provider(
13 "describeClusterSchedulerConfig on a nonexistent id fails with ResourceNotFound",
14 () =>
15 Effect.gen(function* () {
16 const error = yield* Effect.flip(
17 sagemaker.describeClusterSchedulerConfig({
18 ClusterSchedulerConfigId: "abcdef012345",
19 }),
20 );
21 expect(error._tag).toBe("ResourceNotFound");
22 }),
23 { tags: ["provider:aws", "provider:aws:sagemaker", "live"] },
24);
25
26const findConfig = (configId: string) =>
27 sagemaker
28 .describeClusterSchedulerConfig({ ClusterSchedulerConfigId: configId })
29 .pipe(Effect.catchTag("ResourceNotFound", () => Effect.succeed(undefined)));
30
31// Cluster policies require an EKS-orchestrated HyperPod cluster, which takes
32// ~30 minutes and real ML + EKS capacity to stand up. Gated behind an
33// existing cluster:
34// AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN=<arn of an EKS HyperPod cluster>
35//
36// AWS allows ONE cluster policy per cluster. When the provided cluster
37// already carries one (e.g. the examples/aws-hyperpod stack's), this test
38// verifies the typed conflict instead of the full lifecycle.
39test.provider.skipIf(!process.env.AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN)(
40 "create cluster policy, update it, destroy",
41 (stack) =>
42 Effect.gen(function* () {
43 yield* stack.destroy();
44
45 const clusterArn =
46 process.env.AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN!;
47
48 const existing = yield* sagemaker.listClusterSchedulerConfigs({
49 ClusterArn: clusterArn,
50 });
51 if (
52 (existing.ClusterSchedulerConfigSummaries ?? []).some(
53 (s) => s.Status !== "Deleted",
54 )
55 ) {
56 // One policy per cluster: creating a second must fail with the
57 // typed ClusterSchedulerConfigAlreadyExists tag.
58 const error = yield* Effect.flip(
59 sagemaker.createClusterSchedulerConfig({
60 Name: "alchemy-conflict-probe",
61 ClusterArn: clusterArn,
62 SchedulerConfig: { FairShare: "Enabled" },
63 }),
64 );
65 expect(error._tag).toBe("ClusterSchedulerConfigAlreadyExists");
66 return;
67 }
68
69 const { policy } = yield* stack.deploy(
70 Effect.gen(function* () {
71 const policy = yield* ClusterSchedulerConfig("TestPolicy", {
72 clusterArn,
73 schedulerConfig: {
74 PriorityClasses: [{ Name: "training", Weight: 75 }],
75 FairShare: "Enabled",
76 },
77 description: "alchemy test policy",
78 tags: { purpose: "alchemy-test" },
79 });
80 return { policy };
81 }),
82 );
83
84 expect(policy.clusterSchedulerConfigArn).toContain(
85 ":cluster-scheduler-config/",
86 );
87 expect(policy.clusterArn).toBe(clusterArn);
88
89 // Out-of-band verification via distilled.
90 const described = yield* findConfig(policy.clusterSchedulerConfigId);
91 expect(described?.Status).toBe("Created");
92 expect(described?.SchedulerConfig?.PriorityClasses).toEqual([
93 { Name: "training", Weight: 75 },
94 ]);
95
96 // Update in place — the id must remain stable and the version bump.
97 const { policy: updated } = yield* stack.deploy(
98 Effect.gen(function* () {
99 const policy = yield* ClusterSchedulerConfig("TestPolicy", {
100 clusterArn,
101 schedulerConfig: {
102 PriorityClasses: [
103 { Name: "inference", Weight: 100 },
104 { Name: "training", Weight: 75 },
105 ],
106 FairShare: "Enabled",
107 },
108 description: "alchemy test policy v2",
109 tags: { purpose: "alchemy-test" },
110 });
111 return { policy };
112 }),
113 );
114 expect(updated.clusterSchedulerConfigId).toBe(
115 policy.clusterSchedulerConfigId,
116 );
117 expect(updated.clusterSchedulerConfigVersion).toBeGreaterThan(
118 policy.clusterSchedulerConfigVersion,
119 );
120
121 // Destroy and verify gone.
122 yield* stack.destroy();
123 const gone = yield* findConfig(policy.clusterSchedulerConfigId);
124 expect(gone === undefined || gone.Status === "Deleted").toBe(true);
125 }),
126 {
127 tags: ["provider:aws", "provider:aws:sagemaker", "live"],
128 timeout: 600_000,
129 },
130);