1import * as AWS from "@/AWS";
2import { CollectionGroup } from "@/AWS/OpenSearchServerless";
3import * as Test from "@/Test/Alchemy";
4import * as aoss from "@distilled.cloud/aws/opensearchserverless";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11const GROUP_NAME = "alchemy-test-aoss-group";
12
13const observe = (name: string) =>
14 aoss
15 .batchGetCollectionGroup({ names: [name] })
16 .pipe(Effect.map((r) => r.collectionGroupDetails?.[0]));
17
18const assertGone = (name: string) =>
19 observe(name).pipe(
20 Effect.flatMap((detail) =>
21 detail === undefined
22 ? Effect.void
23 : Effect.fail(new Error(`collection group ${name} still exists`)),
24 ),
25 Effect.retry({
26 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(5)]),
27 }),
28 );
29
30// An empty collection group is free (capacity limits only — no OCUs are
31// provisioned until collections are placed in it), so the full lifecycle
32// runs ungated: create, no-op, update description, destroy, verify gone.
33test.provider(
34 "collection group lifecycle: create, no-op, update, destroy, verify gone",
35 (stack) =>
36 Effect.gen(function* () {
37 // Clean slate in case a previous run died mid-flight.
38 yield* stack.destroy();
39
40 const deployGroup = (description?: string) =>
41 stack.deploy(
42 Effect.gen(function* () {
43 const group = yield* CollectionGroup("Group", {
44 groupName: GROUP_NAME,
45 standbyReplicas: "DISABLED",
46 description,
47 tags: { purpose: "alchemy-test" },
48 });
49 return { group };
50 }),
51 );
52
53 // Create.
54 const created = yield* deployGroup("alchemy test group");
55 expect(created.group.collectionGroupName).toBe(GROUP_NAME);
56 expect(created.group.collectionGroupArn).toContain(":aoss:");
57 expect(created.group.collectionGroupId).toBeDefined();
58 expect(created.group.standbyReplicas).toBe("DISABLED");
59
60 // Out-of-band verification via distilled (including internal tags —
61 // batchGetCollectionGroup does not return tags, so read them via
62 // listTagsForResource).
63 const observed = yield* observe(GROUP_NAME);
64 expect(observed?.name).toBe(GROUP_NAME);
65 const observedTags = yield* aoss
66 .listTagsForResource({
67 resourceArn: created.group.collectionGroupArn,
68 })
69 .pipe(Effect.map((r) => (r.tags ?? []).map((t) => t.key)));
70 expect(observedTags).toContain("purpose");
71 expect(observedTags.some((k) => k.startsWith("alchemy::"))).toBe(true);
72
73 // No-op redeploy.
74 const noop = yield* deployGroup("alchemy test group");
75 expect(noop.group.collectionGroupId).toBe(
76 created.group.collectionGroupId,
77 );
78
79 // Update the description.
80 yield* deployGroup("alchemy test group v2");
81 const observedUpdated = yield* observe(GROUP_NAME);
82 expect(observedUpdated?.description).toBe("alchemy test group v2");
83
84 // Destroy and verify deletion out-of-band.
85 yield* stack.destroy();
86 yield* assertGone(GROUP_NAME);
87 }),
88 {
89 tags: ["provider:aws", "provider:aws:opensearchserverless", "live"],
90 timeout: 180_000,
91 },
92);