1import * as AWS from "@/AWS";
2import { FeatureGroup } 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: FeatureGroup APIs return the model's own typed
11// ResourceNotFound (no patch needed) — prove it stays that way.
12test.provider(
13 "describeFeatureGroup on a nonexistent group fails with ResourceNotFound",
14 () =>
15 Effect.gen(function* () {
16 const error = yield* Effect.flip(
17 sagemaker.describeFeatureGroup({
18 FeatureGroupName: "alchemy-nonexistent-feature-group-probe",
19 }),
20 );
21 expect(error._tag).toBe("ResourceNotFound");
22 }),
23 { tags: ["provider:aws", "provider:aws:sagemaker", "live"] },
24);
25
26const findFeatureGroup = (name: string) =>
27 sagemaker
28 .describeFeatureGroup({ FeatureGroupName: name })
29 .pipe(Effect.catchTag("ResourceNotFound", () => Effect.succeed(undefined)));
30
31test.provider(
32 "create online-store feature group, verify out-of-band, destroy",
33 (stack) =>
34 Effect.gen(function* () {
35 yield* stack.destroy();
36
37 const { featureGroup } = yield* stack.deploy(
38 Effect.gen(function* () {
39 const featureGroup = yield* FeatureGroup("TestFeatures", {
40 recordIdentifierFeatureName: "user_id",
41 eventTimeFeatureName: "event_time",
42 featureDefinitions: [
43 { FeatureName: "user_id", FeatureType: "String" },
44 { FeatureName: "event_time", FeatureType: "String" },
45 { FeatureName: "clicks", FeatureType: "Integral" },
46 ],
47 onlineStoreConfig: { EnableOnlineStore: true },
48 tags: { purpose: "alchemy-test" },
49 });
50 return { featureGroup };
51 }),
52 );
53
54 expect(featureGroup.featureGroupName).toBeDefined();
55 expect(featureGroup.featureGroupArn).toContain(":feature-group/");
56 expect(featureGroup.recordIdentifierFeatureName).toBe("user_id");
57 expect(featureGroup.eventTimeFeatureName).toBe("event_time");
58
59 // out-of-band verification via distilled — reconcile waits for Created
60 const observed = yield* findFeatureGroup(featureGroup.featureGroupName);
61 expect(observed?.FeatureGroupStatus).toBe("Created");
62 expect(observed?.OnlineStoreConfig?.EnableOnlineStore).toBe(true);
63 expect(observed?.FeatureDefinitions?.length).toBe(3);
64
65 // internal ownership tags applied
66 const tags = yield* sagemaker
67 .listTags({ ResourceArn: featureGroup.featureGroupArn })
68 .pipe(Effect.map((r) => r.Tags ?? []));
69 const tagMap = Object.fromEntries(tags.map((t) => [t.Key, t.Value]));
70 expect(tagMap["alchemy::id"]).toBe("TestFeatures");
71 expect(tagMap.purpose).toBe("alchemy-test");
72
73 // delete waits until the group is fully gone
74 yield* stack.destroy();
75 expect(
76 yield* findFeatureGroup(featureGroup.featureGroupName),
77 ).toBeUndefined();
78 }),
79 {
80 tags: ["provider:aws", "provider:aws:sagemaker", "live"],
81 timeout: 240_000,
82 },
83);