From daeb916a5710820e074ea0a6d98b0a16bc727bed Mon Sep 17 00:00:00 2001 From: smrht <62819456+smrht@users.noreply.github.com> Date: Sun, 6 Sep 2026 09:19:56 +0200 Subject: [PATCH 1/3] fix(openapi): fetch analyticsdata Discovery from the service host The central Discovery directory does not list the GA4 Data API, so https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest answers 404 and importing the source fails. Route analyticsdata to its own host the way forms, keep and photospicker already are. --- .changeset/great-hoops-repeat.md | 9 +++++++++ .../openapi/src/providers/google/discovery.test.ts | 10 ++++++++++ .../plugins/openapi/src/providers/google/discovery.ts | 1 + 3 files changed, 20 insertions(+) create mode 100644 .changeset/great-hoops-repeat.md diff --git a/.changeset/great-hoops-repeat.md b/.changeset/great-hoops-repeat.md new file mode 100644 index 0000000000..9943a92068 --- /dev/null +++ b/.changeset/great-hoops-repeat.md @@ -0,0 +1,9 @@ +--- +"@executor-js/plugin-openapi": patch +--- + +Fetch Google Analytics Data (`analyticsdata`) Discovery from the service's own +host. The central directory does not list the GA4 Data API, so the canonical +`https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest` answers +404 and the source fails to import. Same treatment `forms`, `keep` and +`photospicker` already get. diff --git a/packages/plugins/openapi/src/providers/google/discovery.test.ts b/packages/plugins/openapi/src/providers/google/discovery.test.ts index 748f8bc917..a6a6f18702 100644 --- a/packages/plugins/openapi/src/providers/google/discovery.test.ts +++ b/packages/plugins/openapi/src/providers/google/discovery.test.ts @@ -159,6 +159,11 @@ it("accepts only supported HTTPS Google Discovery endpoints", () => { "https://www.googleapis.com/discovery/v1/apis/photospicker/v1/rest", ), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); + expect( + normalizeGoogleDiscoveryUrl( + "https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest", + ), + ).toBe("https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta"); expect( normalizeGoogleDiscoveryUrl("https://www.googleapis.com/discovery/v1/apis/forms/v1/rest"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); @@ -171,6 +176,11 @@ it("accepts only supported HTTPS Google Discovery endpoints", () => { expect( normalizeGoogleDiscoveryUrl("https://photospicker.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://photospicker.googleapis.com/$discovery/rest?version=v1"); + expect( + normalizeGoogleDiscoveryUrl( + "https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta", + ), + ).toBe("https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta"); expect( normalizeGoogleDiscoveryUrl("https://forms.googleapis.com/$discovery/rest?version=v1"), ).toBe("https://forms.googleapis.com/$discovery/rest?version=v1"); diff --git a/packages/plugins/openapi/src/providers/google/discovery.ts b/packages/plugins/openapi/src/providers/google/discovery.ts index 9d79076986..8d346561c3 100644 --- a/packages/plugins/openapi/src/providers/google/discovery.ts +++ b/packages/plugins/openapi/src/providers/google/discovery.ts @@ -41,6 +41,7 @@ type GoogleDiscoveryServiceOverride = { }; const GOOGLE_DISCOVERY_SERVICE_OVERRIDES: Record = { + analyticsdata: { preserveServiceHostedUrl: true }, forms: { preserveServiceHostedUrl: true }, keep: { preserveServiceHostedUrl: true }, [GOOGLE_PHOTOS_PICKER_SERVICE]: { From 9e46dd8888cbc25bc7fcef5cb8b049afad2e8d38 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Sat, 12 Sep 2026 10:12:06 -0700 Subject: [PATCH 2/3] Test queue timeout with a controlled clock --- apps/cloud/src/mcp/session-build-semaphore.test.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/apps/cloud/src/mcp/session-build-semaphore.test.ts b/apps/cloud/src/mcp/session-build-semaphore.test.ts index 3d4ad76343..584b65ee0e 100644 --- a/apps/cloud/src/mcp/session-build-semaphore.test.ts +++ b/apps/cloud/src/mcp/session-build-semaphore.test.ts @@ -1,4 +1,4 @@ -import { describe, expect, it, beforeEach } from "@effect/vitest"; +import { describe, expect, it, beforeEach, afterEach, vi } from "@effect/vitest"; import { acquireBuildSlot, @@ -13,6 +13,10 @@ describe("session-build-semaphore", () => { resetBuildSlotsForTest(); }); + afterEach(() => { + vi.useRealTimers(); + }); + it("grants up to the cap immediately, with no wait", async () => { const results = await Promise.all([ acquireBuildSlot().promise, @@ -214,6 +218,7 @@ describe("session-build-semaphore", () => { }); it("proceeds without a slot when the queue wait exceeds the timeout, and does not count it as active", async () => { + vi.useFakeTimers(); await Promise.all([ acquireBuildSlot().promise, acquireBuildSlot().promise, @@ -223,6 +228,10 @@ describe("session-build-semaphore", () => { expect(currentActiveBuildsForTest()).toBe(4); const timedOutHandle = acquireBuildSlot(10); + await vi.advanceTimersByTimeAsync(9); + expect(currentQueueLengthForTest()).toBe(1); + expect(currentActiveBuildsForTest()).toBe(4); + await vi.advanceTimersByTimeAsync(1); const result = await timedOutHandle.promise; expect(result).toEqual({ acquired: false, waitMs: expect.any(Number), timedOut: true }); From c77bf8c7dfadf2bc183fdc437af5c706aea26bf5 Mon Sep 17 00:00:00 2001 From: Rhys Sullivan <39114868+RhysSullivan@users.noreply.github.com> Date: Sat, 12 Sep 2026 11:39:30 -0700 Subject: [PATCH 3/3] Verify Analytics Data discovery import end to end --- .../google-analyticsdata-discovery.test.ts | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 e2e/scenarios/google-analyticsdata-discovery.test.ts diff --git a/e2e/scenarios/google-analyticsdata-discovery.test.ts b/e2e/scenarios/google-analyticsdata-discovery.test.ts new file mode 100644 index 0000000000..5a18a94822 --- /dev/null +++ b/e2e/scenarios/google-analyticsdata-discovery.test.ts @@ -0,0 +1,57 @@ +import { randomBytes, randomUUID } from "node:crypto"; +import { expect } from "@effect/vitest"; +import { Effect } from "effect"; +import { composePluginApi } from "@executor-js/api/server"; +import { openApiHttpPlugin } from "@executor-js/plugin-openapi/api"; +import { + AuthTemplateSlug, + ConnectionName, + IntegrationSlug, + ProviderItemId, +} from "@executor-js/sdk/shared"; + +import { scenario } from "../src/scenario"; +import { Api, Target } from "../src/services"; + +const api = composePluginApi([openApiHttpPlugin()] as const); + +scenario( + "Google Analytics Data ยท public Discovery import exposes report tools", + { timeout: 120_000 }, + Effect.gen(function* () { + const target = yield* Target; + const { client } = yield* Api; + const identity = yield* target.newIdentity(); + const apiClient = yield* client(api, identity); + const slug = IntegrationSlug.make(`analytics_${randomBytes(4).toString("hex")}`); + // Fetch Google's public document through the real server import path. + // No Analytics account or authenticated report request is needed. + const added = yield* apiClient.openapi.addSpec({ + payload: { + slug, + spec: { + kind: "url", + url: "https://www.googleapis.com/discovery/v1/apis/analyticsdata/v1beta/rest", + }, + }, + }); + yield* Effect.gen(function* () { + expect(added.toolCount).toBeGreaterThan(0); + const providers = yield* apiClient.providers.list(); + const provider = providers[0]; + if (provider === undefined) return yield* Effect.die("No credential provider available"); + yield* apiClient.connections.create({ + payload: { + owner: "org", + name: ConnectionName.make("main"), + integration: slug, + template: AuthTemplateSlug.make("googleOAuth2"), + from: { provider, id: ProviderItemId.make(randomUUID()) }, + }, + }); + const tools = yield* apiClient.tools.list({ query: { integration: slug } }); + expect(tools.some((tool) => tool.name.endsWith("runReport"))).toBe(true); + expect(tools.some((tool) => tool.name.endsWith("runRealtimeReport"))).toBe(true); + }).pipe(Effect.ensuring(apiClient.openapi.removeSpec({ params: { slug } }).pipe(Effect.orDie))); + }), +);