1import * as AWS from "@/AWS";
2import * as Core from "@/Test/Core";
3import * as Test from "@/Test/Alchemy";
4import { describe, expect } from "alchemy-test";
5import * as Data from "effect/Data";
6import * as Effect from "effect/Effect";
7import * as Schedule from "effect/Schedule";
8import * as HttpClient from "effect/http/HttpClient";
9import * as HttpClientRequest from "effect/http/HttpClientRequest";
10import VectorsTestFunctionLive, {
11 VectorsTestFunction,
12} from "./vectors-handler";
13
14const testOptions = { providers: AWS.providers() };
15const { test, beforeAll, afterAll } = Test.make(testOptions);
16const sharedStack = Core.scratchStack(testOptions, "S3VectorsBindings");
17
18const readinessPolicy = Schedule.max([
19 Schedule.fixed("2 seconds"),
20 Schedule.recurs(75),
21]);
22
23let baseUrl: string;
24
25class TransientUpstream extends Data.TaggedError("TransientUpstream")<{
26 readonly status: number;
27 readonly body: string;
28}> {}
29
30const send = (request: HttpClientRequest.HttpClientRequest) =>
31 HttpClient.execute(request).pipe(
32 Effect.flatMap((response) =>
33 response.status >= 500
34 ? response.text.pipe(
35 Effect.flatMap((body) =>
36 Effect.fail(
37 new TransientUpstream({ status: response.status, body }),
38 ),
39 ),
40 )
41 : Effect.succeed(response),
42 ),
43 Effect.retry({
44 while: (e) => e._tag === "TransientUpstream",
45 schedule: Schedule.max([
46 Schedule.exponential("1 second"),
47 Schedule.recurs(5),
48 ]),
49 }),
50 );
51
52describe(
53 "S3Vectors Bindings",
54 {
55 tags: [
56 "provider:aws",
57 "provider:aws:lambda",
58 "provider:aws:s3vectors",
59 "live",
60 ],
61 },
62 () => {
63 beforeAll(
64 Effect.gen(function* () {
65 yield* Effect.logInfo("S3Vectors test setup: destroying previous");
66 yield* sharedStack.destroy();
67
68 yield* Effect.logInfo("S3Vectors test setup: deploying fixture");
69 const { functionUrl } = yield* sharedStack.deploy(
70 Effect.gen(function* () {
71 return yield* VectorsTestFunction;
72 }).pipe(Effect.provide(VectorsTestFunctionLive)),
73 );
74
75 expect(functionUrl).toBeTruthy();
76 baseUrl = functionUrl!.replace(/\/+$/, "");
77
78 yield* HttpClient.get(`${baseUrl}/ping`).pipe(
79 Effect.flatMap((response) =>
80 response.status === 200
81 ? Effect.succeed(response)
82 : Effect.fail(
83 new Error(`Function not ready: ${response.status}`),
84 ),
85 ),
86 Effect.retry({ schedule: readinessPolicy }),
87 );
88 }),
89 { timeout: 240_000 },
90 );
91
92 afterAll(sharedStack.destroy(), { timeout: 300_000 });
93
94 describe("Vectors", () => {
95 test.provider(
96 "put + query round-trips through the bound index",
97 (_stack) =>
98 Effect.gen(function* () {
99 // insert
100 const putBody = (yield* send(
101 HttpClientRequest.get(`${baseUrl}/put`),
102 ).pipe(Effect.flatMap((r) => r.json))) as { put: number };
103 expect(putBody.put).toBe(3);
104
105 // query nearest to [1,0,0,0] — expect "a" (exact) and "c" (close),
106 // retrying through data-plane eventual consistency.
107 const queryBody = yield* send(
108 HttpClientRequest.get(`${baseUrl}/query`),
109 ).pipe(
110 Effect.flatMap((r) => r.json),
111 Effect.map(
112 (b) => b as { keys: string[]; distanceMetric: string },
113 ),
114 Effect.flatMap((b) =>
115 b.keys.includes("a")
116 ? Effect.succeed(b)
117 : Effect.fail(
118 new Error(`query not ready: ${JSON.stringify(b)}`),
119 ),
120 ),
121 Effect.retry({
122 schedule: Schedule.max([
123 Schedule.spaced("3 seconds"),
124 Schedule.recurs(10),
125 ]),
126 }),
127 );
128 expect(queryBody.distanceMetric).toBe("cosine");
129 expect(queryBody.keys).toContain("a");
130 expect(queryBody.keys.length).toBe(2);
131
132 // get by key (read-only client)
133 const getBody = (yield* send(
134 HttpClientRequest.get(`${baseUrl}/get`),
135 ).pipe(Effect.flatMap((r) => r.json))) as { keys: string[] };
136 expect(getBody.keys).toContain("a");
137
138 // list keys (read-only client)
139 const listBody = (yield* send(
140 HttpClientRequest.get(`${baseUrl}/list`),
141 ).pipe(Effect.flatMap((r) => r.json))) as { keys: string[] };
142 expect(listBody.keys).toContain("a");
143
144 // delete a key (write-only client)
145 const delBody = (yield* send(
146 HttpClientRequest.get(`${baseUrl}/delete`),
147 ).pipe(Effect.flatMap((r) => r.json))) as { deleted: string };
148 expect(delBody.deleted).toBe("b");
149
150 // put + get through the composed ReadWrite client
151 const rwBody = (yield* send(
152 HttpClientRequest.get(`${baseUrl}/rw`),
153 ).pipe(Effect.flatMap((r) => r.json))) as { keys: string[] };
154 expect(rwBody.keys).toContain("rw");
155 }),
156 { timeout: 120_000 },
157 );
158 });
159 },
160);