1import * as AWS from "@/AWS";
2import {
3 AnnotationStore,
4 ReferenceStore,
5 RunGroup,
6 SequenceStore,
7 VariantStore,
8 Workflow,
9} from "@/AWS/Omics";
10import * as Test from "@/Test/Alchemy";
11import * as omics from "@distilled.cloud/aws/omics";
12import { expect } from "alchemy-test";
13import * as Effect from "effect/Effect";
14import * as Schedule from "effect/Schedule";
15
16const { test } = Test.make({ providers: AWS.providers() });
17
18// Ungated typed-error probe: prove the distilled error union carries the
19// not-found tag every Omics read/delete path depends on. Runs in every CI
20// pass at near-zero cost, unlike the gated lifecycles below.
21test.provider(
22 "getReferenceStore on a nonexistent id fails with ResourceNotFoundException",
23 () =>
24 Effect.gen(function* () {
25 const error = yield* Effect.flip(
26 omics.getReferenceStore({ id: "1234567890" }),
27 );
28 expect(error._tag).toBe("ResourceNotFoundException");
29 }),
30 { tags: ["provider:aws", "provider:aws:omics", "live"] },
31);
32
33// Ungated probes for the `analytics-` HealthOmics endpoint (annotation/variant
34// stores). A missing-store read is safe (creates nothing) and proves both the
35// operation-level `analytics-` hostPrefix routing and the typed not-found tag
36// the AnnotationStore/VariantStore providers depend on. The create path for
37// these two stores is entitlement-gated (see the gated lifecycles below).
38test.provider(
39 "getAnnotationStore on a nonexistent name fails with ResourceNotFoundException",
40 () =>
41 Effect.gen(function* () {
42 const error = yield* Effect.flip(
43 omics.getAnnotationStore({ name: "alchemy_probe_nonexistent_store" }),
44 );
45 expect(error._tag).toBe("ResourceNotFoundException");
46 }),
47 { tags: ["provider:aws", "provider:aws:omics", "live"] },
48);
49
50test.provider(
51 "getVariantStore on a nonexistent name fails with ResourceNotFoundException",
52 () =>
53 Effect.gen(function* () {
54 const error = yield* Effect.flip(
55 omics.getVariantStore({ name: "alchemy_probe_nonexistent_store" }),
56 );
57 expect(error._tag).toBe("ResourceNotFoundException");
58 }),
59 { tags: ["provider:aws", "provider:aws:omics", "live"] },
60);
61
62const assertReferenceStoreGone = (id: string) =>
63 Effect.gen(function* () {
64 const gone = yield* omics.getReferenceStore({ id }).pipe(
65 Effect.map(() => false),
66 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(true)),
67 );
68 if (!gone) {
69 return yield* Effect.fail(
70 new Error(`reference store '${id}' still exists`),
71 );
72 }
73 }).pipe(
74 Effect.retry({
75 schedule: Schedule.max([
76 Schedule.fixed("3 seconds"),
77 Schedule.recurs(10),
78 ]),
79 }),
80 );
81
82test.provider(
83 "ReferenceStore: create, verify out-of-band, destroy",
84 (stack) =>
85 Effect.gen(function* () {
86 yield* stack.destroy();
87
88 const { store } = yield* stack.deploy(
89 Effect.gen(function* () {
90 const store = yield* ReferenceStore("Refs", {
91 description: "alchemy omics test reference store",
92 tags: { fixture: "omics-reference-store" },
93 });
94 return { store };
95 }),
96 );
97
98 expect(store.referenceStoreId).toBeDefined();
99 expect(store.referenceStoreArn).toContain(":referenceStore/");
100
101 const live = yield* omics.getReferenceStore({
102 id: store.referenceStoreId,
103 });
104 expect(live.id).toBe(store.referenceStoreId);
105 expect(live.description).toBe("alchemy omics test reference store");
106
107 const tags = yield* omics.listTagsForResource({
108 resourceArn: store.referenceStoreArn,
109 });
110 expect(tags.tags["fixture"]).toBe("omics-reference-store");
111 expect(tags.tags["alchemy::id"]).toBe("Refs");
112
113 yield* stack.destroy();
114 yield* assertReferenceStoreGone(store.referenceStoreId);
115 }),
116 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 180_000 },
117);
118
119const assertSequenceStoreGone = (id: string) =>
120 Effect.gen(function* () {
121 const gone = yield* omics.getSequenceStore({ id }).pipe(
122 Effect.map(() => false),
123 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(true)),
124 );
125 if (!gone) {
126 return yield* Effect.fail(
127 new Error(`sequence store '${id}' still exists`),
128 );
129 }
130 }).pipe(
131 Effect.retry({
132 schedule: Schedule.max([
133 Schedule.fixed("3 seconds"),
134 Schedule.recurs(10),
135 ]),
136 }),
137 );
138
139test.provider(
140 "SequenceStore: create, verify out-of-band, destroy",
141 (stack) =>
142 Effect.gen(function* () {
143 yield* stack.destroy();
144
145 const { store } = yield* stack.deploy(
146 Effect.gen(function* () {
147 const store = yield* SequenceStore("Reads", {
148 description: "alchemy omics test sequence store",
149 eTagAlgorithmFamily: "SHA256up",
150 tags: { fixture: "omics-sequence-store" },
151 });
152 return { store };
153 }),
154 );
155
156 expect(store.sequenceStoreId).toBeDefined();
157 expect(store.sequenceStoreArn).toContain(":sequenceStore/");
158
159 const live = yield* omics.getSequenceStore({
160 id: store.sequenceStoreId,
161 });
162 expect(live.id).toBe(store.sequenceStoreId);
163 expect(live.eTagAlgorithmFamily).toBe("SHA256up");
164
165 yield* stack.destroy();
166 yield* assertSequenceStoreGone(store.sequenceStoreId);
167 }),
168 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 180_000 },
169);
170
171const assertRunGroupGone = (id: string) =>
172 Effect.gen(function* () {
173 const gone = yield* omics.getRunGroup({ id }).pipe(
174 Effect.map(() => false),
175 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(true)),
176 );
177 if (!gone) {
178 return yield* Effect.fail(new Error(`run group '${id}' still exists`));
179 }
180 }).pipe(
181 Effect.retry({
182 schedule: Schedule.max([
183 Schedule.fixed("3 seconds"),
184 Schedule.recurs(10),
185 ]),
186 }),
187 );
188
189test.provider(
190 "RunGroup: create, update limits, destroy",
191 (stack) =>
192 Effect.gen(function* () {
193 yield* stack.destroy();
194
195 // Create with an initial CPU cap.
196 const first = yield* stack.deploy(
197 Effect.gen(function* () {
198 const group = yield* RunGroup("Batch", {
199 maxCpus: 4,
200 maxRuns: 2,
201 tags: { fixture: "omics-run-group" },
202 });
203 return { group };
204 }),
205 );
206 expect(first.group.runGroupId).toBeDefined();
207 const runGroupId = first.group.runGroupId;
208
209 const created = yield* omics.getRunGroup({ id: runGroupId });
210 expect(created.maxCpus).toBe(4);
211 expect(created.maxRuns).toBe(2);
212
213 // Update the CPU cap in place — same logical id, no replacement.
214 const second = yield* stack.deploy(
215 Effect.gen(function* () {
216 const group = yield* RunGroup("Batch", {
217 maxCpus: 8,
218 maxRuns: 2,
219 tags: { fixture: "omics-run-group" },
220 });
221 return { group };
222 }),
223 );
224 expect(second.group.runGroupId).toBe(runGroupId);
225
226 const updated = yield* omics.getRunGroup({ id: runGroupId });
227 expect(updated.maxCpus).toBe(8);
228
229 yield* stack.destroy();
230 yield* assertRunGroupGone(runGroupId);
231 }),
232 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 180_000 },
233);
234
235// ---------------------------------------------------------------------------
236// Gated lifecycles — Workflow, AnnotationStore, and VariantStore each require
237// async provisioning (and VariantStore a real reference genome ARN), so they
238// are gated behind AWS_TEST_OMICS=1. VariantStore additionally reads a
239// reference ARN from OMICS_REFERENCE_ARN.
240//
241// AnnotationStore/VariantStore CREATE additionally requires the account to be
242// entitled for HealthOmics Analytics; without it the analytics- endpoint
243// returns `AccessDeniedException: "Unable to determine service/operation name
244// to be authorized"`. The ungated read probes above cover the endpoint routing
245// and typed errors on every CI pass regardless of entitlement.
246// ---------------------------------------------------------------------------
247
248// A minimal WDL workflow definition, zipped in memory. A trivial workflow is
249// accepted by HealthOmics without any genomics data.
250const minimalWdl = `version 1.0
251workflow HelloWorld {
252 call Hello {}
253}
254task Hello {
255 command { echo "hello" }
256 output { String out = read_string(stdout()) }
257 runtime { cpus: 1, memory: "1 GiB" }
258}
259`;
260
261// Build a store-only (uncompressed) ZIP archive containing main.wdl. Kept
262// dependency-free so the fixture needs no zip library.
263const buildZip = (fileName: string, content: string): Uint8Array => {
264 const enc = new TextEncoder();
265 const nameBytes = enc.encode(fileName);
266 const dataBytes = enc.encode(content);
267 const crcTable = Array.from({ length: 256 }, (_, n) => {
268 let c = n;
269 for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
270 return c >>> 0;
271 });
272 let crc = 0xffffffff;
273 for (const b of dataBytes) crc = crcTable[(crc ^ b) & 0xff] ^ (crc >>> 8);
274 crc = (crc ^ 0xffffffff) >>> 0;
275
276 const chunks: number[] = [];
277 const push32 = (v: number) => {
278 chunks.push(
279 v & 0xff,
280 (v >>> 8) & 0xff,
281 (v >>> 16) & 0xff,
282 (v >>> 24) & 0xff,
283 );
284 };
285 const push16 = (v: number) => chunks.push(v & 0xff, (v >>> 8) & 0xff);
286
287 // Local file header.
288 push32(0x04034b50);
289 push16(20);
290 push16(0);
291 push16(0);
292 push16(0);
293 push16(0);
294 push32(crc);
295 push32(dataBytes.length);
296 push32(dataBytes.length);
297 push16(nameBytes.length);
298 push16(0);
299 for (const b of nameBytes) chunks.push(b);
300 for (const b of dataBytes) chunks.push(b);
301 const centralOffset = chunks.length;
302
303 // Central directory header.
304 push32(0x02014b50);
305 push16(20);
306 push16(20);
307 push16(0);
308 push16(0);
309 push16(0);
310 push16(0);
311 push32(crc);
312 push32(dataBytes.length);
313 push32(dataBytes.length);
314 push16(nameBytes.length);
315 push16(0);
316 push16(0);
317 push16(0);
318 push16(0);
319 push32(0);
320 push32(0);
321 for (const b of nameBytes) chunks.push(b);
322 const centralSize = chunks.length - centralOffset;
323
324 // End of central directory.
325 push32(0x06054b50);
326 push16(0);
327 push16(0);
328 push16(1);
329 push16(1);
330 push32(centralSize);
331 push32(centralOffset);
332 push16(0);
333
334 return Uint8Array.from(chunks);
335};
336
337const assertWorkflowGone = (id: string) =>
338 Effect.gen(function* () {
339 const status = yield* omics.getWorkflow({ id }).pipe(
340 Effect.map((w) => w.status ?? "gone"),
341 Effect.catchTag("ResourceNotFoundException", () =>
342 Effect.succeed("gone" as const),
343 ),
344 );
345 if (status !== "gone" && status !== "DELETED") {
346 return yield* Effect.fail(
347 new Error(`workflow '${id}' still exists (status: ${status})`),
348 );
349 }
350 }).pipe(
351 Effect.retry({
352 schedule: Schedule.max([
353 Schedule.fixed("5 seconds"),
354 Schedule.recurs(12),
355 ]),
356 }),
357 );
358
359test.provider.skipIf(!process.env.AWS_TEST_OMICS)(
360 "Workflow: create from inline WDL zip, verify ACTIVE, destroy",
361 (stack) =>
362 Effect.gen(function* () {
363 yield* stack.destroy();
364
365 const definitionZip = buildZip("main.wdl", minimalWdl);
366
367 const { workflow } = yield* stack.deploy(
368 Effect.gen(function* () {
369 const workflow = yield* Workflow("Hello", {
370 engine: "WDL",
371 main: "main.wdl",
372 definitionZip,
373 tags: { fixture: "omics-workflow" },
374 });
375 return { workflow };
376 }),
377 );
378
379 expect(workflow.workflowId).toBeDefined();
380 expect(workflow.status).toBe("ACTIVE");
381
382 const live = yield* omics.getWorkflow({ id: workflow.workflowId });
383 expect(live.status).toBe("ACTIVE");
384
385 yield* stack.destroy();
386 yield* assertWorkflowGone(workflow.workflowId);
387 }),
388 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 240_000 },
389);
390
391const assertAnnotationStoreGone = (name: string) =>
392 Effect.gen(function* () {
393 const gone = yield* omics.getAnnotationStore({ name }).pipe(
394 Effect.map(() => false),
395 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(true)),
396 );
397 if (!gone) {
398 return yield* Effect.fail(
399 new Error(`annotation store '${name}' still exists`),
400 );
401 }
402 }).pipe(
403 Effect.retry({
404 schedule: Schedule.max([
405 Schedule.fixed("5 seconds"),
406 Schedule.recurs(24),
407 ]),
408 }),
409 );
410
411test.provider.skipIf(!process.env.AWS_TEST_OMICS)(
412 "AnnotationStore: create TSV store, verify ACTIVE, destroy",
413 (stack) =>
414 Effect.gen(function* () {
415 yield* stack.destroy();
416
417 const { store } = yield* stack.deploy(
418 Effect.gen(function* () {
419 const store = yield* AnnotationStore("Annotations", {
420 storeFormat: "TSV",
421 storeOptions: {
422 tsvStoreOptions: { annotationType: "GENERIC" },
423 },
424 tags: { fixture: "omics-annotation-store" },
425 });
426 return { store };
427 }),
428 );
429
430 expect(store.annotationStoreId).toBeDefined();
431 expect(store.status).toBe("ACTIVE");
432
433 const live = yield* omics.getAnnotationStore({ name: store.name });
434 expect(live.status).toBe("ACTIVE");
435 expect(live.storeFormat).toBe("TSV");
436
437 yield* stack.destroy();
438 yield* assertAnnotationStoreGone(store.name);
439 }),
440 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 240_000 },
441);
442
443const assertVariantStoreGone = (name: string) =>
444 Effect.gen(function* () {
445 const gone = yield* omics.getVariantStore({ name }).pipe(
446 Effect.map(() => false),
447 Effect.catchTag("ResourceNotFoundException", () => Effect.succeed(true)),
448 );
449 if (!gone) {
450 return yield* Effect.fail(
451 new Error(`variant store '${name}' still exists`),
452 );
453 }
454 }).pipe(
455 Effect.retry({
456 schedule: Schedule.max([
457 Schedule.fixed("5 seconds"),
458 Schedule.recurs(24),
459 ]),
460 }),
461 );
462
463test.provider.skipIf(
464 !process.env.AWS_TEST_OMICS || !process.env.OMICS_REFERENCE_ARN,
465)(
466 "VariantStore: create with reference, verify ACTIVE, destroy",
467 (stack) =>
468 Effect.gen(function* () {
469 yield* stack.destroy();
470
471 const referenceArn = process.env.OMICS_REFERENCE_ARN!;
472
473 const { store } = yield* stack.deploy(
474 Effect.gen(function* () {
475 const store = yield* VariantStore("Variants", {
476 reference: { referenceArn },
477 tags: { fixture: "omics-variant-store" },
478 });
479 return { store };
480 }),
481 );
482
483 expect(store.variantStoreId).toBeDefined();
484 expect(store.status).toBe("ACTIVE");
485
486 const live = yield* omics.getVariantStore({ name: store.name });
487 expect(live.status).toBe("ACTIVE");
488
489 yield* stack.destroy();
490 yield* assertVariantStoreGone(store.name);
491 }),
492 { tags: ["provider:aws", "provider:aws:omics", "live"], timeout: 240_000 },
493);