1import * as AWS from "@/AWS";
2import { AnomalyMonitor } from "@/AWS/CostExplorer/AnomalyMonitor.ts";
3import * as Provider from "@/Provider";
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 * as sts from "@distilled.cloud/aws/sts";
8import { expect } from "alchemy-test";
9import * as Effect from "effect/Effect";
10import * as Schedule from "effect/Schedule";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14// Cost Explorer is served exclusively from us-east-1.
15const pin = <A, E, R>(effect: Effect.Effect<A, E, R>) =>
16 effect.pipe(Effect.provideService(AwsRegion, Effect.succeed("us-east-1")));
17
18const monitorName = "alchemy-test-anomaly-monitor";
19
20// getAnomalyMonitors returns an empty list (NOT an error) for an unknown ARN,
21// so absence is `AnomalyMonitors[0] === undefined`.
22const getMonitor = (monitorArn: string) =>
23 pin(ce.getAnomalyMonitors({ MonitorArnList: [monitorArn] })).pipe(
24 Effect.map((r) => r.AnomalyMonitors[0]),
25 Effect.catchTag("UnknownMonitorException", () => Effect.succeed(undefined)),
26 );
27
28// Typed wait-until-gone: deletion is synchronous but tolerate a short
29// eventual-consistency window.
30const assertMonitorGone = (monitorArn: string) =>
31 Effect.gen(function* () {
32 const found = yield* getMonitor(monitorArn);
33 if (found !== undefined) {
34 return yield* Effect.fail(
35 new Error(`monitor '${monitorArn}' still exists`),
36 );
37 }
38 }).pipe(
39 Effect.retry({
40 schedule: Schedule.max([Schedule.fixed("2 seconds"), Schedule.recurs(5)]),
41 }),
42 );
43
44// Ungated typed-error probe: prove the distilled error union carries the
45// not-found tag this provider's delete path depends on. (GetAnomalyMonitors
46// succeeds with an empty list for unknown ARNs — delete is the op that
47// raises the typed tag.)
48test.provider(
49 "deleteAnomalyMonitor on a nonexistent monitor ARN fails with UnknownMonitorException",
50 () =>
51 Effect.gen(function* () {
52 // The ARN must carry the caller's account — a foreign account ARN is
53 // rejected earlier with AccessDeniedException.
54 const identity = yield* sts.getCallerIdentity({});
55 const error = yield* Effect.flip(
56 pin(
57 ce.deleteAnomalyMonitor({
58 MonitorArn: `arn:aws:ce::${identity.Account}:anomalymonitor/00000000-0000-0000-0000-000000000000`,
59 }),
60 ),
61 );
62 expect(error._tag).toBe("UnknownMonitorException");
63 }),
64 {
65 tags: ["provider:aws", "provider:aws:costexplorer", "live"],
66 timeout: 60_000,
67 },
68);
69
70test.provider(
71 "lifecycle: create CUSTOM monitor with explicit name, rename in place, destroy",
72 (stack) =>
73 Effect.gen(function* () {
74 yield* stack.destroy();
75
76 // CUSTOM avoids the one-DIMENSIONAL-SERVICE-monitor-per-account quota.
77 const deployed = yield* stack.deploy(
78 Effect.gen(function* () {
79 const monitor = yield* AnomalyMonitor("Monitor", {
80 monitorName,
81 monitorType: "CUSTOM",
82 monitorSpecification: {
83 Tags: { Key: "CostCenter", Values: ["alchemy-test"] },
84 },
85 tags: { fixture: "cost-explorer-anomaly-monitor" },
86 });
87 return { monitorArn: monitor.monitorArn };
88 }),
89 );
90
91 // Out-of-band verification via distilled.
92 const created = yield* getMonitor(deployed.monitorArn);
93 expect(created?.MonitorName).toBe(monitorName);
94 expect(created?.MonitorType).toBe("CUSTOM");
95 const tags = yield* pin(
96 ce.listTagsForResource({ ResourceArn: deployed.monitorArn }),
97 );
98 const tagRecord = Object.fromEntries(
99 (tags.ResourceTags ?? []).map((t) => [t.Key, t.Value]),
100 );
101 expect(tagRecord.fixture).toBe("cost-explorer-anomaly-monitor");
102 expect(tagRecord["alchemy::id"]).toBe("Monitor");
103
104 // Canonical list() coverage.
105 const provider = yield* Provider.findProvider(AnomalyMonitor);
106 const all = yield* provider.list();
107 expect(all.some((m) => m.monitorArn === deployed.monitorArn)).toBe(true);
108
109 // Rename — the name is mutable in place; the ARN must be stable.
110 const renamed = yield* stack.deploy(
111 Effect.gen(function* () {
112 const monitor = yield* AnomalyMonitor("Monitor", {
113 monitorName: `${monitorName}-renamed`,
114 monitorType: "CUSTOM",
115 monitorSpecification: {
116 Tags: { Key: "CostCenter", Values: ["alchemy-test"] },
117 },
118 tags: { fixture: "cost-explorer-anomaly-monitor" },
119 });
120 return { monitorArn: monitor.monitorArn };
121 }),
122 );
123 expect(renamed.monitorArn).toBe(deployed.monitorArn);
124 const afterRename = yield* getMonitor(deployed.monitorArn);
125 expect(afterRename?.MonitorName).toBe(`${monitorName}-renamed`);
126
127 // Destroy — the monitor is gone.
128 yield* stack.destroy();
129 yield* assertMonitorGone(deployed.monitorArn);
130 }),
131 {
132 tags: ["provider:aws", "provider:aws:costexplorer", "live"],
133 timeout: 120_000,
134 },
135);
136
137// Monitor names are unique per account and the specification is create-only,
138// so replacement coverage uses the engine-generated physical name (a new
139// instance ID yields a new name, letting create-before-delete succeed).
140test.provider(
141 "replacement: changing the monitor specification replaces the monitor",
142 (stack) =>
143 Effect.gen(function* () {
144 yield* stack.destroy();
145
146 const makeMonitor = (costCenter: string) =>
147 Effect.gen(function* () {
148 const monitor = yield* AnomalyMonitor("ReplaceMonitor", {
149 monitorType: "CUSTOM",
150 monitorSpecification: {
151 Tags: { Key: "CostCenter", Values: [costCenter] },
152 },
153 });
154 return { monitorArn: monitor.monitorArn };
155 });
156
157 const deployed = yield* stack.deploy(makeMonitor("alchemy-replace-1"));
158 const created = yield* getMonitor(deployed.monitorArn);
159 expect(created?.MonitorType).toBe("CUSTOM");
160
161 // Change the specification — create-only, must replace (new ARN).
162 const replaced = yield* stack.deploy(makeMonitor("alchemy-replace-2"));
163 expect(replaced.monitorArn).not.toBe(deployed.monitorArn);
164 yield* assertMonitorGone(deployed.monitorArn);
165 const after = yield* getMonitor(replaced.monitorArn);
166 expect(after?.MonitorSpecification?.Tags?.Values).toEqual([
167 "alchemy-replace-2",
168 ]);
169
170 // Destroy — the replacement monitor is gone.
171 yield* stack.destroy();
172 yield* assertMonitorGone(replaced.monitorArn);
173 }),
174 {
175 tags: ["provider:aws", "provider:aws:costexplorer", "live"],
176 timeout: 120_000,
177 },
178);