1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as translate from "@distilled.cloud/gcp/translate_v3";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import * as Stream from "effect/Stream";
8import { location, logLevel, currentParent, runLifecycle } from "./common.ts";
9
10const { test } = Test.make({ providers: GCP.providers() });
11
12const waitUntilGone = (name: string) =>
13 translate.getProjectsLocationsModels({ name }).pipe(
14 Effect.as("found" as const),
15 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
16 Effect.repeat({
17 schedule: Schedule.spaced("2 seconds"),
18 until: (status) => status === "gone",
19 times: 10,
20 }),
21 );
22
23const findDataset = (parent: string, displayName: string) =>
24 translate.listProjectsLocationsDatasets.pages({ parent, pageSize: 100 }).pipe(
25 Stream.flatMap((page) => Stream.fromIterable(page.datasets ?? [])),
26 Stream.filter((dataset) => dataset.displayName === displayName),
27 Stream.runHead,
28 );
29
30/** An empty Translation dataset (no sentence pairs imported). */
31const createEmptyDataset = (displayName: string) =>
32 Effect.gen(function* () {
33 const parent = yield* currentParent;
34 const existing = yield* findDataset(parent, displayName);
35 if (existing._tag === "Some" && existing.value.name) {
36 return existing.value.name;
37 }
38 const operation = yield* translate.createProjectsLocationsDatasets({
39 parent,
40 body: {
41 displayName,
42 sourceLanguageCode: "en",
43 targetLanguageCode: "es",
44 },
45 });
46 yield* GCP.Translate.waitForOperation(operation);
47 const created = yield* findDataset(parent, displayName);
48 expect(created._tag).toEqual("Some");
49 return created._tag === "Some" ? (created.value.name ?? "") : "";
50 });
51
52const deleteDataset = (name: string) =>
53 translate.deleteProjectsLocationsDatasets({ name }).pipe(
54 Effect.flatMap((operation) =>
55 GCP.Translate.waitForOperation(operation, { notFoundOk: true }),
56 ),
57 Effect.catchTag("NotFound", () => Effect.void),
58 );
59
60test.provider(
61 "getProjectsLocationsModels on a missing model fails with a typed tag",
62 (stack) =>
63 Effect.gen(function* () {
64 const parent = yield* currentParent;
65 yield* stack.destroy();
66
67 const error = yield* Effect.flip(
68 translate.getProjectsLocationsModels({
69 name: `${parent}/models/NM0000000000000000000`,
70 }),
71 );
72 expect(error._tag).toEqual("NotFound");
73
74 yield* stack.destroy();
75 }).pipe(logLevel),
76 { tags: ["provider:gcp", "provider:gcp:translate", "live"], timeout: 90_000 },
77);
78
79test.provider.skipIf(!runLifecycle)(
80 "a model trained from an empty dataset fails with GCP.OperationFailed",
81 (stack) =>
82 Effect.gen(function* () {
83 yield* stack.destroy();
84
85 const dataset = yield* createEmptyDataset("alcemptymodelds");
86
87 const error = yield* Effect.flip(
88 stack.deploy(
89 Effect.gen(function* () {
90 return yield* GCP.Translate.Model("Empty", {
91 location,
92 dataset,
93 displayName: "empty",
94 });
95 }),
96 ),
97 );
98 expect(error._tag).toEqual("GCP.OperationFailed");
99
100 yield* stack.destroy();
101 yield* deleteDataset(dataset);
102 }).pipe(logLevel),
103 {
104 tags: ["provider:gcp", "provider:gcp:translate", "live"],
105 timeout: 180_000,
106 },
107);
108
109// Training a model needs a dataset with imported sentence pairs and takes
110// hours. Point GCP_TEST_TRANSLATE_MODEL_DATASET at such a dataset.
111const runModelLifecycle =
112 runLifecycle &&
113 process.env.GCP_TEST_TRANSLATE_MODEL === "1" &&
114 !!process.env.GCP_TEST_TRANSLATE_MODEL_DATASET;
115
116test.provider.skipIf(!runModelLifecycle)(
117 "create and delete a translation model",
118 (stack) =>
119 Effect.gen(function* () {
120 yield* stack.destroy();
121 const dataset = process.env.GCP_TEST_TRANSLATE_MODEL_DATASET ?? "";
122
123 const model = yield* stack.deploy(
124 Effect.gen(function* () {
125 return yield* GCP.Translate.Model("EnEs", {
126 location,
127 dataset,
128 displayName: "enes",
129 });
130 }),
131 );
132 expect(model.name).toContain("/models/");
133 expect(model.location).toEqual(location);
134 expect(model.displayName).toEqual("enes");
135
136 const fetched = yield* translate.getProjectsLocationsModels({
137 name: model.name,
138 });
139 expect(fetched.name).toEqual(model.name);
140 expect(fetched.displayName).toEqual("enes");
141
142 yield* stack.destroy();
143 const gone = yield* waitUntilGone(model.name);
144 expect(gone).toEqual("gone");
145 }).pipe(logLevel),
146 {
147 tags: ["provider:gcp", "provider:gcp:translate", "live"],
148 timeout: 12 * 60 * 60_000,
149 },
150);