1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import * as omics from "@distilled.cloud/aws/omics";
5import { describe, expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import OmicsBindingsFunctionLive, {
10 OmicsBindingsFunction,
11} from "./bindings-handler";
12
13const testOptions = { providers: AWS.providers() };
14const { test, beforeAll, afterAll } = Test.make(testOptions);
15
16// The live Lambda E2E provisions a SequenceStore + ReferenceStore + Lambda and
17// exercises the runtime bindings end-to-end. It is gated behind AWS_TEST_OMICS
18// (consistent with the async-provisioning lifecycles in Omics.test.ts). The
19// ungated typed-error probes below prove the distilled error unions each
20// binding depends on on every CI pass, entitled or not, at near-zero cost.
21const RUN_LIVE = !!process.env.AWS_TEST_OMICS;
22
23// Ungated typed-error probes — import the distilled ops directly, so this
24// green path never loads the alchemy Omics binding module.
25test.provider(
26 "listReadSets on a nonexistent sequence store fails with a typed tag",
27 () =>
28 Effect.gen(function* () {
29 const error = yield* Effect.flip(
30 omics.listReadSets({ sequenceStoreId: "0000000000" }),
31 );
32 expect(["ResourceNotFoundException", "ValidationException"]).toContain(
33 error._tag,
34 );
35 }),
36 { tags: ["provider:aws", "provider:aws:omics", "live"] },
37);
38
39test.provider(
40 "getRun on a nonexistent id fails with a typed tag",
41 () =>
42 Effect.gen(function* () {
43 const error = yield* Effect.flip(omics.getRun({ id: "0000000000" }));
44 expect(["ResourceNotFoundException", "ValidationException"]).toContain(
45 error._tag,
46 );
47 }),
48 { tags: ["provider:aws", "provider:aws:omics", "live"] },
49);
50
51test.provider(
52 "listReferences on a nonexistent reference store fails with a typed tag",
53 () =>
54 Effect.gen(function* () {
55 const error = yield* Effect.flip(
56 omics.listReferences({ referenceStoreId: "0000000000" }),
57 );
58 expect(["ResourceNotFoundException", "ValidationException"]).toContain(
59 error._tag,
60 );
61 }),
62 { tags: ["provider:aws", "provider:aws:omics", "live"] },
63);
64
65const sharedStack = Core.scratchStack(testOptions, "OmicsBindings");
66
67let baseUrl: string;
68
69const get = (path: string) =>
70 HttpClient.get(`${baseUrl}${path}`).pipe(Effect.flatMap((r) => r.json));
71
72describe(
73 "Omics Bindings (E2E)",
74 {
75 tags: [
76 "provider:aws",
77 "provider:aws:batch",
78 "provider:aws:lambda",
79 "provider:aws:omics",
80 "live",
81 ],
82 },
83 () => {
84 beforeAll(
85 Effect.gen(function* () {
86 if (!RUN_LIVE) return;
87 yield* Effect.logInfo("Omics E2E setup: destroying previous run");
88 yield* sharedStack.destroy();
89
90 yield* Effect.logInfo("Omics E2E setup: deploying stores + Lambda");
91 const { functionUrl } = yield* sharedStack.deploy(
92 Effect.gen(function* () {
93 return yield* OmicsBindingsFunction;
94 }).pipe(Effect.provide(OmicsBindingsFunctionLive)),
95 );
96
97 expect(functionUrl).toBeTruthy();
98 baseUrl = functionUrl!.replace(/\/+$/, "");
99
100 // Readiness probe — fresh function URLs take seconds to serve 200s.
101 yield* HttpClient.get(`${baseUrl}/bindings`).pipe(
102 Effect.flatMap((response) =>
103 response.status === 200
104 ? Effect.succeed(response)
105 : Effect.fail(
106 new Error(`Function not ready: ${response.status}`),
107 ),
108 ),
109 Effect.retry({
110 schedule: Schedule.max([
111 Schedule.fixed("2 seconds"),
112 Schedule.recurs(60),
113 ]),
114 }),
115 );
116 }),
117 { timeout: 300_000 },
118 );
119 afterAll(
120 Effect.gen(function* () {
121 if (!RUN_LIVE) return;
122 yield* sharedStack.destroy();
123 }),
124 { timeout: 300_000 },
125 );
126
127 test.provider.skipIf(!RUN_LIVE)(
128 "all 8 capabilities initialize in the runtime",
129 () =>
130 Effect.gen(function* () {
131 const response = (yield* get("/bindings")) as { bound: string[] };
132 expect(response.bound).toHaveLength(8);
133 }),
134 );
135
136 test.provider.skipIf(!RUN_LIVE)(
137 "ListReadSets + ListReferences round-trip against the empty stores",
138 () =>
139 Effect.gen(function* () {
140 const readSets = (yield* get("/readsets")) as { count: number };
141 expect(readSets.count).toBe(0);
142 const references = (yield* get("/references")) as { count: number };
143 expect(references.count).toBe(0);
144 }),
145 );
146
147 test.provider.skipIf(!RUN_LIVE)(
148 "ListRuns round-trips at the account level",
149 () =>
150 Effect.gen(function* () {
151 const runs = (yield* get("/runs")) as { count: number };
152 expect(runs.count).toBeGreaterThanOrEqual(0);
153 }),
154 );
155
156 test.provider.skipIf(!RUN_LIVE)(
157 "GetReadSetMetadata + GetRun surface the typed not-found",
158 () =>
159 Effect.gen(function* () {
160 const readset = (yield* get("/readset/typed-not-found")) as {
161 typed: boolean;
162 };
163 expect(readset.typed).toBe(true);
164 const run = (yield* get("/run/typed-not-found")) as {
165 typed: boolean;
166 };
167 expect(run.typed).toBe(true);
168 }),
169 );
170 },
171);