1import { GcpEnvironment } from "@/GCP/Environment";
2import * as GCP from "@/GCP";
3import { quotaTolerant } from "./parent.ts";
4import * as Test from "@/Test/Alchemy";
5import * as discoveryengine from "@distilled.cloud/gcp/discoveryengine_v1";
6import { expect } from "alchemy-test";
7import * as Effect from "effect/Effect";
8import { MinimumLogLevel } from "effect/References";
9import * as Schedule from "effect/Schedule";
10
11const { test } = Test.make({ providers: GCP.providers() });
12
13const logLevel = Effect.provideService(
14 MinimumLogLevel,
15 process.env.DEBUG ? "Debug" : "Info",
16);
17
18const runLifecycle = !process.env.FAST;
19// Conversations need the Large Language Model add-on (BadRequest "This
20// feature is only available when Large Language Model add-on is enabled.").
21// Set GCP_TEST_DISCOVERYENGINE_LLM=1 on a project with the add-on.
22const runLlmLifecycle =
23 runLifecycle && !!process.env.GCP_TEST_DISCOVERYENGINE_LLM;
24
25const waitUntilGone = (name: string) =>
26 discoveryengine
27 .getProjectsLocationsCollectionsDataStoresConversations({ name })
28 .pipe(
29 Effect.as("found" as const),
30 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
31 Effect.repeat({
32 schedule: Schedule.spaced("2 seconds"),
33 until: (status) => status === "gone",
34 times: 10,
35 }),
36 );
37
38test.provider(
39 "getProjectsLocationsCollectionsDataStoresConversations on a missing conversation fails with a typed tag",
40 (stack) =>
41 Effect.gen(function* () {
42 const { project } = yield* GcpEnvironment.current;
43
44 yield* stack.destroy();
45
46 const error = yield* Effect.flip(
47 discoveryengine.getProjectsLocationsCollectionsDataStoresConversations({
48 name: `projects/${project}/locations/global/collections/default_collection/dataStores/alchemy-missing/conversations/alchemy-missing`,
49 }),
50 );
51 expect(error._tag).toEqual("NotFound");
52
53 yield* stack.destroy();
54 }).pipe(logLevel, quotaTolerant),
55 {
56 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
57 timeout: 90_000,
58 },
59);
60
61test.provider.skipIf(!runLifecycle || runLlmLifecycle)(
62 "create conversation without LLM add-on fails with LlmAddOnRequired",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const store = yield* stack.deploy(
68 Effect.gen(function* () {
69 return yield* GCP.DiscoveryEngine.CollectionsDataStore("Docs", {
70 location: "global",
71 displayName: "docs",
72 disableCmek: true,
73 });
74 }),
75 );
76
77 const error = yield* Effect.flip(
78 discoveryengine.createProjectsLocationsCollectionsDataStoresConversations(
79 {
80 parent: store.name,
81 body: { userPseudoId: "probe", state: "IN_PROGRESS" },
82 },
83 ),
84 );
85 expect(error._tag).toEqual("LlmAddOnRequired");
86
87 yield* stack.destroy();
88 }).pipe(logLevel, quotaTolerant),
89 {
90 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
91 timeout: 120_000,
92 },
93);
94
95test.provider.skipIf(!runLlmLifecycle)(
96 "create, update, and delete a collection data store conversation",
97 (stack) =>
98 Effect.gen(function* () {
99 yield* stack.destroy();
100
101 const created = yield* stack.deploy(
102 Effect.gen(function* () {
103 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
104 "Docs",
105 {
106 location: "global",
107 displayName: "docs",
108 disableCmek: true,
109 },
110 );
111 const conversation =
112 yield* GCP.DiscoveryEngine.CollectionsDataStoresConversation(
113 "Visitor",
114 {
115 dataStore: store.name,
116 userPseudoId: "user-1",
117 state: "IN_PROGRESS",
118 },
119 );
120 return { store, conversation };
121 }),
122 );
123
124 expect(created.conversation.name).toContain("/conversations/");
125 expect(created.conversation.userPseudoId).toEqual("user-1");
126
127 const fetched =
128 yield* discoveryengine.getProjectsLocationsCollectionsDataStoresConversations(
129 { name: created.conversation.name },
130 );
131 expect(fetched.name).toEqual(created.conversation.name);
132 expect(fetched.userPseudoId).toContain("alc-");
133
134 const updated = yield* stack.deploy(
135 Effect.gen(function* () {
136 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
137 "Docs",
138 {
139 dataStoreId: created.store.dataStoreId,
140 location: "global",
141 collection: created.store.collection,
142 displayName: "docs",
143 disableCmek: true,
144 },
145 );
146 const conversation =
147 yield* GCP.DiscoveryEngine.CollectionsDataStoresConversation(
148 "Visitor",
149 {
150 dataStore: store.name,
151 userPseudoId: "user-2",
152 state: "COMPLETED",
153 },
154 );
155 return { store, conversation };
156 }),
157 );
158
159 expect(updated.conversation.name).toEqual(created.conversation.name);
160 expect(updated.conversation.userPseudoId).toEqual("user-2");
161 expect(updated.conversation.state).toEqual("COMPLETED");
162
163 yield* stack.destroy();
164
165 const gone = yield* waitUntilGone(created.conversation.name);
166 expect(gone).toEqual("gone");
167 }).pipe(logLevel, quotaTolerant),
168 {
169 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
170 timeout: 120_000,
171 },
172);