1import * as AWS from "@/AWS";
2import { Monitor } from "@/AWS/InternetMonitor";
3import * as Test from "@/Test/Alchemy";
4import * as logs from "@distilled.cloud/aws/cloudwatch-logs";
5import * as im from "@distilled.cloud/aws/internetmonitor";
6import { describe, expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: AWS.providers() });
11
12// Ungated typed-error probe: prove the distilled error union carries the
13// not-found tag this provider's read/delete paths depend on (getMonitor's
14// Smithy model omits ResourceNotFoundException — patched in distilled).
15test.provider(
16 "getMonitor on a nonexistent monitor fails with ResourceNotFoundException",
17 () =>
18 Effect.gen(function* () {
19 const error = yield* Effect.flip(
20 im.getMonitor({
21 MonitorName: "alchemy-nonexistent-internetmonitor-probe",
22 }),
23 );
24 expect(error._tag).toBe("ResourceNotFoundException");
25 }),
26 { tags: ["provider:aws", "provider:aws:internetmonitor", "live"] },
27);
28
29// Typed wait-until-gone after destroy.
30const assertMonitorGone = (monitorName: string) =>
31 im.getMonitor({ MonitorName: monitorName }).pipe(
32 Effect.flatMap(() =>
33 Effect.fail(new Error(`monitor ${monitorName} still exists`)),
34 ),
35 Effect.catchTag("ResourceNotFoundException", () => Effect.void),
36 Effect.retry({
37 while: (e) => e instanceof Error,
38 schedule: Schedule.max([
39 Schedule.fixed("2 seconds"),
40 Schedule.recurs(10),
41 ]),
42 }),
43 );
44
45describe(
46 "AWS.InternetMonitor.Monitor",
47 { tags: ["provider:aws", "provider:aws:internetmonitor", "live"] },
48 () => {
49 test.provider(
50 "monitor lifecycle: create, update city-networks cap + tags, destroy",
51 (stack) =>
52 Effect.gen(function* () {
53 yield* stack.destroy();
54
55 const deployMonitor = (props: {
56 maxCityNetworksToMonitor: number;
57 tags: Record<string, string>;
58 }) =>
59 stack.deploy(
60 Effect.gen(function* () {
61 const monitor = yield* Monitor("AppMonitor", {
62 // No resources — an empty monitor is cheap and still
63 // exercises the full PENDING -> ACTIVE lifecycle.
64 resources: [],
65 ...props,
66 });
67 return {
68 monitorName: monitor.monitorName,
69 monitorArn: monitor.monitorArn,
70 status: monitor.status,
71 maxCityNetworksToMonitor: monitor.maxCityNetworksToMonitor,
72 tags: monitor.tags,
73 };
74 }),
75 );
76
77 const created = yield* deployMonitor({
78 maxCityNetworksToMonitor: 1,
79 tags: { purpose: "alchemy-test" },
80 });
81 expect(created.monitorArn).toContain(":monitor/");
82 expect(created.status).toBe("ACTIVE");
83 expect(created.maxCityNetworksToMonitor).toBe(1);
84 expect(created.tags.purpose).toBe("alchemy-test");
85 expect(created.tags["alchemy::id"]).toBe("AppMonitor");
86
87 // Out-of-band verification via distilled.
88 const observed = yield* im.getMonitor({
89 MonitorName: created.monitorName,
90 });
91 expect(observed.MonitorArn).toBe(created.monitorArn);
92 expect(observed.Status).toBe("ACTIVE");
93 expect(observed.Resources).toEqual([]);
94 expect(observed.MaxCityNetworksToMonitor).toBe(1);
95
96 // Update in place: raise the city-networks cap and add a tag. The
97 // monitor identity (name/arn) is stable.
98 const updated = yield* deployMonitor({
99 maxCityNetworksToMonitor: 2,
100 tags: { purpose: "alchemy-test", updated: "true" },
101 });
102 expect(updated.monitorName).toBe(created.monitorName);
103 expect(updated.monitorArn).toBe(created.monitorArn);
104 expect(updated.maxCityNetworksToMonitor).toBe(2);
105 expect(updated.tags.updated).toBe("true");
106
107 const observedUpdated = yield* im.getMonitor({
108 MonitorName: created.monitorName,
109 });
110 expect(observedUpdated.MaxCityNetworksToMonitor).toBe(2);
111 expect(observedUpdated.Tags?.updated).toBe("true");
112
113 yield* stack.destroy();
114 yield* assertMonitorGone(created.monitorName);
115
116 // Internet Monitor auto-creates per-monitor CloudWatch log groups
117 // (/aws/internet-monitor/{name}/{byCity,byCountry,byMetro,
118 // bySubdivision}) that survive DeleteMonitor. The provider delete
119 // reaps them — prove none remain after destroy.
120 const remainingLogGroups = yield* logs
121 .describeLogGroups({
122 logGroupNamePrefix: `/aws/internet-monitor/${created.monitorName}`,
123 })
124 .pipe(
125 Effect.map((r) => (r.logGroups ?? []).map((g) => g.logGroupName)),
126 );
127 expect(remainingLogGroups).toEqual([]);
128 }),
129 { timeout: 300_000 },
130 );
131 },
132);