1import * as AWS from "@/AWS";
2import { AnomalyMonitor } from "@/AWS/CostExplorer/AnomalyMonitor.ts";
3import { AnomalySubscription } from "@/AWS/CostExplorer/AnomalySubscription.ts";
4import * as Test from "@/Test/Alchemy";
5import { Region as AwsRegion } from "@distilled.cloud/aws/Region";
6import * as ce from "@distilled.cloud/aws/cost-explorer";
7import { expect } from "alchemy-test";
8import * as Effect from "effect/Effect";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: AWS.providers() });
12
13// Cost Explorer is served exclusively from us-east-1.
14const pin = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
15 effect.pipe(Effect.provideService(AwsRegion, Effect.succeed("us-east-1")));
16
17const subscriptionName = "alchemy-test-anomaly-subscription";
18
19const getSubscription = (subscriptionArn: string) =>
20 pin(
21 ce.getAnomalySubscriptions({ SubscriptionArnList: [subscriptionArn] }),
22 ).pipe(
23 Effect.map((r) => r.AnomalySubscriptions[0]),
24 Effect.catchTag("UnknownSubscriptionException", () =>
25 Effect.succeed(undefined),
26 ),
27 );
28
29// Typed wait-until-gone on delete.
30const assertSubscriptionGone = (subscriptionArn: string) =>
31 Effect.gen(function* () {
32 const found = yield* getSubscription(subscriptionArn);
33 if (found !== undefined) {
34 return yield* Effect.fail(
35 new Error(`subscription '${subscriptionArn}' still exists`),
36 );
37 }
38 }).pipe(
39 Effect.retry({
40 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(5)]),
41 }),
42 );
43
44const makeStack = (frequency: "DAILY" | "WEEKLY", impactThreshold: string) =>
45 Effect.gen(function* () {
46 const monitor = yield* AnomalyMonitor("SubMonitor", {
47 monitorName: "alchemy-test-subscription-monitor",
48 monitorType: "CUSTOM",
49 monitorSpecification: {
50 Tags: { Key: "CostCenter", Values: ["alchemy-test-sub"] },
51 },
52 });
53 const subscription = yield* AnomalySubscription("Subscription", {
54 subscriptionName,
55 monitorArnList: [monitor.monitorArn],
56 frequency,
57 subscribers: [{ type: "EMAIL", address: "anomaly-test@example.com" }],
58 thresholdExpression: {
59 Dimensions: {
60 Key: "ANOMALY_TOTAL_IMPACT_ABSOLUTE",
61 MatchOptions: ["GREATER_THAN_OR_EQUAL"],
62 Values: [impactThreshold],
63 },
64 },
65 tags: { fixture: "cost-explorer-anomaly-subscription" },
66 });
67 return {
68 monitorArn: monitor.monitorArn,
69 subscriptionArn: subscription.subscriptionArn,
70 };
71 });
72
73test.provider(
74 "lifecycle: create subscription on a monitor, update frequency + threshold, destroy",
75 (stack) =>
76 Effect.gen(function* () {
77 yield* stack.destroy();
78
79 const deployed = yield* stack.deploy(makeStack("DAILY", "100"));
80
81 // Out-of-band verification via distilled.
82 const created = yield* getSubscription(deployed.subscriptionArn);
83 expect(created?.SubscriptionName).toBe(subscriptionName);
84 expect(created?.Frequency).toBe("DAILY");
85 expect(created?.MonitorArnList).toEqual([deployed.monitorArn]);
86 expect(created?.Subscribers[0]?.Address).toBe("anomaly-test@example.com");
87 expect(created?.Subscribers[0]?.Type).toBe("EMAIL");
88
89 // Update — frequency and threshold expression are mutable in place.
90 const updated = yield* stack.deploy(makeStack("WEEKLY", "250"));
91 expect(updated.subscriptionArn).toBe(deployed.subscriptionArn);
92 const afterUpdate = yield* getSubscription(deployed.subscriptionArn);
93 expect(afterUpdate?.Frequency).toBe("WEEKLY");
94 expect(afterUpdate?.ThresholdExpression?.Dimensions?.Values).toEqual([
95 "250",
96 ]);
97
98 // Destroy — subscription and monitor are gone.
99 yield* stack.destroy();
100 yield* assertSubscriptionGone(deployed.subscriptionArn);
101 }),
102 {
103 tags: ["provider:aws", "provider:aws:costexplorer", "live"],
104 timeout: 120_000,
105 },
106);