1import * as GCP from "@/GCP";
2import * as Test from "@/Test/Alchemy";
3import * as speech from "@distilled.cloud/gcp/speech_v1";
4import { expect } from "alchemy-test";
5import * as Effect from "effect/Effect";
6import * as Schedule from "effect/Schedule";
7import { location, logLevel, currentParent, currentProject } from "./common.ts";
8
9const { test } = Test.make({ providers: GCP.providers() });
10
11const waitUntilGone = (name: string) =>
12 speech.getProjectsLocationsPhraseSets({ name }).pipe(
13 Effect.as("found" as const),
14 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
15 Effect.repeat({
16 schedule: Schedule.spaced("1 second"),
17 until: (status) => status === "gone",
18 times: 10,
19 }),
20 );
21
22test.provider(
23 "getProjectsLocationsPhraseSets on a missing phrase set fails with a typed tag",
24 (stack) =>
25 Effect.gen(function* () {
26 const parent = yield* currentParent;
27 yield* stack.destroy();
28
29 const error = yield* Effect.flip(
30 speech.getProjectsLocationsPhraseSets({
31 name: `${parent}/phraseSets/alchemy-missing-phrases`,
32 }),
33 );
34 expect(error._tag).toEqual("NotFound");
35
36 yield* stack.destroy();
37 }).pipe(logLevel),
38 { tags: ["provider:gcp", "provider:gcp:speech", "live"], timeout: 90_000 },
39);
40
41test.provider(
42 "create, update, and delete a phrase set",
43 (stack) =>
44 Effect.gen(function* () {
45 const project = yield* currentProject;
46 yield* stack.destroy();
47
48 const created = yield* stack.deploy(
49 Effect.gen(function* () {
50 return yield* GCP.Speech.PhraseSet("Hints", {
51 location,
52 phrases: [{ value: "weather", boost: 10 }],
53 boost: 5,
54 });
55 }),
56 );
57
58 expect(created.name).toMatch(
59 new RegExp(`^projects/[^/]+/locations/${location}/phraseSets/`),
60 );
61 expect(created.phraseSetId.length).toBeGreaterThanOrEqual(4);
62 expect(created.project).toEqual(project);
63 expect(created.location).toEqual(location);
64 expect(created.boost).toEqual(5);
65 expect(
66 created.phrases.some(
67 (phrase) => phrase.value === "weather" && phrase.boost === 10,
68 ),
69 ).toEqual(true);
70
71 const fetched = yield* speech.getProjectsLocationsPhraseSets({
72 name: created.name,
73 });
74 expect(fetched.name).toEqual(created.name);
75 expect(fetched.boost).toEqual(5);
76 const fetchedValues = (fetched.phrases ?? []).map(
77 (phrase) => phrase.value ?? "",
78 );
79 expect(fetchedValues).toContain("weather");
80 // Items steer recognition: Alchemy adds no ownership item.
81 expect(fetchedValues.some((value) => value.startsWith("alc "))).toEqual(
82 false,
83 );
84
85 const updated = yield* stack.deploy(
86 Effect.gen(function* () {
87 return yield* GCP.Speech.PhraseSet("Hints", {
88 location,
89 phraseSetId: created.phraseSetId,
90 phrases: [{ value: "weather", boost: 15 }, { value: "forecast" }],
91 boost: 8,
92 });
93 }),
94 );
95
96 expect(updated.name).toEqual(created.name);
97 expect(updated.boost).toEqual(8);
98 expect(updated.phrases.map((phrase) => phrase.value).sort()).toEqual([
99 "forecast",
100 "weather",
101 ]);
102
103 const fetchedUpdate = yield* speech.getProjectsLocationsPhraseSets({
104 name: created.name,
105 });
106 expect(fetchedUpdate.boost).toEqual(8);
107 const updatedValues = (fetchedUpdate.phrases ?? []).map(
108 (phrase) => phrase.value ?? "",
109 );
110 expect(updatedValues).toContain("weather");
111 expect(updatedValues).toContain("forecast");
112
113 yield* stack.destroy();
114
115 const gone = yield* waitUntilGone(created.name);
116 expect(gone).toEqual("gone");
117 }).pipe(logLevel),
118 { tags: ["provider:gcp", "provider:gcp:speech", "live"], timeout: 90_000 },
119);