1import * as AWS from "@/AWS";
2import { DataAutomationLibrary } from "@/AWS/BedrockDataAutomation";
3import * as Test from "@/Test/Alchemy";
4import * as bda from "@distilled.cloud/aws/bedrock-data-automation";
5import * as sts from "@distilled.cloud/aws/sts";
6import { expect } from "alchemy-test";
7import * as Data from "effect/Data";
8import * as Effect from "effect/Effect";
9import * as Redacted from "effect/Redacted";
10import * as Schedule from "effect/Schedule";
11import * as Stream from "effect/Stream";
12
13const { test } = Test.make({ providers: AWS.providers() });
14
15const unredact = (value: string | Redacted.Redacted<string>): string =>
16 Redacted.isRedacted(value) ? Redacted.value(value) : value;
17
18const findLibrary = (libraryArn: string) =>
19 bda.getDataAutomationLibrary({ libraryArn }).pipe(
20 Effect.map((r) => r.library),
21 Effect.catchTag("ResourceNotFoundException", () =>
22 Effect.succeed(undefined),
23 ),
24 );
25
26class LibraryStillExists extends Data.TaggedError("LibraryStillExists")<{
27 readonly libraryArn: string;
28}> {}
29
30const assertLibraryDeleted = (libraryArn: string) =>
31 findLibrary(libraryArn).pipe(
32 Effect.flatMap((library) =>
33 library === undefined
34 ? Effect.void
35 : Effect.fail(new LibraryStillExists({ libraryArn })),
36 ),
37 Effect.retry({
38 while: (e) => e._tag === "LibraryStillExists",
39 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
40 }),
41 );
42
43// Sweep libraries leaked by interrupted probe/dev runs (deterministic names
44// only — never touches libraries owned by other suites).
45const sweepLeakedProbeLibraries = Effect.gen(function* () {
46 const summaries = yield* bda.listDataAutomationLibraries
47 .items({})
48 .pipe(Stream.runCollect);
49 yield* Effect.forEach(
50 Array.from(summaries).filter(
51 (s) =>
52 s.libraryName !== undefined &&
53 unredact(s.libraryName) === "alchemy-probe-lib",
54 ),
55 (s) =>
56 bda
57 .deleteDataAutomationLibrary({ libraryArn: s.libraryArn })
58 .pipe(Effect.catchTag("ResourceNotFoundException", () => Effect.void)),
59 { concurrency: 1 },
60 );
61});
62
63// Ungated typed-error probe: prove the distilled error union carries the
64// not-found tag this provider's read/delete paths depend on.
65test.provider(
66 "getDataAutomationLibrary on a nonexistent ARN fails with ResourceNotFoundException",
67 () =>
68 Effect.gen(function* () {
69 const { Account } = yield* sts.getCallerIdentity({});
70 const region = yield* Effect.sync(
71 () => process.env.AWS_REGION ?? "us-west-2",
72 );
73 const error = yield* Effect.flip(
74 bda.getDataAutomationLibrary({
75 libraryArn: `arn:aws:bedrock:${region}:${Account}:data-automation-library/nonexistentalchemyprobe0`,
76 }),
77 );
78 expect(error._tag).toBe("ResourceNotFoundException");
79 }),
80 { tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"] },
81);
82
83test.provider(
84 "create, update description, delete library",
85 (stack) =>
86 Effect.gen(function* () {
87 yield* sweepLeakedProbeLibraries;
88 yield* stack.destroy();
89
90 const library = yield* stack.deploy(
91 Effect.gen(function* () {
92 return yield* DataAutomationLibrary("TestLibrary", {
93 libraryDescription: "alchemy test library",
94 tags: { Environment: "test" },
95 });
96 }),
97 );
98
99 expect(library.libraryArn).toContain(":data-automation-library/");
100 expect(library.status).toBe("ACTIVE");
101
102 // out-of-band verification via distilled
103 const created = yield* findLibrary(library.libraryArn);
104 expect(created).toBeDefined();
105 expect(unredact(created!.libraryName)).toBe(library.libraryName);
106 expect(unredact(created!.libraryDescription ?? "")).toBe(
107 "alchemy test library",
108 );
109 const tags = yield* bda
110 .listTagsForResource({ resourceARN: library.libraryArn })
111 .pipe(
112 Effect.map((r) =>
113 Object.fromEntries((r.tags ?? []).map((t) => [t.key, t.value])),
114 ),
115 );
116 expect(tags.Environment).toBe("test");
117 expect(tags["alchemy::id"]).toBe("TestLibrary");
118
119 // update the description in place — same physical library
120 const updated = yield* stack.deploy(
121 Effect.gen(function* () {
122 return yield* DataAutomationLibrary("TestLibrary", {
123 libraryDescription: "alchemy test library (updated)",
124 tags: { Environment: "test" },
125 });
126 }),
127 );
128 expect(updated.libraryArn).toBe(library.libraryArn);
129
130 const afterUpdate = yield* findLibrary(library.libraryArn);
131 expect(unredact(afterUpdate!.libraryDescription ?? "")).toBe(
132 "alchemy test library (updated)",
133 );
134
135 yield* stack.destroy();
136 yield* assertLibraryDeleted(library.libraryArn);
137 }),
138 {
139 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
140 timeout: 120_000,
141 },
142);
143
144test.provider(
145 "custom library name and replacement on rename",
146 (stack) =>
147 Effect.gen(function* () {
148 yield* stack.destroy();
149
150 const first = yield* stack.deploy(
151 Effect.gen(function* () {
152 return yield* DataAutomationLibrary("NamedLibrary", {
153 libraryName: "alchemy-test-library-a",
154 });
155 }),
156 );
157 expect(first.libraryName).toBe("alchemy-test-library-a");
158
159 // renaming triggers a replacement: new physical library, old one gone
160 const second = yield* stack.deploy(
161 Effect.gen(function* () {
162 return yield* DataAutomationLibrary("NamedLibrary", {
163 libraryName: "alchemy-test-library-b",
164 });
165 }),
166 );
167 expect(second.libraryName).toBe("alchemy-test-library-b");
168 expect(second.libraryArn).not.toBe(first.libraryArn);
169
170 yield* assertLibraryDeleted(first.libraryArn);
171
172 yield* stack.destroy();
173 yield* assertLibraryDeleted(second.libraryArn);
174 }),
175 {
176 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
177 timeout: 120_000,
178 },
179);