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
18// createAssistants is rejected on a plain search engine with BadRequest "Only
19// default_assistant is allowed." even for assistantId default_assistant; it
20// needs a Gemini Enterprise (Agentspace) engine. Set
21// GCP_TEST_DISCOVERYENGINE_ASSISTANT=1 on a project with one.
22const runLifecycle =
23 !process.env.FAST && !!process.env.GCP_TEST_DISCOVERYENGINE_ASSISTANT;
24
25const waitUntilGone = (name: string) =>
26 discoveryengine
27 .getProjectsLocationsCollectionsEnginesAssistants({ 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 "getProjectsLocationsCollectionsEnginesAssistants on a missing assistant 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.getProjectsLocationsCollectionsEnginesAssistants({
48 name: `projects/${project}/locations/global/collections/default_collection/engines/alchemy-missing/assistants/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)(
62 "create, update, and delete an engine assistant",
63 (stack) =>
64 Effect.gen(function* () {
65 yield* stack.destroy();
66
67 const created = yield* stack.deploy(
68 Effect.gen(function* () {
69 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
70 "Docs",
71 {
72 location: "global",
73 displayName: "assistant-docs",
74 },
75 );
76 const engine = yield* GCP.DiscoveryEngine.CollectionsEngine(
77 "Search",
78 {
79 location: "global",
80 dataStoreIds: [store.dataStoreId],
81 displayName: "assistant engine",
82 },
83 );
84 const assistant =
85 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant("Help", {
86 engine: engine.name,
87 // Engines accept only their default assistant.
88 assistantId: "default_assistant",
89 displayName: "docs helper",
90 description: "answers",
91 });
92 return { store, engine, assistant };
93 }),
94 );
95
96 expect(created.assistant.name).toContain("/assistants/");
97 expect(created.assistant.engine).toEqual(created.engine.name);
98 expect(created.assistant.displayName).toEqual("docs helper");
99 expect(created.assistant.description).toEqual("answers");
100
101 const fetched =
102 yield* discoveryengine.getProjectsLocationsCollectionsEnginesAssistants(
103 { name: created.assistant.name },
104 );
105 expect(fetched.name).toEqual(created.assistant.name);
106 expect(fetched.description).toEqual("answers");
107
108 const updated = yield* stack.deploy(
109 Effect.gen(function* () {
110 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
111 "Docs",
112 {
113 dataStoreId: created.store.dataStoreId,
114 location: "global",
115 displayName: "assistant-docs",
116 },
117 );
118 const engine = yield* GCP.DiscoveryEngine.CollectionsEngine(
119 "Search",
120 {
121 engineId: created.engine.engineId,
122 location: "global",
123 dataStoreIds: [store.dataStoreId],
124 displayName: "assistant engine",
125 },
126 );
127 const assistant =
128 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant("Help", {
129 engine: engine.name,
130 assistantId: created.assistant.assistantId,
131 displayName: "docs helper prod",
132 description: "answers prod",
133 });
134 return { store, engine, assistant };
135 }),
136 );
137
138 expect(updated.assistant.name).toEqual(created.assistant.name);
139 expect(updated.assistant.displayName).toEqual("docs helper prod");
140 expect(updated.assistant.description).toEqual("answers prod");
141
142 yield* stack.destroy();
143
144 const gone = yield* waitUntilGone(created.assistant.name);
145 expect(gone).toEqual("gone");
146 }).pipe(logLevel, quotaTolerant),
147 {
148 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
149 timeout: 120_000,
150 },
151);