1import * as GCP from "@/GCP";
2import { GcpEnvironment } from "@/GCP/Environment";
3import * as Test from "@/Test/Alchemy";
4import * as cci from "@distilled.cloud/gcp/contactcenterinsights_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 datasetId = "alchemy-cci-label-ds";
18const conversationId = "alchemy-cci-ds-label-conv";
19const namesOf = (project: string) => {
20 const locationParent = `projects/${project}/locations/us-central1`;
21 const datasetName = `${locationParent}/datasets/${datasetId}`;
22 const conversationName = `${datasetName}/conversations/${conversationId}`;
23 return { locationParent, datasetName, conversationName };
24};
25
26const waitUntilGone = (name: string) =>
27 cci.getProjectsLocationsDatasetsConversationsFeedbackLabels({ name }).pipe(
28 Effect.as("found" as const),
29 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
30 Effect.repeat({
31 schedule: Schedule.spaced("1 second"),
32 until: (status) => status === "gone",
33 times: 10,
34 }),
35 );
36
37const waitUntilDatasetGone = (name: string) =>
38 cci.getProjectsLocationsDatasets({ name }).pipe(
39 Effect.as("found" as const),
40 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
41 Effect.repeat({
42 schedule: Schedule.spaced("2 seconds"),
43 until: (status) => status === "gone",
44 times: 10,
45 }),
46 );
47
48const ensureDataset = ({
49 locationParent,
50 datasetName,
51}: ReturnType<typeof namesOf>) =>
52 cci.getProjectsLocationsDatasets({ name: datasetName }).pipe(
53 Effect.catchTag("NotFound", () =>
54 cci.createProjectsLocationsDatasets({
55 parent: locationParent,
56 datasetId,
57 body: {
58 displayName: "alchemy-cci-label-ds",
59 type: "EVAL",
60 description: "alchemy test dataset",
61 },
62 }),
63 ),
64 );
65
66const ensureConversation = ({
67 datasetName,
68 conversationName,
69}: ReturnType<typeof namesOf>) =>
70 cci
71 .getProjectsLocationsDatasetsConversations({ name: conversationName })
72 .pipe(
73 Effect.catchTag("NotFound", () =>
74 cci.createProjectsLocationsConversations({
75 parent: datasetName,
76 conversationId,
77 body: {
78 medium: "CHAT",
79 languageCode: "en-US",
80 labels: { "alchemy-test": "cci" },
81 },
82 }),
83 ),
84 );
85
86const deleteParents = ({
87 datasetName,
88 conversationName,
89}: ReturnType<typeof namesOf>) =>
90 Effect.gen(function* () {
91 yield* cci
92 .deleteProjectsLocationsDatasetsConversations({
93 name: conversationName,
94 force: true,
95 })
96 .pipe(Effect.catchTag("NotFound", () => Effect.void));
97 const deleted = yield* cci
98 .deleteProjectsLocationsDatasets({ name: datasetName })
99 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
100 if (deleted !== undefined) {
101 yield* waitUntilDatasetGone(datasetName);
102 }
103 });
104
105test.provider(
106 "getProjectsLocationsDatasetsConversationsFeedbackLabels on a missing label fails with a typed tag",
107 (stack) =>
108 Effect.gen(function* () {
109 const { project } = yield* GcpEnvironment.current;
110 const names = namesOf(project);
111 const { conversationName } = names;
112 yield* stack.destroy();
113
114 const error = yield* Effect.flip(
115 cci.getProjectsLocationsDatasetsConversationsFeedbackLabels({
116 name: `${conversationName}/feedbackLabels/missing`,
117 }),
118 );
119 expect(error._tag).toEqual("NotFound");
120
121 yield* stack.destroy();
122 }).pipe(logLevel),
123 {
124 tags: ["provider:gcp", "provider:gcp:contactcenterinsights", "live"],
125 timeout: 90_000,
126 },
127);
128
129// The fixture creates its dataset conversation with
130// createProjectsLocationsConversations under a dataset parent, which the
131// testing project answers with NotFound "404"; dataset conversations have
132// to be ingested. Set GCP_TEST_CCI_DATASETS=1 where that path works.
133test.provider.skipIf(!!process.env.FAST || !process.env.GCP_TEST_CCI_DATASETS)(
134 "create, update, and delete a dataset conversation feedback label",
135 (stack) =>
136 Effect.gen(function* () {
137 const { project } = yield* GcpEnvironment.current;
138 const names = namesOf(project);
139 const { conversationName } = names;
140 yield* stack.destroy();
141 yield* ensureDataset(names);
142 const conversation = yield* ensureConversation(names);
143
144 const created = yield* stack.deploy(
145 Effect.gen(function* () {
146 return yield* GCP.ContactCenterInsights.DatasetsConversationsFeedbackLabel(
147 "Topic",
148 {
149 parent: conversation.name ?? conversationName,
150 label: "billing",
151 },
152 );
153 }),
154 );
155
156 expect(created.name).toContain("/feedbackLabels/");
157 expect(created.label).toEqual("billing");
158
159 const fetched =
160 yield* cci.getProjectsLocationsDatasetsConversationsFeedbackLabels({
161 name: created.name,
162 });
163 expect(fetched.name).toEqual(created.name);
164 expect(fetched.label).toContain("alchemy-id=");
165
166 const updated = yield* stack.deploy(
167 Effect.gen(function* () {
168 return yield* GCP.ContactCenterInsights.DatasetsConversationsFeedbackLabel(
169 "Topic",
170 {
171 parent: conversation.name ?? conversationName,
172 feedbackLabelId: created.feedbackLabelId,
173 label: "invoices",
174 },
175 );
176 }),
177 );
178 expect(updated.name).toEqual(created.name);
179 expect(updated.label).toEqual("invoices");
180
181 yield* stack.destroy();
182 const gone = yield* waitUntilGone(created.name);
183 expect(gone).toEqual("gone");
184 yield* deleteParents(names);
185 }).pipe(logLevel),
186 {
187 tags: ["provider:gcp", "provider:gcp:contactcenterinsights", "live"],
188 timeout: 120_000,
189 },
190);