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";
9import { uploadChatTranscript } from "./transcript.ts";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const transcriptBucketOf = (project: string) =>
19 `alchemy-cci-transcripts-${project}`;
20const conversationId = "alchemy-cci-analyses-conv";
21const conversationNameOf = (project: string) =>
22 `projects/${project}/locations/us-central1/conversations/${conversationId}`;
23
24const waitUntilGone = (name: string) =>
25 cci.getProjectsLocationsConversationsAnalyses({ name }).pipe(
26 Effect.as("found" as const),
27 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
28 Effect.repeat({
29 schedule: Schedule.spaced("2 seconds"),
30 until: (status) => status === "gone",
31 times: 10,
32 }),
33 );
34
35const ensureConversation = (project: string) =>
36 Effect.gen(function* () {
37 const transcriptBucket = transcriptBucketOf(project);
38 const conversationName = conversationNameOf(project);
39 yield* uploadChatTranscript(transcriptBucket);
40 const existing = yield* cci
41 .getProjectsLocationsConversations({
42 name: conversationName,
43 view: "BASIC",
44 })
45 .pipe(Effect.catchTag("NotFound", () => Effect.succeed(undefined)));
46 if (existing !== undefined) return existing;
47 return yield* cci.createProjectsLocationsConversations({
48 parent: `projects/${project}/locations/us-central1`,
49 conversationId,
50 body: {
51 medium: "CHAT",
52 languageCode: "en-US",
53 labels: { "alchemy-test": "cci" },
54 dataSource: {
55 gcsSource: {
56 transcriptUri: `gs://${transcriptBucket}/transcript.json`,
57 },
58 },
59 },
60 });
61 });
62
63const deleteConversation = (project: string) =>
64 cci
65 .deleteProjectsLocationsConversations({
66 name: conversationNameOf(project),
67 force: true,
68 })
69 .pipe(Effect.catchTag("NotFound", () => Effect.void));
70
71test.provider(
72 "getProjectsLocationsConversationsAnalyses on a missing analysis fails with a typed tag",
73 (stack) =>
74 Effect.gen(function* () {
75 const { project } = yield* GcpEnvironment.current;
76 const conversationName = conversationNameOf(project);
77 yield* stack.destroy();
78
79 const error = yield* Effect.flip(
80 cci.getProjectsLocationsConversationsAnalyses({
81 name: `${conversationName}/analyses/missing`,
82 }),
83 );
84 expect(error._tag).toEqual("NotFound");
85
86 yield* stack.destroy();
87 }).pipe(logLevel),
88 {
89 tags: ["provider:gcp", "provider:gcp:contactcenterinsights", "live"],
90 timeout: 90_000,
91 },
92);
93
94test.provider.skipIf(!!process.env.FAST)(
95 "create and delete a conversation analysis",
96 (stack) =>
97 Effect.gen(function* () {
98 const { project } = yield* GcpEnvironment.current;
99 const conversationName = conversationNameOf(project);
100 yield* stack.destroy();
101 const conversation = yield* ensureConversation(project);
102
103 const created = yield* stack.deploy(
104 Effect.gen(function* () {
105 return yield* GCP.ContactCenterInsights.ConversationsAnalyses(
106 "Silence",
107 {
108 parent: conversation.name ?? conversationName,
109 annotatorSelector: { runSilenceAnnotator: true },
110 },
111 );
112 }),
113 );
114
115 expect(created.name).toContain("/analyses/");
116
117 const fetched = yield* cci.getProjectsLocationsConversationsAnalyses({
118 name: created.name,
119 });
120 expect(fetched.name).toEqual(created.name);
121
122 yield* stack.destroy();
123 const gone = yield* waitUntilGone(created.name);
124 expect(gone).toEqual("gone");
125 yield* deleteConversation(project);
126 }).pipe(logLevel),
127 {
128 tags: ["provider:gcp", "provider:gcp:contactcenterinsights", "live"],
129 timeout: 120_000,
130 },
131);