1import * as AWS from "@/AWS";
2import { LifecyclePolicy } 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 POLICY_NAME = "alchemy-test-retention";
12
13const retentionPolicy = (retention: string) => ({
14 Rules: [
15 {
16 ResourceType: "index",
17 Resource: ["index/alchemy-lp-test/*"],
18 MinIndexRetention: retention,
19 },
20 ],
21});
22
23const observe = (name: string) =>
24 aoss
25 .batchGetLifecyclePolicy({ identifiers: [{ type: "retention", name }] })
26 .pipe(Effect.map((r) => r.lifecyclePolicyDetails?.[0]));
27
28const assertGone = (name: string) =>
29 observe(name).pipe(
30 Effect.flatMap((detail) =>
31 detail === undefined
32 ? Effect.void
33 : Effect.fail(new Error(`lifecycle policy ${name} still exists`)),
34 ),
35 Effect.retry({
36 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(5)]),
37 }),
38 );
39
40// Lifecycle policies are free and provision instantly, so the full lifecycle
41// runs ungated: create, no-op, update (policy version bump), destroy, verify
42// gone — all out-of-band verified via distilled.
43test.provider(
44 "lifecycle policy lifecycle: create, no-op, update, destroy, verify gone",
45 (stack) =>
46 Effect.gen(function* () {
47 // Clean slate in case a previous run died mid-flight.
48 yield* stack.destroy();
49
50 const deployPolicy = (retention: string, description?: string) =>
51 stack.deploy(
52 Effect.gen(function* () {
53 const policy = yield* LifecyclePolicy("Retention", {
54 policyName: POLICY_NAME,
55 policy: retentionPolicy(retention),
56 description,
57 });
58 return { policy };
59 }),
60 );
61
62 // Create.
63 const created = yield* deployPolicy("24h", "alchemy test retention");
64 expect(created.policy.policyName).toBe(POLICY_NAME);
65 expect(created.policy.type).toBe("retention");
66 const initialVersion = created.policy.policyVersion;
67
68 // Out-of-band verification via distilled.
69 const observed = yield* observe(POLICY_NAME);
70 expect(observed?.name).toBe(POLICY_NAME);
71 expect(JSON.stringify(observed?.policy)).toContain("24h");
72
73 // No-op redeploy: version must not change.
74 const noop = yield* deployPolicy("24h", "alchemy test retention");
75 expect(noop.policy.policyVersion).toBe(initialVersion);
76
77 // Update the retention window → new policy version.
78 const updated = yield* deployPolicy("48h", "alchemy test retention");
79 expect(updated.policy.policyVersion).not.toBe(initialVersion);
80 const observedUpdated = yield* observe(POLICY_NAME);
81 expect(JSON.stringify(observedUpdated?.policy)).toContain("48h");
82
83 // Destroy and verify deletion out-of-band.
84 yield* stack.destroy();
85 yield* assertGone(POLICY_NAME);
86 }),
87 {
88 tags: ["provider:aws", "provider:aws:opensearchserverless", "live"],
89 timeout: 120_000,
90 },
91);