1import * as AWS from "@/AWS";
2import { ResourceCollection } from "@/AWS/DevOpsGuru/ResourceCollection.ts";
3import * as Test from "@/Test/Alchemy";
4import * as devopsguru from "@distilled.cloud/aws/devops-guru";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7
8const { test } = Test.make({ providers: AWS.providers() });
9
10// Observe the account's tag-based coverage out-of-band. An account that has
11// never configured a collection fails with the typed ResourceNotFoundException
12// ("No CustomerResourceFilter present") — treated as empty. A key whose last
13// value was removed lingers with an empty TagValues list — also empty.
14const observedTags = devopsguru
15 .getResourceCollection({ ResourceCollectionType: "AWS_TAGS" })
16 .pipe(
17 Effect.map((r) =>
18 (r.ResourceCollection?.Tags ?? []).filter((t) => t.TagValues.length > 0),
19 ),
20 Effect.catchTag("ResourceNotFoundException", () =>
21 Effect.succeed([] as devopsguru.TagCollectionFilter[]),
22 ),
23 );
24
25// Ungated typed-error probe: getResourceCollection either returns the
26// configured collection or fails with the typed not-found tag — never an
27// untyped catch-all.
28test.provider(
29 "getResourceCollection returns typed results",
30 () =>
31 Effect.gen(function* () {
32 const tags = yield* observedTags;
33 expect(Array.isArray(tags)).toBe(true);
34 }),
35 { tags: ["provider:aws", "provider:aws:devopsguru", "live"] },
36);
37
38// The resource collection is an account/region singleton. This test only
39// runs when the account has no tag-based coverage it doesn't own — it must
40// never clobber coverage the user already operates.
41test.provider(
42 "lifecycle: configure tag coverage, converge values, remove",
43 (stack) =>
44 Effect.gen(function* () {
45 const preexisting = yield* observedTags;
46 const foreign = preexisting.filter(
47 (t) => t.AppBoundaryKey.toLowerCase() !== "devops-guru-alchemy",
48 );
49 if (foreign.length > 0) {
50 yield* Effect.logInfo(
51 `DevOps Guru tag coverage already configured (${foreign
52 .map((t) => t.AppBoundaryKey)
53 .join(", ")}) — skipping destructive lifecycle test`,
54 );
55 return;
56 }
57
58 yield* stack.destroy();
59
60 // Create — tag-based coverage.
61 const created = yield* stack.deploy(
62 Effect.gen(function* () {
63 return yield* ResourceCollection("Coverage", {
64 tags: [
65 {
66 appBoundaryKey: "devops-guru-alchemy",
67 tagValues: ["devopsguru-test-a", "devopsguru-test-b"],
68 },
69 ],
70 });
71 }),
72 );
73 expect(created.cloudFormationStackNames).toEqual([]);
74 expect(created.tags).toHaveLength(1);
75 expect(created.tags[0]!.tagValues.sort()).toEqual([
76 "devopsguru-test-a",
77 "devopsguru-test-b",
78 ]);
79
80 // Out-of-band verification via distilled.
81 const observed = yield* observedTags;
82 expect(observed).toHaveLength(1);
83 expect([...(observed[0]?.TagValues ?? [])].sort()).toEqual([
84 "devopsguru-test-a",
85 "devopsguru-test-b",
86 ]);
87
88 // Update — add one value, remove another; the provider applies only
89 // the delta (REMOVE b, ADD c).
90 const updated = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* ResourceCollection("Coverage", {
93 tags: [
94 {
95 appBoundaryKey: "devops-guru-alchemy",
96 tagValues: ["devopsguru-test-a", "devopsguru-test-c"],
97 },
98 ],
99 });
100 }),
101 );
102 expect(updated.tags[0]!.tagValues.sort()).toEqual([
103 "devopsguru-test-a",
104 "devopsguru-test-c",
105 ]);
106 const observedUpdated = yield* observedTags;
107 expect([...(observedUpdated[0]?.TagValues ?? [])].sort()).toEqual([
108 "devopsguru-test-a",
109 "devopsguru-test-c",
110 ]);
111
112 // Destroy — the collection is emptied and the account is clean again.
113 yield* stack.destroy();
114 const after = yield* observedTags;
115 expect(after).toEqual([]);
116 }),
117 {
118 tags: ["provider:aws", "provider:aws:devopsguru", "live"],
119 timeout: 180_000,
120 },
121);