1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as aiplatform from "@distilled.cloud/gcp/aiplatform_v1";
5import { expect } from "alchemy-test";
6import * as Effect from "effect/Effect";
7import { MinimumLogLevel } from "effect/References";
8import * as Schedule from "effect/Schedule";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const logLevel = Effect.provideService(
13 MinimumLogLevel,
14 process.env.DEBUG ? "Debug" : "Info",
15);
16
17const waitUntilGone = (name: string) =>
18 aiplatform.getProjectsLocationsIndexes({ name }).pipe(
19 Effect.as("found" as const),
20 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
21 Effect.repeat({
22 schedule: Schedule.spaced("2 seconds"),
23 until: (status) => status === "gone",
24 times: 10,
25 }),
26 );
27
28const indexMetadata = {
29 config: {
30 dimensions: 8,
31 approximateNeighborsCount: 10,
32 distanceMeasureType: "DOT_PRODUCT_DISTANCE",
33 shardSize: "SHARD_SIZE_SMALL",
34 algorithmConfig: { bruteForceConfig: {} },
35 },
36};
37
38test.provider(
39 "getProjectsLocationsIndexes on a missing index fails with a typed tag",
40 (stack) =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43 const parent = `projects/${project}/locations/us-central1`;
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 aiplatform.getProjectsLocationsIndexes({
48 name: `${parent}/indexes/1234567890123456789`,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52 const page = yield* aiplatform.listProjectsLocationsIndexes({
53 parent,
54 pageSize: 10,
55 });
56 expect((page.indexes ?? []).map((item) => item.name)).not.toContain(
57 `${parent}/indexes/1234567890123456789`,
58 );
59
60 yield* stack.destroy();
61 }).pipe(logLevel),
62 {
63 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
64 timeout: 90_000,
65 },
66);
67
68test.provider(
69 "create, update, and delete a matching engine index",
70 (stack) =>
71 Effect.gen(function* () {
72 yield* stack.destroy();
73
74 const created = yield* stack.deploy(
75 Effect.gen(function* () {
76 return yield* GCP.AIPlatform.Index("Embeddings", {
77 location: "us-central1",
78 displayName: "alchemy-index",
79 description: "embeddings",
80 indexUpdateMethod: "STREAM_UPDATE",
81 metadata: indexMetadata,
82 labels: { env: "test" },
83 });
84 }),
85 );
86
87 expect(created.name).toContain("/indexes/");
88 expect(created.location).toEqual("us-central1");
89 expect(created.labels).toMatchObject({ env: "test" });
90 expect(created.description).toEqual("embeddings");
91
92 const fetched = yield* aiplatform.getProjectsLocationsIndexes({
93 name: created.name,
94 });
95 expect(fetched.name).toEqual(created.name);
96 expect(fetched.labels?.env).toEqual("test");
97
98 const updated = yield* stack.deploy(
99 Effect.gen(function* () {
100 return yield* GCP.AIPlatform.Index("Embeddings", {
101 location: "us-central1",
102 displayName: "alchemy-index",
103 description: "embeddings-v2",
104 indexUpdateMethod: "STREAM_UPDATE",
105 metadata: indexMetadata,
106 labels: { env: "prod", role: "search" },
107 });
108 }),
109 );
110
111 expect(updated.name).toEqual(created.name);
112 expect(updated.description).toEqual("embeddings-v2");
113 expect(updated.labels).toMatchObject({ env: "prod", role: "search" });
114
115 yield* stack.destroy();
116
117 const gone = yield* waitUntilGone(created.name);
118 expect(gone).toEqual("gone");
119 }).pipe(logLevel),
120 {
121 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
122 timeout: 180_000,
123 },
124);