1import * as AWS from "@/AWS";
2import { FHIRDatastore } from "@/AWS/HealthLake";
3import * as Test from "@/Test/Alchemy";
4import * as healthlake from "@distilled.cloud/aws/healthlake";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8
9const { test } = Test.make({ providers: AWS.providers() });
10
11// Ungated typed-error probe: prove the distilled error union carries the
12// not-found tag this provider's read/delete paths depend on. Runs in every
13// CI pass at near-zero cost, unlike the gated lifecycle below.
14test.provider(
15 "describeFHIRDatastore on a nonexistent datastore fails with ResourceNotFoundException",
16 () =>
17 Effect.gen(function* () {
18 const error = yield* Effect.flip(
19 healthlake.describeFHIRDatastore({
20 DatastoreId: "0123456789abcdef0123456789abcdef",
21 }),
22 );
23 expect(error._tag).toBe("ResourceNotFoundException");
24 }),
25 { tags: ["provider:aws", "provider:aws:healthlake", "live"] },
26);
27
28// The provider's delete already waits until the data store is gone; this
29// out-of-band check confirms it (gone, or terminal DELETED status).
30const assertDatastoreGone = (datastoreId: string) =>
31 Effect.gen(function* () {
32 const status = yield* healthlake
33 .describeFHIRDatastore({ DatastoreId: datastoreId })
34 .pipe(
35 Effect.map((r) => r.DatastoreProperties.DatastoreStatus),
36 Effect.catchTag("ResourceNotFoundException", () =>
37 Effect.succeed("gone" as const),
38 ),
39 );
40 if (status !== "gone" && status !== "DELETED") {
41 return yield* Effect.fail(
42 new Error(
43 `datastore '${datastoreId}' still exists (status: ${status})`,
44 ),
45 );
46 }
47 }).pipe(
48 Effect.retry({
49 schedule: Schedule.max([
50 Schedule.fixed("10 seconds"),
51 Schedule.recurs(8),
52 ]),
53 }),
54 );
55
56// A HealthLake FHIR data store takes ~15-30 minutes to provision (and
57// several more to delete) and bills while it exists. The full lifecycle is
58// gated behind AWS_TEST_HEALTHLAKE=1 and always destroys what it created.
59test.provider.skipIf(!process.env.AWS_TEST_HEALTHLAKE)(
60 "create FHIR R4 datastore, verify, update tags, destroy",
61 (stack) =>
62 Effect.gen(function* () {
63 yield* stack.destroy();
64
65 const { datastore } = yield* stack.deploy(
66 Effect.gen(function* () {
67 const datastore = yield* FHIRDatastore("Records", {
68 tags: { fixture: "healthlake-datastore" },
69 });
70 return { datastore };
71 }),
72 );
73
74 expect(datastore.datastoreId).toBeDefined();
75 expect(datastore.datastoreArn).toContain(":datastore/fhir/");
76 expect(datastore.datastoreStatus).toBe("ACTIVE");
77 expect(datastore.datastoreTypeVersion).toBe("R4");
78 expect(datastore.datastoreEndpoint).toContain("healthlake");
79 expect(datastore.tags.fixture).toBe("healthlake-datastore");
80
81 // Out-of-band verification via distilled.
82 const described = yield* healthlake.describeFHIRDatastore({
83 DatastoreId: datastore.datastoreId,
84 });
85 expect(described.DatastoreProperties.DatastoreStatus).toBe("ACTIVE");
86 expect(described.DatastoreProperties.DatastoreTypeVersion).toBe("R4");
87 expect(described.DatastoreProperties.DatastoreName).toBe(
88 datastore.datastoreName,
89 );
90
91 // Idempotent re-deploy with a tag change syncs in place (no replace,
92 // no second 15-30min provisioning wait — the datastore is ACTIVE).
93 const { datastore: updated } = yield* stack.deploy(
94 Effect.gen(function* () {
95 const datastore = yield* FHIRDatastore("Records", {
96 tags: { fixture: "healthlake-datastore", updated: "true" },
97 });
98 return { datastore };
99 }),
100 );
101 expect(updated.datastoreId).toBe(datastore.datastoreId);
102 expect(updated.tags.updated).toBe("true");
103
104 // Destroy — datastores bill while they exist — and verify deletion
105 // out-of-band. The provider waits until the datastore is gone.
106 yield* stack.destroy();
107 yield* assertDatastoreGone(datastore.datastoreId);
108 }),
109 // create (~15-30 min) + tag sync + delete wait-until-gone, one test.
110 {
111 tags: ["provider:aws", "provider:aws:healthlake", "live"],
112 timeout: 5_400_000,
113 },
114);