History

Fix Google Discovery execution base URL

Rhys Sullivan committed 7 months ago

Commit c128df31 parent 4758a443 files · +209 −6Browse files

Unified · Split

3 files
  1. .changeset/rude-shrimps-teach.md +5 −0
  2. packages/sources/google-discovery/src/adapter.test.ts +202 −0
  3. packages/sources/google-discovery/src/adapter.ts +2 −6
.changeset/rude-shrimps-teach.md+5−0
@@ -0,0 +1,5 @@
1+---
2+"executor": patch
3+---
4+
5+Fix Google Discovery tool execution for sources stored with discovery document endpoints
packages/sources/google-discovery/src/adapter.test.ts+202−0
@@ -0,0 +1,202 @@
1+import { createServer } from "node:http";
2+
3+import { describe, expect, it } from "@effect/vitest";
4+import {
5+ EXECUTABLE_BINDING_VERSION,
6+ type Source,
7+} from "@executor/source-core";
8+import * as Effect from "effect/Effect";
9+
10+import { googleDiscoverySourceAdapter } from "./adapter";
11+import { googleDiscoveryProviderDataFromDefinition } from "./tools";
12+
13+const withJsonServer = async <T>(handler: (input: {
14+ baseUrl: string;
15+ requests: Array<{
16+ method: string;
17+ url: string;
18+ headers: Record<string, string | string[] | undefined>;
19+ body: string;
20+ }>;
21+}) => Promise<T>): Promise<T> => {
22+ const requests: Array<{
23+ method: string;
24+ url: string;
25+ headers: Record<string, string | string[] | undefined>;
26+ body: string;
27+ }> = [];
28+ const server = createServer(async (request, response) => {
29+ const chunks: Buffer[] = [];
30+ for await (const chunk of request) {
31+ chunks.push(Buffer.from(chunk));
32+ }
33+ requests.push({
34+ method: request.method ?? "GET",
35+ url: request.url ?? "/",
36+ headers: request.headers,
37+ body: Buffer.concat(chunks).toString("utf8"),
38+ });
39+
40+ response.statusCode = 200;
41+ response.setHeader("content-type", "application/json");
42+ response.end(
43+ JSON.stringify({
44+ ok: true,
45+ url: request.url ?? "/",
46+ }),
47+ );
48+ });
49+
50+ await new Promise<void>((resolve, reject) => {
51+ server.listen(0, "127.0.0.1", (error?: Error) => {
52+ if (error) {
53+ reject(error);
54+ return;
55+ }
56+ resolve();
57+ });
58+ });
59+
60+ try {
61+ const address = server.address();
62+ if (!address || typeof address === "string") {
63+ throw new Error("Failed to resolve local adapter test server address");
64+ }
65+ return await handler({
66+ baseUrl: `http://127.0.0.1:${address.port}`,
67+ requests,
68+ });
69+ } finally {
70+ await new Promise<void>((resolve, reject) => {
71+ server.close((error) => {
72+ if (error) {
73+ reject(error);
74+ return;
75+ }
76+ resolve();
77+ });
78+ });
79+ }
80+};
81+
82+const makeGoogleDiscoverySource = (discoveryUrl: string): Source => ({
83+ id: "src_google_gmail",
84+ workspaceId: "ws_test",
85+ name: "Gmail",
86+ kind: "google_discovery",
87+ endpoint: discoveryUrl,
88+ status: "connected",
89+ enabled: true,
90+ namespace: "google.gmail",
91+ bindingVersion: 1,
92+ binding: {
93+ service: "gmail",
94+ version: "v1",
95+ discoveryUrl,
96+ defaultHeaders: null,
97+ scopes: [],
98+ },
99+ importAuthPolicy: "reuse_runtime",
100+ importAuth: { kind: "none" },
101+ auth: { kind: "none" },
102+ sourceHash: null,
103+ lastError: null,
104+ createdAt: 1,
105+ updatedAt: 1,
106+});
107+
108+describe("google discovery source adapter", () => {
109+ it("invokes requests against the discovery document runtime root instead of the stored discovery endpoint", () =>
110+ Effect.tryPromise({
111+ try: async () => {
112+ await withJsonServer(async ({ baseUrl, requests }) => {
113+ const discoveryUrl = `${baseUrl}/$discovery/rest?version=v1`;
114+ const source = makeGoogleDiscoverySource(discoveryUrl);
115+ const providerData = googleDiscoveryProviderDataFromDefinition({
116+ service: "gmail",
117+ version: "v1",
118+ rootUrl: `${baseUrl}/`,
119+ servicePath: "gmail/v1/",
120+ definition: {
121+ toolId: "users.messages.list",
122+ rawToolId: "users.messages.list",
123+ methodId: "gmail.users.messages.list",
124+ name: "List Messages",
125+ description: null,
126+ group: "users.messages",
127+ leaf: "list",
128+ method: "get",
129+ path: "users/{userId}/messages",
130+ flatPath: null,
131+ parameters: [
132+ {
133+ name: "userId",
134+ location: "path",
135+ required: true,
136+ repeated: false,
137+ description: null,
138+ type: "string",
139+ },
140+ {
141+ name: "labelIds",
142+ location: "query",
143+ required: false,
144+ repeated: true,
145+ description: null,
146+ type: "string",
147+ },
148+ ],
149+ requestSchemaId: null,
150+ responseSchemaId: null,
151+ scopes: [],
152+ supportsMediaUpload: false,
153+ supportsMediaDownload: false,
154+ },
155+ });
156+
157+ const result = await Effect.runPromise(
158+ googleDiscoverySourceAdapter.invoke({
159+ source,
160+ capability: {} as any,
161+ executable: {
162+ id: "exe_google_gmail_list",
163+ bindingVersion: EXECUTABLE_BINDING_VERSION,
164+ binding: providerData,
165+ } as any,
166+ descriptor: {} as any,
167+ catalog: {} as any,
168+ args: {
169+ userId: "me",
170+ labelIds: ["INBOX", "UNREAD"],
171+ },
172+ auth: {
173+ placements: [],
174+ headers: {
175+ authorization: "Bearer live-test-token",
176+ },
177+ queryParams: {},
178+ cookies: {},
179+ bodyValues: {},
180+ expiresAt: null,
181+ refreshAfter: null,
182+ },
183+ }),
184+ );
185+
186+ expect(result.status).toBe(200);
187+ expect(result.error).toBeNull();
188+ expect(result.data).toEqual({
189+ ok: true,
190+ url: "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD",
191+ });
192+ expect(requests).toHaveLength(1);
193+ expect(requests[0]?.url).toBe(
194+ "/gmail/v1/users/me/messages?labelIds=INBOX&labelIds=UNREAD",
195+ );
196+ expect(requests[0]?.headers.authorization).toBe("Bearer live-test-token");
197+ });
198+ },
199+ catch: (cause) =>
200+ cause instanceof Error ? cause : new Error(String(cause)),
201+ }));
202+});
packages/sources/google-discovery/src/adapter.ts+2−6
@@ -266,14 +266,11 @@ const replacePathParameters = (input: { · 265 unmodified lines
266266 return encodeURIComponent(values[0]!);
267267 });
268268
269+// `source.endpoint` stores the discovery document URL, not the runtime API
270+// root. Always derive invocation targets from the discovery document metadata.
269271 const resolveGoogleDiscoveryBaseUrl = (input: {
270272 providerData: GoogleDiscoveryToolProviderData;
271− baseUrl?: string;
272273 }): string => {
273− if (input.baseUrl) {
274− return new URL(input.baseUrl).toString();
275− }
276−
277274 return new URL(
278275 input.providerData.invocation.servicePath || "",
279276 input.providerData.invocation.rootUrl,
@@ -618,7 +615,6 @@ export const googleDiscoverySourceAdapter = { · 338 unmodified lines
618615 resolvedPath.replace(/^\//, ""),
619616 resolveGoogleDiscoveryBaseUrl({
620617 providerData,
621− baseUrl: input.source.endpoint,
622618 }),
623619 );
624620 const headers: Record<string, string> = {