1import * as AWS from "@/AWS";
2import { ComputeQuota } 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 describeComputeQuota returns the typed
11// ResourceNotFound for a nonexistent quota id.
12test.provider(
13 "describeComputeQuota on a nonexistent id fails with ResourceNotFound",
14 () =>
15 Effect.gen(function* () {
16 const error = yield* Effect.flip(
17 sagemaker.describeComputeQuota({
18 ComputeQuotaId: "abcdef012345",
19 }),
20 );
21 expect(error._tag).toBe("ResourceNotFound");
22 }),
23 { tags: ["provider:aws", "provider:aws:sagemaker", "live"] },
24);
25
26const findQuota = (quotaId: string) =>
27 sagemaker
28 .describeComputeQuota({ ComputeQuotaId: quotaId })
29 .pipe(Effect.catchTag("ResourceNotFound", () => Effect.succeed(undefined)));
30
31// Compute quotas 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>
35test.provider.skipIf(!process.env.AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN)(
36 "create compute quota, update it, destroy",
37 (stack) =>
38 Effect.gen(function* () {
39 yield* stack.destroy();
40
41 const clusterArn =
42 process.env.AWS_TEST_SAGEMAKER_HYPERPOD_EKS_CLUSTER_ARN!;
43
44 const { quota } = yield* stack.deploy(
45 Effect.gen(function* () {
46 const quota = yield* ComputeQuota("TestQuota", {
47 clusterArn,
48 computeQuotaTarget: {
49 TeamName: "alchemy-research",
50 FairShareWeight: 10,
51 },
52 computeQuotaConfig: {
53 ComputeQuotaResources: [
54 { InstanceType: "ml.t3.medium", Count: 1 },
55 ],
56 },
57 activationState: "Enabled",
58 tags: { purpose: "alchemy-test" },
59 });
60 return { quota };
61 }),
62 );
63
64 expect(quota.computeQuotaArn).toContain(":compute-quota/");
65 expect(quota.clusterArn).toBe(clusterArn);
66
67 // Out-of-band verification via distilled.
68 const described = yield* findQuota(quota.computeQuotaId);
69 expect(described?.Status).toBe("Created");
70 expect(described?.ComputeQuotaTarget?.TeamName).toBe("alchemy-research");
71
72 // Update in place — the id must remain stable and the version bump.
73 const { quota: updated } = yield* stack.deploy(
74 Effect.gen(function* () {
75 const quota = yield* ComputeQuota("TestQuota", {
76 clusterArn,
77 computeQuotaTarget: {
78 TeamName: "alchemy-research",
79 FairShareWeight: 20,
80 },
81 computeQuotaConfig: {
82 ComputeQuotaResources: [
83 { InstanceType: "ml.t3.medium", Count: 2 },
84 ],
85 },
86 activationState: "Enabled",
87 tags: { purpose: "alchemy-test" },
88 });
89 return { quota };
90 }),
91 );
92 expect(updated.computeQuotaId).toBe(quota.computeQuotaId);
93 expect(updated.computeQuotaVersion).toBeGreaterThan(
94 quota.computeQuotaVersion,
95 );
96
97 // Destroy and verify gone.
98 yield* stack.destroy();
99 const gone = yield* findQuota(quota.computeQuotaId);
100 expect(gone === undefined || gone.Status === "Deleted").toBe(true);
101 }),
102 {
103 tags: ["provider:aws", "provider:aws:sagemaker", "live"],
104 timeout: 600_000,
105 },
106);