1import * as AWS from "@/AWS";
2import { Blueprint } 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
17// A deterministic, valid BDA blueprint schema (checked-in constant fixture).
18const invoiceSchema = (instruction: string) =>
19 JSON.stringify({
20 $schema: "http://json-schema.org/draft-07/schema#",
21 description: "Extract invoice fields",
22 class: "invoice",
23 type: "object",
24 definitions: {},
25 properties: {
26 invoice_number: {
27 type: "string",
28 inferenceType: "explicit",
29 instruction,
30 },
31 },
32 });
33
34const findBlueprint = (blueprintArn: string) =>
35 bda.getBlueprint({ blueprintArn }).pipe(
36 Effect.map((r) => r.blueprint),
37 Effect.catchTag("ResourceNotFoundException", () =>
38 Effect.succeed(undefined),
39 ),
40 );
41
42class BlueprintStillExists extends Data.TaggedError("BlueprintStillExists")<{
43 readonly blueprintArn: string;
44}> {}
45
46const assertBlueprintDeleted = (blueprintArn: string) =>
47 findBlueprint(blueprintArn).pipe(
48 Effect.flatMap((blueprint) =>
49 blueprint === undefined
50 ? Effect.void
51 : Effect.fail(new BlueprintStillExists({ blueprintArn })),
52 ),
53 Effect.retry({
54 while: (e) => e._tag === "BlueprintStillExists",
55 schedule: Schedule.max([Schedule.exponential(500), Schedule.recurs(8)]),
56 }),
57 );
58
59// Ungated typed-error probe: prove the distilled error union carries the
60// not-found tag this provider's read/delete paths depend on.
61test.provider(
62 "getBlueprint on a nonexistent ARN fails with ResourceNotFoundException",
63 () =>
64 Effect.gen(function* () {
65 const { Account } = yield* sts.getCallerIdentity({});
66 const region = yield* Effect.sync(
67 () => process.env.AWS_REGION ?? "us-west-2",
68 );
69 const error = yield* Effect.flip(
70 bda.getBlueprint({
71 blueprintArn: `arn:aws:bedrock:${region}:${Account}:blueprint/nonexistent-alchemy-probe`,
72 }),
73 );
74 expect(error._tag).toBe("ResourceNotFoundException");
75 }),
76 { tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"] },
77);
78
79test.provider(
80 "create, update schema, delete blueprint",
81 (stack) =>
82 Effect.gen(function* () {
83 yield* stack.destroy();
84
85 const blueprint = yield* stack.deploy(
86 Effect.gen(function* () {
87 return yield* Blueprint("TestBlueprint", {
88 type: "DOCUMENT",
89 schema: invoiceSchema("The invoice number"),
90 tags: { Environment: "test" },
91 });
92 }),
93 );
94
95 expect(blueprint.blueprintArn).toContain(":blueprint/");
96 expect(blueprint.type).toBe("DOCUMENT");
97
98 // out-of-band verification via distilled
99 const created = yield* findBlueprint(blueprint.blueprintArn);
100 expect(created).toBeDefined();
101 expect(unredact(created!.blueprintName)).toBe(blueprint.blueprintName);
102 expect(
103 JSON.parse(unredact(created!.schema)).properties.invoice_number,
104 ).toBeDefined();
105 const tags = yield* bda
106 .listTagsForResource({ resourceARN: blueprint.blueprintArn })
107 .pipe(
108 Effect.map((r) =>
109 Object.fromEntries((r.tags ?? []).map((t) => [t.key, t.value])),
110 ),
111 );
112 expect(tags.Environment).toBe("test");
113 expect(tags["alchemy::id"]).toBe("TestBlueprint");
114
115 // update the schema in place — same physical blueprint
116 const updated = yield* stack.deploy(
117 Effect.gen(function* () {
118 return yield* Blueprint("TestBlueprint", {
119 type: "DOCUMENT",
120 schema: invoiceSchema("The unique invoice number on the header"),
121 tags: { Environment: "test" },
122 });
123 }),
124 );
125 expect(updated.blueprintArn).toBe(blueprint.blueprintArn);
126
127 const afterUpdate = yield* findBlueprint(blueprint.blueprintArn);
128 expect(
129 JSON.parse(unredact(afterUpdate!.schema)).properties.invoice_number
130 .instruction,
131 ).toBe("The unique invoice number on the header");
132
133 yield* stack.destroy();
134 yield* assertBlueprintDeleted(blueprint.blueprintArn);
135 }),
136 {
137 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
138 timeout: 120_000,
139 },
140);
141
142test.provider(
143 "custom blueprint name and replacement on rename",
144 (stack) =>
145 Effect.gen(function* () {
146 yield* stack.destroy();
147
148 const first = yield* stack.deploy(
149 Effect.gen(function* () {
150 return yield* Blueprint("NamedBlueprint", {
151 blueprintName: "alchemy-test-blueprint-a",
152 type: "DOCUMENT",
153 schema: invoiceSchema("The invoice number"),
154 });
155 }),
156 );
157 expect(first.blueprintName).toBe("alchemy-test-blueprint-a");
158
159 // renaming triggers a replacement: new physical blueprint, old one gone
160 const second = yield* stack.deploy(
161 Effect.gen(function* () {
162 return yield* Blueprint("NamedBlueprint", {
163 blueprintName: "alchemy-test-blueprint-b",
164 type: "DOCUMENT",
165 schema: invoiceSchema("The invoice number"),
166 });
167 }),
168 );
169 expect(second.blueprintName).toBe("alchemy-test-blueprint-b");
170 expect(second.blueprintArn).not.toBe(first.blueprintArn);
171
172 yield* assertBlueprintDeleted(first.blueprintArn);
173
174 yield* stack.destroy();
175 yield* assertBlueprintDeleted(second.blueprintArn);
176 }),
177 {
178 tags: ["provider:aws", "provider:aws:bedrockdataautomation", "live"],
179 timeout: 120_000,
180 },
181);