1import * as AWS from "@/AWS";
2import { Blueprint, DataAutomationProject } 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";
11
12const { test } = Test.make({ providers: AWS.providers() });
13
14const unredact = (value: string | Redacted.Redacted<string>): string =>
15 Redacted.isRedacted(value) ? Redacted.value(value) : value;
16
17const documentStandardOutput = (
18 granularity: bda.DocumentExtractionGranularityType[],
19): bda.StandardOutputConfiguration => ({
20 document: {
21 extraction: {
22 granularity: { types: granularity },
23 boundingBox: { state: "DISABLED" },
24 },
25 generativeField: { state: "DISABLED" },
26 outputFormat: {
27 textFormat: { types: ["MARKDOWN"] },
28 additionalFileFormat: { state: "DISABLED" },
29 },
30 },
31});
32
33const findProject = (projectArn: string) =>
34 bda.getDataAutomationProject({ projectArn }).pipe(
35 Effect.map((r) => r.project),
36 Effect.catchTag("ResourceNotFoundException", () =>
37 Effect.succeed(undefined),
38 ),
39 );
40
41class ProjectStillExists extends Data.TaggedError("ProjectStillExists")<{
42 readonly projectArn: string;
43}> {}
44
45const assertProjectDeleted = (projectArn: string) =>
46 findProject(projectArn).pipe(
47 Effect.flatMap((project) =>
48 project === undefined
49 ? Effect.void
50 : Effect.fail(new ProjectStillExists({ projectArn })),
51 ),
52 Effect.retry({
53 while: (e) => e._tag === "ProjectStillExists",
54 schedule: Schedule.max([Schedule.exponential(1000), Schedule.recurs(8)]),
55 }),
56 );
57
58// Ungated typed-error probe: prove the distilled error union carries the
59// not-found tag this provider's read/delete paths depend on.
60test.provider(
61 "getDataAutomationProject on a nonexistent ARN fails with ResourceNotFoundException",
62 () =>
63 Effect.gen(function* () {
64 const { Account } = yield* sts.getCallerIdentity({});
65 const region = yield* Effect.sync(
66 () => process.env.AWS_REGION ?? "us-west-2",
67 );
68 const error = yield* Effect.flip(
69 bda.getDataAutomationProject({
70 projectArn: `arn:aws:bedrock:${region}:${Account}:data-automation-project/nonexistent-alchemy-probe`,
71 }),
72 );
73 expect(error._tag).toBe("ResourceNotFoundException");
74 }),
75 { tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"] },
76);
77
78test.provider(
79 "create, update configuration, delete project",
80 (stack) =>
81 Effect.gen(function* () {
82 yield* stack.destroy();
83
84 const project = yield* stack.deploy(
85 Effect.gen(function* () {
86 return yield* DataAutomationProject("TestProject", {
87 projectDescription: "alchemy test project",
88 standardOutputConfiguration: documentStandardOutput([
89 "DOCUMENT",
90 "PAGE",
91 ]),
92 tags: { Environment: "test" },
93 });
94 }),
95 );
96
97 expect(project.projectArn).toContain(":data-automation-project/");
98 expect(project.status).toBe("COMPLETED");
99
100 // out-of-band verification via distilled
101 const created = yield* findProject(project.projectArn);
102 expect(created).toBeDefined();
103 expect(unredact(created!.projectName)).toBe(project.projectName);
104 expect(unredact(created!.projectDescription ?? "")).toBe(
105 "alchemy test project",
106 );
107 expect(
108 created!.standardOutputConfiguration?.document?.extraction?.granularity
109 ?.types,
110 ).toEqual(["DOCUMENT", "PAGE"]);
111 const tags = yield* bda
112 .listTagsForResource({ resourceARN: project.projectArn })
113 .pipe(
114 Effect.map((r) =>
115 Object.fromEntries((r.tags ?? []).map((t) => [t.key, t.value])),
116 ),
117 );
118 expect(tags.Environment).toBe("test");
119 expect(tags["alchemy::id"]).toBe("TestProject");
120
121 // update the description + granularity in place — same physical project
122 const updated = yield* stack.deploy(
123 Effect.gen(function* () {
124 return yield* DataAutomationProject("TestProject", {
125 projectDescription: "alchemy test project v2",
126 standardOutputConfiguration: documentStandardOutput([
127 "DOCUMENT",
128 "PAGE",
129 "ELEMENT",
130 ]),
131 tags: { Environment: "test" },
132 });
133 }),
134 );
135 expect(updated.projectArn).toBe(project.projectArn);
136
137 const afterUpdate = yield* findProject(project.projectArn);
138 expect(unredact(afterUpdate!.projectDescription ?? "")).toBe(
139 "alchemy test project v2",
140 );
141 expect(
142 afterUpdate!.standardOutputConfiguration?.document?.extraction
143 ?.granularity?.types,
144 ).toEqual(["DOCUMENT", "PAGE", "ELEMENT"]);
145
146 yield* stack.destroy();
147 yield* assertProjectDeleted(project.projectArn);
148 }),
149 {
150 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
151 timeout: 240_000,
152 },
153);
154
155test.provider(
156 "project with custom output driven by a blueprint",
157 (stack) =>
158 Effect.gen(function* () {
159 yield* stack.destroy();
160
161 const { blueprint, project } = yield* stack.deploy(
162 Effect.gen(function* () {
163 const blueprint = yield* Blueprint("CustomBlueprint", {
164 type: "DOCUMENT",
165 schema: JSON.stringify({
166 $schema: "http://json-schema.org/draft-07/schema#",
167 description: "Extract receipt fields",
168 class: "receipt",
169 type: "object",
170 definitions: {},
171 properties: {
172 total_amount: {
173 type: "string",
174 inferenceType: "explicit",
175 instruction: "The total amount on the receipt",
176 },
177 },
178 }),
179 });
180 const project = yield* DataAutomationProject("CustomProject", {
181 standardOutputConfiguration: documentStandardOutput(["DOCUMENT"]),
182 customOutputConfiguration: {
183 blueprints: [{ blueprintArn: blueprint.blueprintArn }],
184 },
185 });
186 return { blueprint, project };
187 }),
188 );
189
190 expect(project.status).toBe("COMPLETED");
191
192 // out-of-band: the project references the blueprint
193 const observed = yield* findProject(project.projectArn);
194 expect(
195 observed?.customOutputConfiguration?.blueprints?.map(
196 (b) => b.blueprintArn,
197 ),
198 ).toEqual([blueprint.blueprintArn]);
199
200 yield* stack.destroy();
201 yield* assertProjectDeleted(project.projectArn);
202 }),
203 {
204 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
205 timeout: 240_000,
206 },
207);