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 = (tenant: string, name: string) =>
26 discoveryengine
27 .getProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
28 { tenant, name },
29 )
30 .pipe(
31 Effect.as("found" as const),
32 Effect.catchTag("NotFound", () => Effect.succeed("gone" as const)),
33 Effect.repeat({
34 schedule: Schedule.spaced("2 seconds"),
35 until: (status) => status === "gone",
36 times: 10,
37 }),
38 );
39
40test.provider(
41 "getProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs on a missing config fails with a typed tag",
42 (stack) =>
43 Effect.gen(function* () {
44 const { project } = yield* GcpEnvironment.current;
45
46 yield* stack.destroy();
47
48 const error = yield* Effect.flip(
49 discoveryengine.getProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
50 {
51 tenant: `projects/${project}/locations/global/collections/default_collection/engines/alchemy-missing/assistants/alchemy-missing`,
52 name: "tasks/alchemy-missing/pushNotificationConfigs/alchemy-missing",
53 },
54 ),
55 );
56 expect(error._tag).toEqual("NotFound");
57
58 yield* stack.destroy();
59 }).pipe(logLevel, quotaTolerant),
60 {
61 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
62 timeout: 90_000,
63 },
64);
65
66test.provider.skipIf(!runLifecycle)(
67 "create, update, and delete an A2A push notification config",
68 (stack) =>
69 Effect.gen(function* () {
70 yield* stack.destroy();
71
72 const created = yield* stack.deploy(
73 Effect.gen(function* () {
74 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
75 "Docs",
76 {
77 location: "global",
78 displayName: "a2a-docs",
79 },
80 );
81 const engine = yield* GCP.DiscoveryEngine.CollectionsEngine(
82 "Search",
83 {
84 location: "global",
85 dataStoreIds: [store.dataStoreId],
86 displayName: "a2a engine",
87 },
88 );
89 const assistant =
90 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant("Help", {
91 engine: engine.name,
92 // Engines accept only their default assistant.
93 assistantId: "default_assistant",
94 displayName: "a2a helper",
95 });
96 const config =
97 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig(
98 "Notify",
99 {
100 tenant: assistant.name,
101 parent: "tasks/alchemy-test-task",
102 url: "https://example.com/hooks/a2a",
103 },
104 );
105 return { store, engine, assistant, config };
106 }),
107 );
108
109 expect(created.config.name).toContain("pushNotificationConfigs");
110 expect(created.config.tenant).toEqual(created.assistant.name);
111 expect(created.config.url).toEqual("https://example.com/hooks/a2a");
112
113 const fetched =
114 yield* discoveryengine.getProjectsLocationsCollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfigs(
115 { tenant: created.config.tenant, name: created.config.name },
116 );
117 expect(fetched.name).toEqual(created.config.name);
118
119 const updated = yield* stack.deploy(
120 Effect.gen(function* () {
121 const store = yield* GCP.DiscoveryEngine.CollectionsDataStore(
122 "Docs",
123 {
124 dataStoreId: created.store.dataStoreId,
125 location: "global",
126 displayName: "a2a-docs",
127 },
128 );
129 const engine = yield* GCP.DiscoveryEngine.CollectionsEngine(
130 "Search",
131 {
132 engineId: created.engine.engineId,
133 location: "global",
134 dataStoreIds: [store.dataStoreId],
135 displayName: "a2a engine",
136 },
137 );
138 const assistant =
139 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistant("Help", {
140 engine: engine.name,
141 assistantId: created.assistant.assistantId,
142 displayName: "a2a helper",
143 });
144 const config =
145 yield* GCP.DiscoveryEngine.CollectionsEnginesAssistantsAgentsA2aV1TasksPushNotificationConfig(
146 "Notify",
147 {
148 tenant: assistant.name,
149 parent: "tasks/alchemy-test-task",
150 configId: created.config.configId,
151 url: "https://example.com/hooks/a2a-prod",
152 },
153 );
154 return { store, engine, assistant, config };
155 }),
156 );
157
158 expect(updated.config.url).toEqual("https://example.com/hooks/a2a-prod");
159
160 yield* stack.destroy();
161
162 const gone = yield* waitUntilGone(
163 created.config.tenant,
164 created.config.name,
165 );
166 expect(gone).toEqual("gone");
167 }).pipe(logLevel, quotaTolerant),
168 {
169 tags: ["provider:gcp", "provider:gcp:discoveryengine", "live"],
170 timeout: 120_000,
171 },
172);