Unified · Split
| @@ -29,6 +29,7 @@ · 28 unmodified lines | |||
| 29 | "@executor-js/plugin-mcp": "workspace:*", | 29 | "@executor-js/plugin-mcp": "workspace:*", |
| 30 | "@executor-js/plugin-microsoft": "workspace:*", | 30 | "@executor-js/plugin-microsoft": "workspace:*", |
| 31 | "@executor-js/plugin-openapi": "workspace:*", | 31 | "@executor-js/plugin-openapi": "workspace:*", |
| 32 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 32 | "@executor-js/plugin-toolkits": "workspace:*", | 33 | "@executor-js/plugin-toolkits": "workspace:*", |
| 33 | "@executor-js/react": "workspace:*", | 34 | "@executor-js/react": "workspace:*", |
| 34 | "@executor-js/runtime-quickjs": "workspace:*", | 35 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -12,8 +12,8 @@ import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google" · 11 unmodified lines | |||
| 12 | import { | 12 | import { |
| 13 | providerServiceSplitDataMigration, | 13 | providerServiceSplitDataMigration, |
| 14 | runSqliteProviderServiceSplitMigration, | 14 | runSqliteProviderServiceSplitMigration, |
| 15 | } from "../../../../scripts/migration/service-split-sqlite"; | 15 | type BlobRow, |
| 16 | import type { BlobRow } from "../../../../scripts/migration/service-split-planner"; | 16 | } from "@executor-js/plugin-provider-service-split"; |
| 17 | 17 | ||
| 18 | const TX_CONTROL = new Set(["BEGIN", "BEGIN TRANSACTION", "COMMIT", "ROLLBACK"]); | 18 | const TX_CONTROL = new Set(["BEGIN", "BEGIN TRANSACTION", "COMMIT", "ROLLBACK"]); |
| 19 | 19 | ||
| @@ -6,6 +6,7 @@ import { fileURLToPath } from "node:url"; · 5 unmodified lines | |||
| 6 | import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"; | 6 | import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest"; |
| 7 | import { Schema } from "effect"; | 7 | import { Schema } from "effect"; |
| 8 | import { unstable_dev, type Unstable_DevWorker } from "wrangler"; | 8 | import { unstable_dev, type Unstable_DevWorker } from "wrangler"; |
| 9 | import { microsoftCatalog } from "@executor-js/plugin-microsoft"; | ||
| 9 | 10 | ||
| 10 | // --------------------------------------------------------------------------- | 11 | // --------------------------------------------------------------------------- |
| 11 | // End-to-end test for the Cloudflare host: boots the REAL worker on workerd via | 12 | // End-to-end test for the Cloudflare host: boots the REAL worker on workerd via |
| @@ -167,13 +168,48 @@ describe("cloudflare host e2e (workerd/miniflare)", () => { · 155 unmodified lines | |||
| 167 | expect(integration?.slug).toBe(slug); | 168 | expect(integration?.slug).toBe(slug); |
| 168 | }, 60_000); | 169 | }, 60_000); |
| 169 | 170 | ||
| 170 | it("registers the Microsoft Graph API route in the runtime plugin set", async () => { | 171 | it("exposes Microsoft catalog presets through the OpenAPI integration catalog", async () => { |
| 171 | const res = await worker.fetch("/api/microsoft/graph", { | 172 | const microsoftFilesPreset = microsoftCatalog.find( |
| 173 | (preset) => preset.defaultSlug === "microsoft_files", | ||
| 174 | ); | ||
| 175 | expect(microsoftFilesPreset).toBeTruthy(); | ||
| 176 | |||
| 177 | const removedProviderRoute = await worker.fetch("/api/microsoft/graph", { | ||
| 172 | method: "POST", | 178 | method: "POST", |
| 173 | headers: { "content-type": "application/json" }, | 179 | headers: { "content-type": "application/json" }, |
| 174 | body: JSON.stringify({}), | 180 | body: JSON.stringify({}), |
| 175 | }); | 181 | }); |
| 176 | expect(res.status).toBe(400); | 182 | expect(removedProviderRoute.status).toBe(404); |
| 183 | |||
| 184 | const slug = `microsoft-files-${runId}`; | ||
| 185 | const add = await worker.fetch("/api/openapi/specs", { | ||
| 186 | method: "POST", | ||
| 187 | headers: { "content-type": "application/json" }, | ||
| 188 | body: JSON.stringify({ | ||
| 189 | spec: { kind: "blob", value: SPEC }, | ||
| 190 | slug, | ||
| 191 | name: microsoftFilesPreset?.name, | ||
| 192 | description: microsoftFilesPreset?.summary, | ||
| 193 | family: microsoftFilesPreset?.family, | ||
| 194 | authenticationTemplate: microsoftFilesPreset?.authTemplate, | ||
| 195 | }), | ||
| 196 | }); | ||
| 197 | expect(add.status).toBe(200); | ||
| 198 | |||
| 199 | const res = await worker.fetch(`/api/integrations/${slug}`); | ||
| 200 | expect(res.status).toBe(200); | ||
| 201 | const integration = (await res.json()) as { | ||
| 202 | readonly slug: string; | ||
| 203 | readonly kind: string; | ||
| 204 | readonly family?: string; | ||
| 205 | readonly authMethods: readonly { readonly template: string; readonly kind: string }[]; | ||
| 206 | }; | ||
| 207 | expect(integration.slug).toBe(slug); | ||
| 208 | expect(integration.kind).toBe("openapi"); | ||
| 209 | expect(integration.family).toBe("microsoft"); | ||
| 210 | expect(integration.authMethods.some((method) => method.template === "azureAdDelegated")).toBe( | ||
| 211 | true, | ||
| 212 | ); | ||
| 177 | }); | 213 | }); |
| 178 | 214 | ||
| 179 | it("gates the API when dev-auth is on but treats the request as the dev admin", async () => { | 215 | it("gates the API when dev-auth is on but treats the request as the dev admin", async () => { |
| @@ -32,6 +32,7 @@ · 31 unmodified lines | |||
| 32 | "@executor-js/plugin-mcp": "workspace:*", | 32 | "@executor-js/plugin-mcp": "workspace:*", |
| 33 | "@executor-js/plugin-microsoft": "workspace:*", | 33 | "@executor-js/plugin-microsoft": "workspace:*", |
| 34 | "@executor-js/plugin-openapi": "workspace:*", | 34 | "@executor-js/plugin-openapi": "workspace:*", |
| 35 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 35 | "@executor-js/plugin-toolkits": "workspace:*", | 36 | "@executor-js/plugin-toolkits": "workspace:*", |
| 36 | "@executor-js/react": "workspace:*", | 37 | "@executor-js/react": "workspace:*", |
| 37 | "@executor-js/runtime-quickjs": "workspace:*", | 38 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -14,7 +14,7 @@ import { · 13 unmodified lines | |||
| 14 | import { graphqlIntrospectionBlobDataMigration } from "@executor-js/plugin-graphql"; | 14 | import { graphqlIntrospectionBlobDataMigration } from "@executor-js/plugin-graphql"; |
| 15 | import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google"; | 15 | import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google"; |
| 16 | 16 | ||
| 17 | import { providerServiceSplitDataMigration } from "../../../../scripts/migration/service-split-sqlite"; | 17 | import { providerServiceSplitDataMigration } from "@executor-js/plugin-provider-service-split"; |
| 18 | import { authConfigTransforms } from "./auth-config-migration"; | 18 | import { authConfigTransforms } from "./auth-config-migration"; |
| 19 | 19 | ||
| 20 | export const selfHostDataMigrations: readonly SqliteDataMigration[] = [ | 20 | export const selfHostDataMigrations: readonly SqliteDataMigration[] = [ |
| @@ -39,6 +39,7 @@ · 38 unmodified lines | |||
| 39 | "@executor-js/plugin-microsoft": "workspace:*", | 39 | "@executor-js/plugin-microsoft": "workspace:*", |
| 40 | "@executor-js/plugin-onepassword": "workspace:*", | 40 | "@executor-js/plugin-onepassword": "workspace:*", |
| 41 | "@executor-js/plugin-openapi": "workspace:*", | 41 | "@executor-js/plugin-openapi": "workspace:*", |
| 42 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 42 | "@executor-js/plugin-toolkits": "workspace:*", | 43 | "@executor-js/plugin-toolkits": "workspace:*", |
| 43 | "@executor-js/react": "workspace:*", | 44 | "@executor-js/react": "workspace:*", |
| 44 | "@executor-js/runtime-quickjs": "workspace:*", | 45 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -19,7 +19,7 @@ import { · 18 unmodified lines | |||
| 19 | import { graphqlIntrospectionBlobDataMigration } from "@executor-js/plugin-graphql"; | 19 | import { graphqlIntrospectionBlobDataMigration } from "@executor-js/plugin-graphql"; |
| 20 | import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google"; | 20 | import { googleOpenApiOwnershipDataMigration } from "@executor-js/plugin-google"; |
| 21 | 21 | ||
| 22 | import { providerServiceSplitDataMigration } from "../../../../scripts/migration/service-split-sqlite"; | 22 | import { providerServiceSplitDataMigration } from "@executor-js/plugin-provider-service-split"; |
| 23 | import { authConfigTransforms } from "./auth-config-migration"; | 23 | import { authConfigTransforms } from "./auth-config-migration"; |
| 24 | import { LOCAL_V1_V2_LEDGER_NAME } from "./v1-v2-migration"; | 24 | import { LOCAL_V1_V2_LEDGER_NAME } from "./v1-v2-migration"; |
| 25 | 25 | ||
| @@ -10,6 +10,7 @@ · 9 unmodified lines | |||
| 10 | "@effect/language-service": "^0.85.1", | 10 | "@effect/language-service": "^0.85.1", |
| 11 | "@effect/tsgo": "^0.5.2", | 11 | "@effect/tsgo": "^0.5.2", |
| 12 | "@effect/vitest": "catalog:", | 12 | "@effect/vitest": "catalog:", |
| 13 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 13 | "@typescript/native-preview": "^7.0.0-dev.20260410.1", | 14 | "@typescript/native-preview": "^7.0.0-dev.20260410.1", |
| 14 | "@vitest/expect": "catalog:", | 15 | "@vitest/expect": "catalog:", |
| 15 | "@vitest/mocker": "catalog:", | 16 | "@vitest/mocker": "catalog:", |
| @@ -181,6 +182,7 @@ · 165 unmodified lines | |||
| 181 | "@executor-js/plugin-mcp": "workspace:*", | 182 | "@executor-js/plugin-mcp": "workspace:*", |
| 182 | "@executor-js/plugin-microsoft": "workspace:*", | 183 | "@executor-js/plugin-microsoft": "workspace:*", |
| 183 | "@executor-js/plugin-openapi": "workspace:*", | 184 | "@executor-js/plugin-openapi": "workspace:*", |
| 185 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 184 | "@executor-js/plugin-toolkits": "workspace:*", | 186 | "@executor-js/plugin-toolkits": "workspace:*", |
| 185 | "@executor-js/react": "workspace:*", | 187 | "@executor-js/react": "workspace:*", |
| 186 | "@executor-js/runtime-quickjs": "workspace:*", | 188 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -230,6 +232,7 @@ · 43 unmodified lines | |||
| 230 | "@executor-js/plugin-mcp": "workspace:*", | 232 | "@executor-js/plugin-mcp": "workspace:*", |
| 231 | "@executor-js/plugin-microsoft": "workspace:*", | 233 | "@executor-js/plugin-microsoft": "workspace:*", |
| 232 | "@executor-js/plugin-openapi": "workspace:*", | 234 | "@executor-js/plugin-openapi": "workspace:*", |
| 235 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 233 | "@executor-js/plugin-toolkits": "workspace:*", | 236 | "@executor-js/plugin-toolkits": "workspace:*", |
| 234 | "@executor-js/react": "workspace:*", | 237 | "@executor-js/react": "workspace:*", |
| 235 | "@executor-js/runtime-quickjs": "workspace:*", | 238 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -283,6 +286,7 @@ · 47 unmodified lines | |||
| 283 | "@executor-js/plugin-microsoft": "workspace:*", | 286 | "@executor-js/plugin-microsoft": "workspace:*", |
| 284 | "@executor-js/plugin-onepassword": "workspace:*", | 287 | "@executor-js/plugin-onepassword": "workspace:*", |
| 285 | "@executor-js/plugin-openapi": "workspace:*", | 288 | "@executor-js/plugin-openapi": "workspace:*", |
| 289 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 286 | "@executor-js/plugin-toolkits": "workspace:*", | 290 | "@executor-js/plugin-toolkits": "workspace:*", |
| 287 | "@executor-js/react": "workspace:*", | 291 | "@executor-js/react": "workspace:*", |
| 288 | "@executor-js/runtime-quickjs": "workspace:*", | 292 | "@executor-js/runtime-quickjs": "workspace:*", |
| @@ -1095,6 +1099,25 @@ · 806 unmodified lines | |||
| 1095 | "react", | 1099 | "react", |
| 1096 | ], | 1100 | ], |
| 1097 | }, | 1101 | }, |
| 1102 | "packages/plugins/provider-service-split": { | ||
| 1103 | "name": "@executor-js/plugin-provider-service-split", | ||
| 1104 | "version": "0.0.0", | ||
| 1105 | "dependencies": { | ||
| 1106 | "@executor-js/plugin-google": "workspace:*", | ||
| 1107 | "@executor-js/plugin-microsoft": "workspace:*", | ||
| 1108 | "@executor-js/plugin-openapi": "workspace:*", | ||
| 1109 | "@executor-js/sdk": "workspace:*", | ||
| 1110 | "effect": "catalog:", | ||
| 1111 | }, | ||
| 1112 | "devDependencies": { | ||
| 1113 | "@effect/vitest": "catalog:", | ||
| 1114 | "@types/node": "catalog:", | ||
| 1115 | "bun-types": "catalog:", | ||
| 1116 | "tsup": "catalog:", | ||
| 1117 | "typescript": "catalog:", | ||
| 1118 | "vitest": "catalog:", | ||
| 1119 | }, | ||
| 1120 | }, | ||
| 1098 | "packages/plugins/toolkits": { | 1121 | "packages/plugins/toolkits": { |
| 1099 | "name": "@executor-js/plugin-toolkits", | 1122 | "name": "@executor-js/plugin-toolkits", |
| 1100 | "version": "1.5.21", | 1123 | "version": "1.5.21", |
| @@ -1797,6 +1820,8 @@ · 696 unmodified lines | |||
| 1797 | 1820 | ||
| 1798 | "@executor-js/plugin-openapi": ["@executor-js/plugin-openapi@workspace:packages/plugins/openapi"], | 1821 | "@executor-js/plugin-openapi": ["@executor-js/plugin-openapi@workspace:packages/plugins/openapi"], |
| 1799 | 1822 | ||
| 1823 | "@executor-js/plugin-provider-service-split": ["@executor-js/plugin-provider-service-split@workspace:packages/plugins/provider-service-split"], | ||
| 1824 | |||
| 1800 | "@executor-js/plugin-toolkits": ["@executor-js/plugin-toolkits@workspace:packages/plugins/toolkits"], | 1825 | "@executor-js/plugin-toolkits": ["@executor-js/plugin-toolkits@workspace:packages/plugins/toolkits"], |
| 1801 | 1826 | ||
| 1802 | "@executor-js/plugin-workos-vault": ["@executor-js/plugin-workos-vault@workspace:packages/plugins/workos-vault"], | 1827 | "@executor-js/plugin-workos-vault": ["@executor-js/plugin-workos-vault@workspace:packages/plugins/workos-vault"], |
| @@ -5,7 +5,7 @@ import { scenario } from "../src/scenario"; · 4 unmodified lines | |||
| 5 | import { Browser, Target } from "../src/services"; | 5 | import { Browser, Target } from "../src/services"; |
| 6 | 6 | ||
| 7 | scenario( | 7 | scenario( |
| 8 | "Google Photos: the focused preset opens a Photos-scoped add flow", | 8 | "Google Photos: separated catalog presets open a Photos service add flow", |
| 9 | {}, | 9 | {}, |
| 10 | Effect.gen(function* () { | 10 | Effect.gen(function* () { |
| 11 | const target = yield* Target; | 11 | const target = yield* Target; |
| @@ -13,30 +13,31 @@ scenario( · 1 unmodified line | |||
| 13 | const identity = yield* target.newIdentity(); | 13 | const identity = yield* target.newIdentity(); |
| 14 | 14 | ||
| 15 | yield* browser.session(identity, async ({ page, step }) => { | 15 | yield* browser.session(identity, async ({ page, step }) => { |
| 16 | await step("Find the Google Photos preset from the integrations picker", async () => { | 16 | await step( |
| 17 | await page.goto("/integrations", { waitUntil: "networkidle" }); | 17 | "Find the separated Google Photos presets from the integrations picker", |
| 18 | await page.getByRole("button", { name: "Connect" }).click(); | 18 | async () => { |
| 19 | const dialog = page.getByRole("dialog", { name: "Connect an integration" }); | 19 | await page.goto("/integrations", { waitUntil: "networkidle" }); |
| 20 | await dialog.waitFor(); | 20 | await page.getByRole("button", { name: "Connect" }).click(); |
| 21 | await dialog.getByPlaceholder(/Search or paste a URL/).fill("google photos"); | 21 | const dialog = page.getByRole("dialog", { name: "Connect an integration" }); |
| 22 | await dialog.getByRole("link", { name: /^Google Photos\b/ }).waitFor(); | 22 | await dialog.waitFor(); |
| 23 | }); | 23 | await dialog.getByPlaceholder(/Search or paste a URL/).fill("google photos"); |
| 24 | await dialog.getByRole("link", { name: /^Google Photos Library\b/ }).waitFor(); | ||
| 25 | await dialog.getByRole("link", { name: /^Google Photos Picker\b/ }).waitFor(); | ||
| 26 | }, | ||
| 27 | ); | ||
| 24 | 28 | ||
| 25 | await step("Open the Google Photos scoped add flow", async () => { | 29 | await step("Open the Google Photos Library add flow", async () => { |
| 26 | const dialog = page.getByRole("dialog", { name: "Connect an integration" }); | 30 | const dialog = page.getByRole("dialog", { name: "Connect an integration" }); |
| 27 | await dialog.getByRole("link", { name: /^Google Photos\b/ }).click(); | 31 | await dialog.getByRole("link", { name: /^Google Photos Library\b/ }).click(); |
| 28 | await page.waitForURL(/\/integrations\/add\/google/); | 32 | await page.waitForURL(/\/integrations\/add\/openapi/); |
| 29 | await page.getByRole("heading", { name: "Add Google integration" }).waitFor(); | 33 | await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor(); |
| 30 | }); | 34 | }); |
| 31 | 35 | ||
| 32 | await step("The Photos preset defaults to the focused namespace and products", async () => { | 36 | await step("The Library link carries the focused OpenAPI preset", async () => { |
| 33 | await page.locator('input[value="Google Photos"]').waitFor(); | 37 | const url = new URL(page.url()); |
| 34 | await page.locator('input[value="google_photos"]').waitFor(); | 38 | expect(url.searchParams.get("preset")).toBe("google-photos-library"); |
| 35 | await page.getByText("Google Photos Library").first().waitFor(); | 39 | expect(url.searchParams.get("url")).toContain("photoslibrary"); |
| 36 | await page.getByText("Google Photos Picker").first().waitFor(); | 40 | await expect.poll(() => page.locator("textarea").inputValue()).toContain("photoslibrary"); |
| 37 | await page.getByText("2 Google APIs").waitFor(); | ||
| 38 | expect(await page.locator('input[value="Google"]').count()).toBe(0); | ||
| 39 | expect(await page.locator('input[value="google"]').count()).toBe(0); | ||
| 40 | }); | 41 | }); |
| 41 | }); | 42 | }); |
| 42 | }), | 43 | }), |
| @@ -86,10 +86,7 @@ return { ok: result.ok, path: item.path, result: result.ok ? result.data : resul · 85 unmodified lines | |||
| 86 | 86 | ||
| 87 | scenario( | 87 | scenario( |
| 88 | "Microsoft · client credentials against the emulator mint a Graph connection and call /users", | 88 | "Microsoft · client credentials against the emulator mint a Graph connection and call /users", |
| 89 | { | 89 | { timeout: 180_000 }, |
| 90 | skip: "phase 3 will replace this deleted microsoft.addGraph scenario with catalog-driven Microsoft e2e coverage", | ||
| 91 | timeout: 180_000, | ||
| 92 | }, | ||
| 93 | Effect.scoped( | 90 | Effect.scoped( |
| 94 | Effect.gen(function* () { | 91 | Effect.gen(function* () { |
| 95 | const target = yield* Target; | 92 | const target = yield* Target; |
| @@ -115,7 +112,15 @@ scenario( · 19 unmodified lines | |||
| 115 | name: "Microsoft Graph Emulator", | 112 | name: "Microsoft Graph Emulator", |
| 116 | baseUrl: emulator.url, | 113 | baseUrl: emulator.url, |
| 117 | family: "microsoft", | 114 | family: "microsoft", |
| 118 | specFormat: "microsoft-graph", | 115 | authenticationTemplate: [ |
| 116 | { | ||
| 117 | slug: MICROSOFT_CLIENT_CREDENTIALS_AUTH_TEMPLATE_SLUG, | ||
| 118 | kind: "oauth2", | ||
| 119 | authorizationUrl: oauth.authorizationUrl, | ||
| 120 | tokenUrl: oauth.tokenUrl, | ||
| 121 | scopes: ["https://graph.microsoft.com/.default"], | ||
| 122 | }, | ||
| 123 | ], | ||
| 119 | }, | 124 | }, |
| 120 | }); | 125 | }); |
| 121 | 126 | ||
| @@ -6,7 +6,6 @@ import { composePluginApi } from "@executor-js/api/server"; · 5 unmodified lines | |||
| 6 | import { | 6 | import { |
| 7 | MICROSOFT_AUTH_TEMPLATE_SLUG, | 7 | MICROSOFT_AUTH_TEMPLATE_SLUG, |
| 8 | MICROSOFT_AUTHORIZATION_URL, | 8 | MICROSOFT_AUTHORIZATION_URL, |
| 9 | MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, | ||
| 10 | MICROSOFT_GRAPH_OPENAPI_URL, | 9 | MICROSOFT_GRAPH_OPENAPI_URL, |
| 11 | MICROSOFT_TOKEN_URL, | 10 | MICROSOFT_TOKEN_URL, |
| 12 | microsoftCatalog, | 11 | microsoftCatalog, |
| @@ -32,17 +31,17 @@ type ToolView = { · 19 unmodified lines | |||
| 32 | }; | 31 | }; |
| 33 | 32 | ||
| 34 | const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; | 33 | const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; |
| 34 | const MICROSOFT_FILES_PRESET_ID = "files"; | ||
| 35 | 35 | ||
| 36 | // Compiling the ~37MB Graph spec inside dev workerd needs more headroom than | 36 | // Compiling the Graph spec inside dev workerd needs more headroom than GitHub's |
| 37 | // GitHub's 2-core runners have: /api/microsoft/graph 500s and the dev stack is | 37 | // 2-core runners have. Local runs and the production Workers streaming path are |
| 38 | // dead for every scenario after it in the shard. Local runs (and the | 38 | // unaffected, so CI keeps this narrow catalog scenario quarantined. |
| 39 | // production Workers streaming path) are unaffected — CI-only quarantine. | ||
| 40 | const CI_GRAPH_SPEC_SKIP = process.env.CI | 39 | const CI_GRAPH_SPEC_SKIP = process.env.CI |
| 41 | ? "compiling the full Microsoft Graph spec exhausts the 2-core CI runner and kills the dev stack for the rest of the shard" | 40 | ? "compiling the full Microsoft Graph spec exhausts the 2-core CI runner and kills the dev stack for the rest of the shard" |
| 42 | : undefined; | 41 | : undefined; |
| 43 | 42 | ||
| 44 | scenario( | 43 | scenario( |
| 45 | "Microsoft Graph: default add stores common Microsoft 365 workloads", | 44 | "Microsoft Graph: catalog add stores the OneDrive files service", |
| 46 | { timeout: 180_000, skip: CI_GRAPH_SPEC_SKIP }, | 45 | { timeout: 180_000, skip: CI_GRAPH_SPEC_SKIP }, |
| 47 | Effect.gen(function* () { | 46 | Effect.gen(function* () { |
| 48 | const target = yield* Target; | 47 | const target = yield* Target; |
| @@ -50,7 +49,7 @@ scenario( · 1 unmodified line | |||
| 50 | const identity = yield* target.newIdentity(); | 49 | const identity = yield* target.newIdentity(); |
| 51 | const client = yield* makeApiClient(api, identity); | 50 | const client = yield* makeApiClient(api, identity); |
| 52 | 51 | ||
| 53 | const integration = unique("msgraph_default"); | 52 | const integration = unique("msgraph_files"); |
| 54 | const connection = ConnectionName.make("main"); | 53 | const connection = ConnectionName.make("main"); |
| 55 | const oauthClient = OAuthClientSlug.make(unique("msgraph_app")); | 54 | const oauthClient = OAuthClientSlug.make(unique("msgraph_app")); |
| 56 | 55 | ||
| @@ -60,10 +59,10 @@ scenario( · 3 unmodified lines | |||
| 60 | payload: { | 59 | payload: { |
| 61 | spec: { | 60 | spec: { |
| 62 | kind: "url", | 61 | kind: "url", |
| 63 | url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_GRAPH_DEFAULT_PRESET_IDS[0]}`, | 62 | url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`, |
| 64 | }, | 63 | }, |
| 65 | slug: integration, | 64 | slug: integration, |
| 66 | name: "Microsoft Graph Defaults", | 65 | name: "Microsoft Graph Files", |
| 67 | family: "microsoft", | 66 | family: "microsoft", |
| 68 | specFormat: "microsoft-graph", | 67 | specFormat: "microsoft-graph", |
| 69 | }, | 68 | }, |
| @@ -71,8 +70,8 @@ scenario( · 1 unmodified line | |||
| 71 | expect(added.slug, "the Microsoft Graph source keeps the requested slug").toBe(integration); | 70 | expect(added.slug, "the Microsoft Graph source keeps the requested slug").toBe(integration); |
| 72 | expect( | 71 | expect( |
| 73 | added.toolCount, | 72 | added.toolCount, |
| 74 | "the default Microsoft Graph add extracts common user-facing operations", | 73 | "the Microsoft files catalog preset extracts a focused operation subtree", |
| 75 | ).toBeGreaterThan(100); | 74 | ).toBeGreaterThan(10); |
| 76 | 75 | ||
| 77 | const config = yield* client.openapi.getConfig({ | 76 | const config = yield* client.openapi.getConfig({ |
| 78 | params: { slug: integration }, | 77 | params: { slug: integration }, |
| @@ -82,10 +81,9 @@ scenario( · 3 unmodified lines | |||
| 82 | ? [...template.scopes] | 81 | ? [...template.scopes] |
| 83 | : [], | 82 | : [], |
| 84 | ); | 83 | ); |
| 85 | expect( | 84 | expect(delegatedScopes, "the files delegated OAuth asks for file scopes").toContain( |
| 86 | delegatedScopes, | 85 | "Files.ReadWrite.All", |
| 87 | "default delegated OAuth asks for common workload scopes", | 86 | ); |
| 88 | ).toContain("User.Read"); | ||
| 89 | 87 | ||
| 90 | yield* client.oauth.createClient({ | 88 | yield* client.oauth.createClient({ |
| 91 | payload: { | 89 | payload: { |
| @@ -116,11 +114,11 @@ scenario( · 24 unmodified lines | |||
| 116 | const authorizeUrl = new URL(authorizationUrl || "https://invalid.example"); | 114 | const authorizeUrl = new URL(authorizationUrl || "https://invalid.example"); |
| 117 | expect( | 115 | expect( |
| 118 | authorizeUrl.toString().length, | 116 | authorizeUrl.toString().length, |
| 119 | "default Microsoft Graph OAuth authorize URLs stay under ordinary proxy limits", | 117 | "Microsoft files OAuth authorize URLs stay under ordinary proxy limits", |
| 120 | ).toBeLessThan(2_000); | 118 | ).toBeLessThan(2_000); |
| 121 | expect( | 119 | expect( |
| 122 | authorizeUrl.searchParams.get("scope"), | 120 | authorizeUrl.searchParams.get("scope"), |
| 123 | "default Microsoft Graph OAuth asks for common workload scopes", | 121 | "Microsoft files OAuth asks for the focused scope set", |
| 124 | ).toBe(delegatedScopes?.join(" ")); | 122 | ).toBe(delegatedScopes?.join(" ")); |
| 125 | 123 | ||
| 126 | yield* client.connections.create({ | 124 | yield* client.connections.create({ |
| @@ -137,15 +135,15 @@ scenario( · 10 unmodified lines | |||
| 137 | query: { integration: IntegrationSlug.make(integration), connection }, | 135 | query: { integration: IntegrationSlug.make(integration), connection }, |
| 138 | }); | 136 | }); |
| 139 | const names = tools.map((tool: ToolView) => tool.name); | 137 | const names = tools.map((tool: ToolView) => tool.name); |
| 140 | const messageTools = names.filter((name) => name.toLowerCase().includes("message")); | 138 | const driveTools = names.filter((name) => name.toLowerCase().includes("drive")); |
| 141 | const siteTools = names.filter((name) => name.toLowerCase().includes("site")); | 139 | const shareTools = names.filter((name) => name.toLowerCase().includes("share")); |
| 142 | expect( | 140 | expect(driveTools, "the retrieved catalog includes Microsoft drive operations").not.toEqual( |
| 143 | messageTools, | ||
| 144 | "the retrieved catalog includes Microsoft message operations", | ||
| 145 | ).not.toEqual([]); | ||
| 146 | expect(siteTools, "the retrieved catalog includes SharePoint site operations").not.toEqual( | ||
| 147 | [], | 141 | [], |
| 148 | ); | 142 | ); |
| 143 | expect( | ||
| 144 | shareTools, | ||
| 145 | "the retrieved catalog includes Microsoft sharing operations", | ||
| 146 | ).not.toEqual([]); | ||
| 149 | }), | 147 | }), |
| 150 | Effect.gen(function* () { | 148 | Effect.gen(function* () { |
| 151 | yield* client.connections | 149 | yield* client.connections |
| @@ -5,8 +5,6 @@ import { Effect } from "effect"; · 4 unmodified lines | |||
| 5 | import { composePluginApi } from "@executor-js/api/server"; | 5 | import { composePluginApi } from "@executor-js/api/server"; |
| 6 | import { | 6 | import { |
| 7 | MICROSOFT_AUTH_TEMPLATE_SLUG, | 7 | MICROSOFT_AUTH_TEMPLATE_SLUG, |
| 8 | MICROSOFT_GRAPH_ALL_PRESET_IDS, | ||
| 9 | MICROSOFT_GRAPH_DELEGATED_DEFAULT_SCOPES, | ||
| 10 | MICROSOFT_GRAPH_OPENAPI_URL, | 8 | MICROSOFT_GRAPH_OPENAPI_URL, |
| 11 | microsoftCatalog, | 9 | microsoftCatalog, |
| 12 | microsoftGraphAdapter, | 10 | microsoftGraphAdapter, |
| @@ -26,18 +24,21 @@ type ToolView = { · 13 unmodified lines | |||
| 26 | }; | 24 | }; |
| 27 | 25 | ||
| 28 | const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; | 26 | const unique = (prefix: string) => `${prefix}_${randomBytes(4).toString("hex")}`; |
| 27 | const MICROSOFT_FILES_PRESET_ID = "files"; | ||
| 28 | const MICROSOFT_FILES_DELEGATED_SCOPES = [ | ||
| 29 | "offline_access", | ||
| 30 | "User.Read", | ||
| 31 | "Files.ReadWrite.All", | ||
| 32 | "Sites.ReadWrite.All", | ||
| 33 | ] as const; | ||
| 29 | 34 | ||
| 30 | // Adding *every* Graph workload pulls the full Microsoft Graph OpenAPI document | 35 | // Adding a catalog service extracts only that service's Microsoft Graph subtree |
| 31 | // (~37MB, ~16.5k operations) and persists a binding per operation. That whole- | 36 | // and persists a binding per operation. This is the regression guard for both |
| 32 | // document path used to 503 on the Cloudflare worker: parsing the spec, and | 37 | // former worker pressure sites: the add streams compile and persist, and |
| 33 | // then re-parsing it on every tools/list, each rebuilt a ~300MB JS tree that | 38 | // tools/list serves from persisted bindings plus the content-addressed defs blob |
| 34 | // blew the 128MB isolate. This scenario is the regression guard for both sites | 39 | // without re-parsing the Graph spec. |
| 35 | // at real scale: the add streams the compile + persist, and tools/list serves | ||
| 36 | // the catalog back from the persisted bindings (+ the content-addressed defs | ||
| 37 | // blob) without ever re-parsing the spec. It drives only the public API, so a | ||
| 38 | // green run is evidence the full catalog lands and serves end to end. | ||
| 39 | scenario( | 40 | scenario( |
| 40 | "Microsoft Graph: the full catalog adds and serves without re-parsing the spec", | 41 | "Microsoft Graph: the files catalog service adds and serves without re-parsing the spec", |
| 41 | { timeout: 300_000 }, | 42 | { timeout: 300_000 }, |
| 42 | Effect.gen(function* () { | 43 | Effect.gen(function* () { |
| 43 | const target = yield* Target; | 44 | const target = yield* Target; |
| @@ -45,30 +46,30 @@ scenario( · 1 unmodified line | |||
| 45 | const identity = yield* target.newIdentity(); | 46 | const identity = yield* target.newIdentity(); |
| 46 | const client = yield* makeApiClient(api, identity); | 47 | const client = yield* makeApiClient(api, identity); |
| 47 | 48 | ||
| 48 | const integration = unique("msgraph_full"); | 49 | const integration = unique("msgraph_files"); |
| 49 | const connection = ConnectionName.make("main"); | 50 | const connection = ConnectionName.make("main"); |
| 50 | 51 | ||
| 51 | yield* Effect.ensuring( | 52 | yield* Effect.ensuring( |
| 52 | Effect.gen(function* () { | 53 | Effect.gen(function* () { |
| 53 | // Add path (1st former OOM site): the full spec is fetched and | 54 | // Add path, first former OOM site: the Graph spec is fetched and |
| 54 | // stream-compiled into one persisted binding per operation. | 55 | // stream-compiled into one persisted binding per selected operation. |
| 55 | const added = yield* client.openapi.addSpec({ | 56 | const added = yield* client.openapi.addSpec({ |
| 56 | payload: { | 57 | payload: { |
| 57 | spec: { | 58 | spec: { |
| 58 | kind: "url", | 59 | kind: "url", |
| 59 | url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_GRAPH_ALL_PRESET_IDS[0]}`, | 60 | url: `${MICROSOFT_GRAPH_OPENAPI_URL}#preset=${MICROSOFT_FILES_PRESET_ID}`, |
| 60 | }, | 61 | }, |
| 61 | slug: integration, | 62 | slug: integration, |
| 62 | name: "Microsoft Graph (full)", | 63 | name: "Microsoft Graph Files", |
| 63 | family: "microsoft", | 64 | family: "microsoft", |
| 64 | specFormat: "microsoft-graph", | 65 | specFormat: "microsoft-graph", |
| 65 | }, | 66 | }, |
| 66 | }); | 67 | }); |
| 67 | expect(added.slug, "the full Graph source keeps the requested slug").toBe(integration); | 68 | expect(added.slug, "the Microsoft files source keeps the requested slug").toBe(integration); |
| 68 | expect( | 69 | expect( |
| 69 | added.toolCount, | 70 | added.toolCount, |
| 70 | "adding every Graph workload extracts the whole catalog (thousands of operations)", | 71 | "adding the files catalog service extracts a focused Graph operation subtree", |
| 71 | ).toBeGreaterThan(5_000); | 72 | ).toBeGreaterThan(10); |
| 72 | 73 | ||
| 73 | const config = yield* client.openapi.getConfig({ params: { slug: integration } }); | 74 | const config = yield* client.openapi.getConfig({ params: { slug: integration } }); |
| 74 | const delegatedScopes = config?.authenticationTemplate?.flatMap((template) => | 75 | const delegatedScopes = config?.authenticationTemplate?.flatMap((template) => |
| @@ -78,8 +79,8 @@ scenario( · 3 unmodified lines | |||
| 78 | ); | 79 | ); |
| 79 | expect( | 80 | expect( |
| 80 | delegatedScopes, | 81 | delegatedScopes, |
| 81 | "full Graph delegates the app-registration default scope set", | 82 | "the files service delegates only the file-service scope set", |
| 82 | ).toEqual([...MICROSOFT_GRAPH_DELEGATED_DEFAULT_SCOPES]); | 83 | ).toEqual([...MICROSOFT_FILES_DELEGATED_SCOPES]); |
| 83 | 84 | ||
| 84 | yield* client.connections.create({ | 85 | yield* client.connections.create({ |
| 85 | payload: { | 86 | payload: { |
| @@ -91,24 +92,22 @@ scenario( · 5 unmodified lines | |||
| 91 | }, | 92 | }, |
| 92 | }); | 93 | }); |
| 93 | 94 | ||
| 94 | // Serve path (2nd former OOM site): tools/list rebuilds the catalog from | 95 | // Serve path, second former OOM site: tools/list rebuilds the catalog |
| 95 | // the persisted bindings. The whole catalog must come back, with real | 96 | // from persisted bindings, with real descriptions, and without |
| 96 | // descriptions, and without re-parsing the 37MB spec. | 97 | // re-parsing the Graph spec. |
| 97 | const tools = yield* client.tools.list({ | 98 | const tools = yield* client.tools.list({ |
| 98 | query: { integration: IntegrationSlug.make(integration), connection }, | 99 | query: { integration: IntegrationSlug.make(integration), connection }, |
| 99 | }); | 100 | }); |
| 100 | expect( | 101 | expect( |
| 101 | tools.length, | 102 | tools.length, |
| 102 | "the served catalog returns the whole set of operations, not a re-parse failure", | 103 | "the served catalog returns the files operation subtree, not a re-parse failure", |
| 103 | ).toBeGreaterThan(5_000); | 104 | ).toBeGreaterThan(10); |
| 104 | 105 | ||
| 105 | const names = tools.map((tool: ToolView) => tool.name); | 106 | const names = tools.map((tool: ToolView) => tool.name); |
| 106 | const messageTools = names.filter((name) => name.toLowerCase().includes("message")); | 107 | const driveTools = names.filter((name) => name.toLowerCase().includes("drive")); |
| 107 | const siteTools = names.filter((name) => name.toLowerCase().includes("site")); | 108 | const shareTools = names.filter((name) => name.toLowerCase().includes("share")); |
| 108 | const userTools = names.filter((name) => name.toLowerCase().includes("user")); | 109 | expect(driveTools, "the served catalog spans drive operations").not.toEqual([]); |
| 109 | expect(messageTools, "the served catalog spans mail operations").not.toEqual([]); | 110 | expect(shareTools, "the served catalog spans sharing operations").not.toEqual([]); |
| 110 | expect(siteTools, "the served catalog spans SharePoint site operations").not.toEqual([]); | ||
| 111 | expect(userTools, "the served catalog spans directory user operations").not.toEqual([]); | ||
| 112 | }), | 111 | }), |
| 113 | Effect.gen(function* () { | 112 | Effect.gen(function* () { |
| 114 | yield* client.connections | 113 | yield* client.connections |
| @@ -316,10 +316,7 @@ const requireOAuthClientCredential = (credential: IssuedCredential) => · 315 unmodified lines | |||
| 316 | 316 | ||
| 317 | scenario( | 317 | scenario( |
| 318 | "OAuth client · agent hands off, the human enters the secret in the browser, and the app connects", | 318 | "OAuth client · agent hands off, the human enters the secret in the browser, and the app connects", |
| 319 | { | 319 | { timeout: 240_000 }, |
| 320 | skip: "phase 3 will replace this deleted microsoft.addGraph scenario with catalog-driven Microsoft e2e coverage", | ||
| 321 | timeout: 240_000, | ||
| 322 | }, | ||
| 323 | Effect.gen(function* () { | 320 | Effect.gen(function* () { |
| 324 | const target = yield* Target; | 321 | const target = yield* Target; |
| 325 | const { client: makeApiClient } = yield* Api; | 322 | const { client: makeApiClient } = yield* Api; |
| @@ -361,7 +358,15 @@ scenario( · 35 unmodified lines | |||
| 361 | name: "Microsoft Graph Emulator", | 358 | name: "Microsoft Graph Emulator", |
| 362 | baseUrl: emulator.baseUrl, | 359 | baseUrl: emulator.baseUrl, |
| 363 | family: "microsoft", | 360 | family: "microsoft", |
| 364 | specFormat: "microsoft-graph", | 361 | authenticationTemplate: [ |
| 362 | { | ||
| 363 | slug: template, | ||
| 364 | kind: "oauth2", | ||
| 365 | authorizationUrl: oauth.authorizationUrl, | ||
| 366 | tokenUrl: oauth.tokenUrl, | ||
| 367 | scopes: ["https://graph.microsoft.com/.default"], | ||
| 368 | }, | ||
| 369 | ], | ||
| 365 | }, | 370 | }, |
| 366 | }); | 371 | }); |
| 367 | 372 | ||
| @@ -420,7 +425,7 @@ scenario( · 52 unmodified lines | |||
| 420 | }); | 425 | }); |
| 421 | 426 | ||
| 422 | await step("Register the app", async () => { | 427 | await step("Register the app", async () => { |
| 423 | await page.getByRole("button", { name: "Register app" }).click(); | 428 | await page.getByRole("button", { name: "Register app", exact: true }).click(); |
| 424 | // onCreated returns to the Add-connection view — the register form closes. | 429 | // onCreated returns to the Add-connection view — the register form closes. |
| 425 | await page | 430 | await page |
| 426 | .getByRole("heading", { name: "Register OAuth app" }) | 431 | .getByRole("heading", { name: "Register OAuth app" }) |
| @@ -5,7 +5,7 @@ import { scenario } from "../src/scenario"; · 4 unmodified lines | |||
| 5 | import { Browser, Target } from "../src/services"; | 5 | import { Browser, Target } from "../src/services"; |
| 6 | 6 | ||
| 7 | scenario( | 7 | scenario( |
| 8 | "Provider plugins · Google and Microsoft own their add flows outside OpenAPI", | 8 | "Provider catalog · Google and Microsoft services are OpenAPI presets", |
| 9 | {}, | 9 | {}, |
| 10 | Effect.gen(function* () { | 10 | Effect.gen(function* () { |
| 11 | const target = yield* Target; | 11 | const target = yield* Target; |
| @@ -19,56 +19,46 @@ scenario( · 7 unmodified lines | |||
| 19 | await page.getByRole("dialog", { name: "Connect an integration" }).waitFor(); | 19 | await page.getByRole("dialog", { name: "Connect an integration" }).waitFor(); |
| 20 | }); | 20 | }); |
| 21 | 21 | ||
| 22 | await step( | 22 | await step("The picker exposes OpenAPI plus provider service presets", async () => { |
| 23 | "The provider list exposes OpenAPI, Google, and Microsoft separately", | 23 | const dialog = page.getByRole("dialog", { |
| 24 | async () => { | 24 | name: "Connect an integration", |
| 25 | const dialog = page.getByRole("dialog", { | 25 | }); |
| 26 | name: "Connect an integration", | 26 | const search = dialog.getByPlaceholder(/Search or paste a URL/); |
| 27 | }); | 27 | await dialog.getByRole("link", { name: "OpenAPI", exact: true }).waitFor(); |
| 28 | await dialog.getByRole("link", { name: "OpenAPI", exact: true }).waitFor(); | 28 | |
| 29 | await dialog.getByRole("link", { name: "Google", exact: true }).waitFor(); | 29 | await search.fill("gmail"); |
| 30 | await dialog.getByRole("link", { name: "Microsoft", exact: true }).waitFor(); | 30 | await dialog.getByRole("link", { name: /^Gmail\b/ }).waitFor(); |
| 31 | }, | 31 | |
| 32 | ); | 32 | await search.fill("onedrive"); |
| 33 | await dialog.getByRole("link", { name: /^OneDrive Files\b/ }).waitFor(); | ||
| 34 | }); | ||
| 33 | 35 | ||
| 34 | await step("OpenAPI add remains generic", async () => { | 36 | await step("OpenAPI add remains generic", async () => { |
| 35 | await page.goto("/integrations/add/openapi", { | 37 | await page.goto("/integrations/add/openapi", { |
| 36 | waitUntil: "domcontentloaded", | 38 | waitUntil: "domcontentloaded", |
| 37 | }); | 39 | }); |
| 38 | await page.getByRole("heading", { name: "Add OpenAPI Integration" }).waitFor(); | 40 | await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor(); |
| 39 | await page.getByText("OpenAPI Spec").waitFor(); | 41 | await page.getByText("OpenAPI Spec").waitFor(); |
| 40 | expect(await page.getByText("Customize your Google connection").count()).toBe(0); | 42 | expect(await page.getByText("Customize your Google connection").count()).toBe(0); |
| 41 | expect(await page.getByText("Customize Microsoft Graph").count()).toBe(0); | 43 | expect(await page.getByText("Customize Microsoft Graph").count()).toBe(0); |
| 42 | }); | 44 | }); |
| 43 | 45 | ||
| 44 | await step("Google has its own product picker", async () => { | 46 | await step("A Google service preset opens the OpenAPI add flow", async () => { |
| 45 | await page.goto("/integrations/add/google", { | 47 | await page.goto( |
| 46 | waitUntil: "domcontentloaded", | 48 | "/integrations/add/openapi?preset=google-gmail&url=https%3A%2F%2Fwww.googleapis.com%2Fdiscovery%2Fv1%2Fapis%2Fgmail%2Fv1%2Frest", |
| 47 | }); | 49 | { waitUntil: "domcontentloaded" }, |
| 48 | await page.getByRole("heading", { name: "Add Google integration" }).waitFor(); | 50 | ); |
| 49 | await page.getByText("Customize your Google connection").waitFor(); | 51 | await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor(); |
| 50 | await page.getByText("Gmail").first().waitFor(); | 52 | await expect.poll(() => page.locator("textarea").inputValue()).toContain("gmail"); |
| 51 | await page.getByText("Google Calendar").first().waitFor(); | ||
| 52 | }); | 53 | }); |
| 53 | 54 | ||
| 54 | await step("Microsoft has its own Graph scope picker", async () => { | 55 | await step("A Microsoft service preset opens the OpenAPI add flow", async () => { |
| 55 | await page.goto("/integrations/add/microsoft", { | 56 | await page.goto( |
| 56 | waitUntil: "domcontentloaded", | 57 | "/integrations/add/openapi?preset=microsoft-files&url=https%3A%2F%2Fraw.githubusercontent.com%2Fmicrosoftgraph%2Fmsgraph-metadata%2Fmaster%2Fopenapi%2Fv1.0%2Fopenapi.yaml%23preset%3Dfiles", |
| 57 | }); | 58 | { waitUntil: "domcontentloaded" }, |
| 58 | await page.getByRole("heading", { name: "Add Microsoft integration" }).waitFor(); | 59 | ); |
| 59 | await page.getByText("Customize Microsoft Graph").waitFor(); | 60 | await page.getByRole("heading", { name: "Add OpenAPI integration" }).waitFor(); |
| 60 | expect(await page.getByText("All Microsoft Graph", { exact: true }).count()).toBe(0); | 61 | await expect.poll(() => page.locator("textarea").inputValue()).toContain("preset=files"); |
| 61 | await page.getByText("Productivity").waitFor(); | ||
| 62 | await page.getByText("Directory and identity").waitFor(); | ||
| 63 | await page.getByText("Outlook Mail").waitFor(); | ||
| 64 | await page.getByText("OneDrive Files").waitFor(); | ||
| 65 | await page.getByText("OneNote", { exact: true }).waitFor(); | ||
| 66 | await page.getByText("Teams Channels").waitFor(); | ||
| 67 | await page.locator('img[src*="svgl.app/library/microsoft-outlook.svg"]').first().waitFor(); | ||
| 68 | await page.locator('img[src*="svgl.app/library/microsoft-onedrive.svg"]').waitFor(); | ||
| 69 | await page.getByRole("button", { name: /View scopes/ }).click(); | ||
| 70 | await page.getByText("offline_access").waitFor(); | ||
| 71 | await page.getByText("Mail.ReadWrite").waitFor(); | ||
| 72 | }); | 62 | }); |
| 73 | }); | 63 | }); |
| 74 | }), | 64 | }), |
| @@ -75,6 +75,7 @@ · 74 unmodified lines | |||
| 75 | "@effect/language-service": "^0.85.1", | 75 | "@effect/language-service": "^0.85.1", |
| 76 | "@effect/tsgo": "^0.5.2", | 76 | "@effect/tsgo": "^0.5.2", |
| 77 | "@effect/vitest": "catalog:", | 77 | "@effect/vitest": "catalog:", |
| 78 | "@executor-js/plugin-provider-service-split": "workspace:*", | ||
| 78 | "@typescript/native-preview": "^7.0.0-dev.20260410.1", | 79 | "@typescript/native-preview": "^7.0.0-dev.20260410.1", |
| 79 | "@vitest/expect": "catalog:", | 80 | "@vitest/expect": "catalog:", |
| 80 | "@vitest/mocker": "catalog:", | 81 | "@vitest/mocker": "catalog:", |
| @@ -0,0 +1,7 @@ | |||
| 1 | # @executor-js/plugin-provider-service-split | ||
| 2 | |||
| 3 | ## 0.0.0 | ||
| 4 | |||
| 5 | ### Patch Changes | ||
| 6 | |||
| 7 | - Initial internal package. | ||
| @@ -0,0 +1,33 @@ | |||
| 1 | { | ||
| 2 | "name": "@executor-js/plugin-provider-service-split", | ||
| 3 | "version": "0.0.0", | ||
| 4 | "private": true, | ||
| 5 | "type": "module", | ||
| 6 | "exports": { | ||
| 7 | ".": "./src/index.ts", | ||
| 8 | "./planner": "./src/planner.ts", | ||
| 9 | "./sqlite": "./src/sqlite.ts" | ||
| 10 | }, | ||
| 11 | "scripts": { | ||
| 12 | "build": "tsup && (tsc --declaration --emitDeclarationOnly --outDir dist --rootDir src || true)", | ||
| 13 | "typecheck": "tsgo --noEmit", | ||
| 14 | "test": "vitest run", | ||
| 15 | "test:watch": "vitest", | ||
| 16 | "typecheck:slow": "bunx tsc --noEmit -p tsconfig.json" | ||
| 17 | }, | ||
| 18 | "dependencies": { | ||
| 19 | "@executor-js/plugin-google": "workspace:*", | ||
| 20 | "@executor-js/plugin-microsoft": "workspace:*", | ||
| 21 | "@executor-js/plugin-openapi": "workspace:*", | ||
| 22 | "@executor-js/sdk": "workspace:*", | ||
| 23 | "effect": "catalog:" | ||
| 24 | }, | ||
| 25 | "devDependencies": { | ||
| 26 | "@effect/vitest": "catalog:", | ||
| 27 | "@types/node": "catalog:", | ||
| 28 | "bun-types": "catalog:", | ||
| 29 | "tsup": "catalog:", | ||
| 30 | "typescript": "catalog:", | ||
| 31 | "vitest": "catalog:" | ||
| 32 | } | ||
| 33 | } | ||
| @@ -0,0 +1,2 @@ | |||
| 1 | export * from "./planner"; | ||
| 2 | export * from "./sqlite"; | ||
| @@ -1,9 +1,6 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Effect } from "effect"; | 2 | import { Effect } from "effect"; |
| 3 | import { | 3 | import { streamOperationBindingsFromStructure, structuralSplit } from "@executor-js/plugin-openapi"; |
| 4 | streamOperationBindingsFromStructure, | ||
| 5 | structuralSplit, | ||
| 6 | } from "../../packages/plugins/openapi/src/sdk"; | ||
| 7 | 4 | ||
| 8 | import { | 5 | import { |
| 9 | googlePresetIdForTool, | 6 | googlePresetIdForTool, |
| @@ -19,8 +16,8 @@ import { · 9 unmodified lines | |||
| 19 | type PluginStorageRow, | 16 | type PluginStorageRow, |
| 20 | type ToolPolicyRow, | 17 | type ToolPolicyRow, |
| 21 | type ToolRow, | 18 | type ToolRow, |
| 22 | } from "../../scripts/migration/service-split-planner"; | 19 | } from "./planner"; |
| 23 | import { googleCatalog } from "../../packages/plugins/google/src/sdk/presets"; | 20 | import { googleCatalog } from "@executor-js/plugin-google"; |
| 24 | 21 | ||
| 25 | const now = "2026-01-01T00:00:00.000Z"; | 22 | const now = "2026-01-01T00:00:00.000Z"; |
| 26 | 23 | ||
| @@ -1,14 +1,12 @@ | |||
| 1 | /* oxlint-disable executor/no-try-catch-or-throw, executor/no-error-constructor, executor/no-instanceof-error, executor/no-unknown-error-message, executor/no-json-parse -- boundary: one-shot provider service split planner preserves the throwing dry-run contract used by migration tooling */ | ||
| 1 | import { createHash } from "node:crypto"; | 2 | import { createHash } from "node:crypto"; |
| 2 | 3 | ||
| 3 | import { | 4 | import { googleCatalog, googlePresetForDiscoveryUrl } from "@executor-js/plugin-google"; |
| 4 | googleCatalog, | ||
| 5 | googlePresetForDiscoveryUrl, | ||
| 6 | } from "../../packages/plugins/google/src/sdk/presets"; | ||
| 7 | import { | 5 | import { |
| 8 | MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, | 6 | MICROSOFT_GRAPH_DEFAULT_PRESET_IDS, |
| 9 | microsoftCatalog, | 7 | microsoftCatalog, |
| 10 | } from "../../packages/plugins/microsoft/src/sdk/presets"; | 8 | } from "@executor-js/plugin-microsoft"; |
| 11 | import type { OpenApiPreset } from "../../packages/plugins/openapi/src/sdk/presets"; | 9 | import type { OpenApiPreset } from "@executor-js/plugin-openapi/presets"; |
| 12 | 10 | ||
| 13 | type MonolithPluginId = "google" | "microsoft"; | 11 | type MonolithPluginId = "google" | "microsoft"; |
| 14 | 12 | ||
| @@ -1,18 +1,18 @@ | |||
| 1 | import { describe, expect, it } from "@effect/vitest"; | 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { Effect, Schema } from "effect"; | 2 | import { Effect, Schema } from "effect"; |
| 3 | import { collectTables } from "../../packages/core/sdk/src/executor"; | ||
| 4 | import { | 3 | import { |
| 4 | collectTables, | ||
| 5 | DataMigrationError, | 5 | DataMigrationError, |
| 6 | runSqliteDataMigrations, | 6 | runSqliteDataMigrations, |
| 7 | type SqliteDataMigrationClient, | 7 | type SqliteDataMigrationClient, |
| 8 | } from "../../packages/core/sdk/src/sqlite-data-migrations"; | 8 | } from "@executor-js/sdk"; |
| 9 | import { createSqliteTestFumaDb } from "../../packages/core/sdk/src/sqlite-test-db"; | 9 | import { createSqliteTestFumaDb } from "@executor-js/sdk/testing"; |
| 10 | 10 | ||
| 11 | import { operationStorageKey } from "../../scripts/migration/service-split-planner"; | 11 | import { operationStorageKey } from "./planner"; |
| 12 | import { | 12 | import { |
| 13 | providerServiceSplitDataMigration, | 13 | providerServiceSplitDataMigration, |
| 14 | runSqliteProviderServiceSplitMigration, | 14 | runSqliteProviderServiceSplitMigration, |
| 15 | } from "../../scripts/migration/service-split-sqlite"; | 15 | } from "./sqlite"; |
| 16 | 16 | ||
| 17 | const now = 1_780_000_000_000; | 17 | const now = 1_780_000_000_000; |
| 18 | const parseJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); | 18 | const parseJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); |
| @@ -1,8 +1,6 @@ | |||
| 1 | /* oxlint-disable executor/no-try-catch-or-throw, executor/no-error-constructor, executor/no-json-parse -- boundary: one-shot provider service split data migration preserves legacy SQLite row coercion and typed DataMigrationError wrapping */ | ||
| 1 | import { Effect } from "effect"; | 2 | import { Effect } from "effect"; |
| 2 | import { | 3 | import { DataMigrationError, type SqliteDataMigrationClient } from "@executor-js/sdk"; |
| 3 | DataMigrationError, | ||
| 4 | type SqliteDataMigrationClient, | ||
| 5 | } from "../../packages/core/sdk/src/sqlite-data-migrations"; | ||
| 6 | 4 | ||
| 7 | import { | 5 | import { |
| 8 | operationStorageKey, | 6 | operationStorageKey, |
| @@ -17,7 +15,7 @@ import { · 8 unmodified lines | |||
| 17 | type PluginStorageRow, | 15 | type PluginStorageRow, |
| 18 | type ToolPolicyRow, | 16 | type ToolPolicyRow, |
| 19 | type ToolRow, | 17 | type ToolRow, |
| 20 | } from "./service-split-planner"; | 18 | } from "./planner"; |
| 21 | 19 | ||
| 22 | const MIGRATION_NAME = "2026-07-08-provider-service-split"; | 20 | const MIGRATION_NAME = "2026-07-08-provider-service-split"; |
| 23 | const LEDGER_TABLE = "provider_service_split_org_migration"; | 21 | const LEDGER_TABLE = "provider_service_split_org_migration"; |
| @@ -0,0 +1,23 @@ | |||
| 1 | { | ||
| 2 | "compilerOptions": { | ||
| 3 | "target": "ES2022", | ||
| 4 | "module": "ESNext", | ||
| 5 | "moduleResolution": "Bundler", | ||
| 6 | "strict": true, | ||
| 7 | "skipLibCheck": true, | ||
| 8 | "lib": ["ES2022", "DOM"], | ||
| 9 | "types": ["bun-types", "node"], | ||
| 10 | "noUnusedLocals": true, | ||
| 11 | "noImplicitOverride": true, | ||
| 12 | "jsx": "react-jsx", | ||
| 13 | "plugins": [ | ||
| 14 | { | ||
| 15 | "name": "@effect/language-service", | ||
| 16 | "ignoreEffectSuggestionsInTscExitCode": true, | ||
| 17 | "ignoreEffectWarningsInTscExitCode": true, | ||
| 18 | "diagnosticSeverity": {} | ||
| 19 | } | ||
| 20 | ] | ||
| 21 | }, | ||
| 22 | "include": ["src/**/*.ts"] | ||
| 23 | } | ||
| @@ -0,0 +1,12 @@ | |||
| 1 | import { defineConfig } from "tsup"; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | entry: { | ||
| 5 | index: "src/index.ts", | ||
| 6 | }, | ||
| 7 | format: ["esm"], | ||
| 8 | dts: false, | ||
| 9 | sourcemap: true, | ||
| 10 | clean: true, | ||
| 11 | external: [/^@executor-js\//, /^effect/, /^@effect\//], | ||
| 12 | }); | ||
| @@ -0,0 +1,8 @@ | |||
| 1 | import { defineConfig } from "vitest/config"; | ||
| 2 | |||
| 3 | export default defineConfig({ | ||
| 4 | test: { | ||
| 5 | include: ["src/**/*.test.ts"], | ||
| 6 | testTimeout: 15_000, | ||
| 7 | }, | ||
| 8 | }); | ||
| @@ -22,7 +22,7 @@ import { · 21 unmodified lines | |||
| 22 | type PluginStorageRow, | 22 | type PluginStorageRow, |
| 23 | type ToolPolicyRow, | 23 | type ToolPolicyRow, |
| 24 | type ToolRow, | 24 | type ToolRow, |
| 25 | } from "./service-split-planner"; | 25 | } from "@executor-js/plugin-provider-service-split/planner"; |
| 26 | 26 | ||
| 27 | export interface SqlClient { | 27 | export interface SqlClient { |
| 28 | readonly unsafe: <T extends readonly object[] = readonly Record<string, unknown>[]>( | 28 | readonly unsafe: <T extends readonly object[] = readonly Record<string, unknown>[]>( |
| @@ -10,7 +10,10 @@ import { · 9 unmodified lines | |||
| 10 | readDatabaseInput, | 10 | readDatabaseInput, |
| 11 | type SqlClient, | 11 | type SqlClient, |
| 12 | } from "../../scripts/migration/service-split-postgres-cli"; | 12 | } from "../../scripts/migration/service-split-postgres-cli"; |
| 13 | import { operationStorageKey, planMigration } from "../../scripts/migration/service-split-planner"; | 13 | import { |
| 14 | operationStorageKey, | ||
| 15 | planMigration, | ||
| 16 | } from "@executor-js/plugin-provider-service-split/planner"; | ||
| 14 | 17 | ||
| 15 | const now = "2026-01-01T00:00:00.000Z"; | 18 | const now = "2026-01-01T00:00:00.000Z"; |
| 16 | const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); | 19 | const decodeJson = Schema.decodeUnknownSync(Schema.fromJsonString(Schema.Unknown)); |