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.getProjectsLocationsHyperparameterTuningJobs({ 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 studySpec = {
29 metrics: [{ metricId: "accuracy", goal: "MAXIMIZE" as const }],
30 parameters: [
31 {
32 parameterId: "lr",
33 doubleValueSpec: { minValue: 0.001, maxValue: 0.1 },
34 },
35 ],
36};
37
38const trialJobSpec = {
39 workerPoolSpecs: [
40 {
41 machineSpec: { machineType: "n1-standard-4" },
42 replicaCount: "1",
43 containerSpec: {
44 imageUri:
45 "us-docker.pkg.dev/vertex-ai/training/tf-cpu.2-12.py310:latest",
46 command: ["echo", "ok"],
47 },
48 },
49 ],
50};
51
52test.provider(
53 "getProjectsLocationsHyperparameterTuningJobs on a missing job fails with a typed tag",
54 (stack) =>
55 Effect.gen(function* () {
56 const { project } = yield* GcpEnvironment.current;
57 const parent = `projects/${project}/locations/us-central1`;
58 yield* stack.destroy();
59
60 const error = yield* Effect.flip(
61 aiplatform.getProjectsLocationsHyperparameterTuningJobs({
62 name: `${parent}/hyperparameterTuningJobs/1234567890123456789`,
63 }),
64 );
65 expect(error._tag).toEqual("NotFound");
66 const page =
67 yield* aiplatform.listProjectsLocationsHyperparameterTuningJobs({
68 parent,
69 pageSize: 10,
70 });
71 expect(
72 (page.hyperparameterTuningJobs ?? []).map((item) => item.name),
73 ).not.toContain(`${parent}/hyperparameterTuningJobs/1234567890123456789`);
74
75 yield* stack.destroy();
76 }).pipe(logLevel),
77 {
78 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
79 timeout: 90_000,
80 },
81);
82
83test.provider(
84 "create and delete a hyperparameter tuning job",
85 (stack) =>
86 Effect.gen(function* () {
87 yield* stack.destroy();
88
89 const created = yield* stack.deploy(
90 Effect.gen(function* () {
91 return yield* GCP.AIPlatform.HyperparameterTuningJob("Tune", {
92 location: "us-central1",
93 displayName: "alchemy-hpt",
94 maxTrialCount: 1,
95 parallelTrialCount: 1,
96 studySpec,
97 trialJobSpec,
98 labels: { env: "test" },
99 });
100 }),
101 );
102
103 expect(created.name).toContain("/hyperparameterTuningJobs/");
104 expect(created.location).toEqual("us-central1");
105 expect(created.labels).toMatchObject({ env: "test" });
106
107 const fetched =
108 yield* aiplatform.getProjectsLocationsHyperparameterTuningJobs({
109 name: created.name,
110 });
111 expect(fetched.name).toEqual(created.name);
112
113 yield* stack.destroy();
114
115 const gone = yield* waitUntilGone(created.name);
116 expect(gone).toEqual("gone");
117 }).pipe(logLevel),
118 {
119 tags: ["provider:gcp", "provider:gcp:aiplatform", "live"],
120 timeout: 180_000,
121 },
122);