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
17// Vertex AI data labeling is shut down: create fails with BadRequest
18// "Data labeling service is shutdown". Set GCP_TEST_AIPLATFORM_DATA_LABELING=1
19// on a project that still has access.
20const runLifecycle =
21 !process.env.FAST && !!process.env.GCP_TEST_AIPLATFORM_DATA_LABELING;
22
23const waitUntilGone = (name: string) =>
24 aiplatform.getProjectsLocationsSpecialistPools({ name }).pipe(
25 Effect.as("found" as const),
26 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
27 Effect.repeat({
28 schedule: Schedule.spaced("2 seconds"),
29 until: (status) => status === "gone",
30 times: 10,
31 }),
32 );
33
34test.provider(
35 "getProjectsLocationsSpecialistPools on a missing pool fails with a typed tag",
36 (stack) =>
37 Effect.gen(function* () {
38 const { project } = yield* GcpEnvironment.current;
39 yield* stack.destroy();
40
41 const error = yield* Effect.flip(
42 aiplatform.getProjectsLocationsSpecialistPools({
43 name: `projects/${project}/locations/us-central1/specialistPools/1234567890123456789`,
44 }),
45 );
46 expect(error._tag).toEqual("NotFound");
47
48 yield* stack.destroy();
49 }).pipe(logLevel),
50 {
51 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
52 timeout: 90_000,
53 },
54);
55
56test.provider.skipIf(runLifecycle)(
57 "createProjectsLocationsSpecialistPools is rejected because data labeling is shut down",
58 (stack) =>
59 Effect.gen(function* () {
60 const { project } = yield* GcpEnvironment.current;
61 yield* stack.destroy();
62 const error = yield* Effect.flip(
63 aiplatform.createProjectsLocationsSpecialistPools({
64 parent: `projects/${project}/locations/us-central1`,
65 body: {
66 displayName: "alchemy-pool-probe",
67 specialistManagerEmails: ["alchemy@example.com"],
68 },
69 }),
70 );
71 expect(error._tag).toEqual("BadRequest");
72 expect(String(error.message)).toContain("shutdown");
73 }).pipe(logLevel),
74 {
75 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
76 timeout: 90_000,
77 },
78);
79
80test.provider.skipIf(!runLifecycle)(
81 "create, update, and delete a specialist pool",
82 (stack) =>
83 Effect.gen(function* () {
84 yield* stack.destroy();
85
86 const created = yield* stack.deploy(
87 Effect.gen(function* () {
88 return yield* GCP.AIPlatform.SpecialistPool("Labelers", {
89 location: "us-central1",
90 displayName: "labelers",
91 });
92 }),
93 );
94
95 expect(created.name).toContain("/specialistPools/");
96 expect(created.displayName).toEqual("labelers");
97
98 const fetched = yield* aiplatform.getProjectsLocationsSpecialistPools({
99 name: created.name,
100 });
101 expect(fetched.name).toEqual(created.name);
102 expect(fetched.displayName).toContain("alchemy-id=");
103
104 const updated = yield* stack.deploy(
105 Effect.gen(function* () {
106 return yield* GCP.AIPlatform.SpecialistPool("Labelers", {
107 location: "us-central1",
108 displayName: "labelers-v2",
109 });
110 }),
111 );
112 expect(updated.name).toEqual(created.name);
113 expect(updated.displayName).toEqual("labelers-v2");
114
115 yield* stack.destroy();
116 const gone = yield* waitUntilGone(created.name);
117 expect(gone).toEqual("gone");
118 }).pipe(logLevel),
119 {
120 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
121 timeout: 120_000,
122 },
123);