// ---------------------------------------------------------------------------// OpenAPI plugin — v2 behaviour.//// Ported from the v1 suite to the v2 data model. The v1-only coverage (scope// shadowing, secret-backed credential slots, sources.configure binding// lifecycle, OAuth2 source-config slots, usagesForSecret, configFile mirroring)// is removed — those surfaces no longer exist in v2. See the inline// `// removed:` notes. The behaviours that survive (preview, static control// tools, addSpec → per-connection tools, invoke + transport envelope, auth// template rendering, removeSpec) are exercised against the v2 surface:// addSpec registers an integration, a connection produces the tools, and the// full `tools.<integration>.<owner>.<connection>.<tool>` address is executed.// --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest";import { Effect, Option, Predicate, Schema } from "effect";import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";import { FetchHttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { createExecutor, AuthTemplateSlug, ConnectionName, IntegrationAlreadyExistsError, IntegrationSlug, ToolAddress, makeInMemoryBlobStore,} from "@executor-js/sdk";import { mkdtempSync } from "node:fs";import { tmpdir } from "node:os";import { join } from "node:path"; import { makeTestConfig, makeTestWorkspaceHarness, memoryCredentialsPlugin, serveTestHttpApp, typeCheckOutputTypeScript,} from "@executor-js/sdk/testing"; import { openApiPlugin } from "./plugin";import { type AuthenticationInput } from "./types";import { addOpenApiTestConnection, makeOpenApiHttpApiTestIntegrationConfig, makeOpenApiTestSpecJson, serveMutableOpenApiSpecTestServer, serveOpenApiEchoTestServer, serveOpenApiHttpApiTestServer, unwrapInvocation,} from "../testing"; const TOOL_ERROR_TYPESCRIPT = "{ code: string; message: string; status?: number; details?: unknown; retryable?: boolean }"; const testPlugins = (httpClientLayer = FetchHttpClient.layer) => [openApiPlugin({ httpClientLayer }), memoryCredentialsPlugin()] as const; const recordingBlobStore = () => { const base = makeInMemoryBlobStore(); let writes = 0; return { store: { ...base, put: (namespace: string, key: string, value: string) => Effect.sync(() => { writes += 1; }).pipe(Effect.andThen(base.put(namespace, key, value))), }, writeCount: () => writes, };}; // ---------------------------------------------------------------------------// Define a test API with Effect HttpApi// --------------------------------------------------------------------------- const Item = Schema.Struct({ id: Schema.Number, name: Schema.String,}); const EchoHeaders = Schema.Struct({ authorization: Schema.optional(Schema.String), "x-api-key": Schema.optional(Schema.String),}); class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, },) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.post("createItem", "/items", { payload: Schema.Struct({ name: Schema.String }), success: Item, }), ) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add( HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders, }), ) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const testApiSpecText = () => { const spec = makeOpenApiHttpApiTestIntegrationConfig(TestApi, {}).spec; if (spec.kind === "blob") return spec.value; return spec.url;}; const MICROSOFT_GRAPH_V1_OPERATION_COUNT = 16_548; const microsoftGraphScaleSpecText = () => { const paths: Record<string, unknown> = {}; for (let index = 0; index < MICROSOFT_GRAPH_V1_OPERATION_COUNT; index += 1) { paths[`/users/{userId}/messages/${index}`] = { get: { operationId: `users_messages_list_${index}`, tags: [`Graph category ${index % 37}`], summary: `List synthetic Graph messages ${index}`, parameters: [ { name: "userId", in: "path", required: true, schema: { type: "string" }, }, { name: "$top", in: "query", schema: { type: "integer", format: "int32" }, }, { name: "$select", in: "query", style: "form", explode: false, schema: { type: "array", items: { type: "string" } }, }, ], responses: { "200": { description: "OK", content: { "application/json": { schema: { $ref: "#/components/schemas/MessageCollectionResponse" }, }, }, }, }, }, }; } return JSON.stringify({ openapi: "3.0.0", info: { title: "Microsoft Graph Scale Fixture", version: "v1.0", description: "Synthetic Graph-scale fixture for generic OpenAPI imports.", }, servers: [{ url: "https://graph.microsoft.com/v1.0" }], security: [{ MicrosoftGraph: ["User.Read", "Mail.Read"] }], components: { securitySchemes: { MicrosoftGraph: { type: "oauth2", flows: { authorizationCode: { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", scopes: { "User.Read": "Read user profile", "Mail.Read": "Read user mail", }, }, clientCredentials: { tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", scopes: { ".default": "Application permissions", }, }, }, }, }, schemas: { Message: { type: "object", properties: { id: { type: "string" }, subject: { type: "string" }, receivedDateTime: { type: "string", format: "date-time" }, }, }, MessageCollectionResponse: { type: "object", properties: { value: { type: "array", items: { $ref: "#/components/schemas/Message" }, }, }, }, }, }, paths, });}; // ---------------------------------------------------------------------------// Implement handlers// --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" },]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => Item.make(item)))) .handle("createItem", (req) => Effect.succeed(Item.make({ id: ITEMS.length + 1, name: req.payload.name })), ) .handle("getItem", (req) => Effect.succeed( Item.make( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return EchoHeaders.make({ authorization: req.headers["authorization"], "x-api-key": req.headers["x-api-key"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ),); const servePluginTestApi = () => serveOpenApiHttpApiTestServer({ api: TestApi, handlersLayer: ItemsGroupLive, }); const recordValue = (value: unknown): Record<string, unknown> | undefined => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined; const withEchoCookieParameter = (spec: Record<string, unknown>): Record<string, unknown> => { const paths = recordValue(spec.paths); const pathItem = recordValue(paths?.["/echo-headers"]); const get = recordValue(pathItem?.get); if (!paths || !pathItem || !get) return spec; const parameters = Array.isArray(get.parameters) ? get.parameters : []; return { ...spec, paths: { ...paths, "/echo-headers": { ...pathItem, get: { ...get, parameters: [ ...parameters, { name: "sessionId", in: "cookie", required: true, schema: { type: "string" }, }, ], }, }, }, };}; // An apiKey auth template that places the connection value into `x-api-key`.const apiKeyTemplate: AuthenticationInput = { slug: AuthTemplateSlug.make("apiKey"), type: "apiKey", headers: { "x-api-key": [{ type: "variable" as const, name: "token" }] },}; // An oauth template — the connection value renders as a bearer token.const oauthTemplate: AuthenticationInput = { slug: AuthTemplateSlug.make("oauth"), kind: "oauth2", authorizationUrl: "https://auth.example.test/authorize", tokenUrl: "https://auth.example.test/token", scopes: ["read"],}; const serveOAuthDiscoverableOpenApiSpec = () => Effect.gen(function* () { let baseUrl = ""; const server = yield* serveTestHttpApp((request) => { if (request.url.includes("/api/schema")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ openapi: "3.0.0", info: { title: "PostHog-like API", version: "1.0.0" }, paths: { "/api/projects/": { get: { operationId: "projects_list", security: [{ PersonalAPIKeyAuth: ["project:read", "wizard_session:write"] }], responses: { "200": { description: "OK" } }, }, }, }, components: { securitySchemes: { PersonalAPIKeyAuth: { type: "http", scheme: "bearer" }, }, }, }), ); } if (request.url.includes("/.well-known/oauth-protected-resource")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["project:read"], }), ); } if (request.url.includes("/.well-known/oauth-authorization-server")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize/`, token_endpoint: `${baseUrl}/oauth/token/`, registration_endpoint: `${baseUrl}/oauth/register/`, client_id_metadata_document_supported: true, response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"], scopes_supported: ["project:read"], }), ); } return Effect.succeed(HttpServerResponse.text("not found", { status: 404 })); }); baseUrl = server.baseUrl; return server; }); // ---------------------------------------------------------------------------// Tests// --------------------------------------------------------------------------- describe("OpenAPI Plugin", () => { it.effect("previewSpec returns metadata and header presets", () => Effect.scoped( Effect.gen(function* () { const server = yield* servePluginTestApi(); const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const preview = yield* executor.openapi.previewSpec(server.specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ), ); it.effect("previewSpec discovers OAuth metadata from a URL-hosted bearer spec", () => Effect.scoped( Effect.gen(function* () { const server = yield* serveOAuthDiscoverableOpenApiSpec(); const executor = yield* createExecutor( makeTestConfig({ plugins: testPlugins(server.httpClientLayer) }), ); const preview = yield* executor.openapi.previewSpec(server.url("/api/schema/")); expect(preview.headerPresets.map((preset) => preset.label)).toContain("Bearer Token"); expect(preview.oauth2Presets).toHaveLength(1); const oauth = preview.oauth2Presets[0]!; expect(oauth.securitySchemeName).toBe("DiscoveredOAuth2"); expect(oauth.flow).toBe("authorizationCode"); expect(oauth.tokenUrl).toBe(`${server.baseUrl}/oauth/token/`); expect(Option.getOrNull(oauth.resource)).toBe(server.baseUrl); expect(oauth.supportsClientIdMetadataDocument).toBe(true); expect(oauth.scopes).toEqual({ "project:read": "" }); yield* executor.openapi.addSpec({ spec: { kind: "url", url: server.url("/api/schema/") }, slug: "posthog_like", baseUrl: server.baseUrl, }); const config = yield* executor.openapi.getConfig("posthog_like"); expect( (config?.authenticationTemplate ?? []).map((template) => ({ slug: String(template.slug), kind: template.kind, ...(template.kind === "oauth2" ? { resource: template.resource ?? null, supportsClientIdMetadataDocument: template.supportsClientIdMetadataDocument === true, } : {}), })), ).toEqual([ { slug: "apikey-0", kind: "apikey" }, { slug: "oauth-DiscoveredOAuth2", kind: "oauth2", resource: server.baseUrl, supportsClientIdMetadataDocument: true, }, ]); }), ), ); it.effect("exposes static openapi executor control tools via execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); // v2: static control tools are NOT part of `tools.list()` (that's the // persisted per-connection catalog) and aren't `tools.schema()`-resolvable; // they're dispatched by `execute`. Their presence is observable by a // successful invocation rather than a catalog listing. // removed: tools.list() / getSource / configureSource assertions — those // listed v1 static source rows and credential-slot control tools. const preview = yield* executor.execute(ToolAddress.make("executor.openapi.previewSpec"), { spec: testApiSpecText(), }); expect(preview).toMatchObject({ ok: true }); }), ); it.effect("invokes static previewSpec through executor.execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const preview = unwrapInvocation( yield* executor.execute(ToolAddress.make("executor.openapi.previewSpec"), { spec: testApiSpecText(), }), ).data as { operationCount: number; operations?: unknown }; expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.operations).toBeUndefined(); }), ); // removed: "describes static previewSpec / addSpec output from Standard Schema" // — `tools.schema(address)` only resolves persisted per-connection tool rows // in v2 (the address must parse to the 5-segment // `tools.<integration>.<owner>.<connection>.<tool>` form). Static control // tools live outside the catalog and have no schema-view surface, so these // schema-introspection assertions no longer apply. it.effect("invokes static addSpec through executor.execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const result = unwrapInvocation(Lines 1–500 of 1543. Load more
// --------------------------------------------------------------------------- // OpenAPI plugin — v2 behaviour. // // Ported from the v1 suite to the v2 data model. The v1-only coverage (scope // shadowing, secret-backed credential slots, sources.configure binding // lifecycle, OAuth2 source-config slots, usagesForSecret, configFile mirroring) // is removed — those surfaces no longer exist in v2. See the inline // `// removed:` notes. The behaviours that survive (preview, static control // tools, addSpec → per-connection tools, invoke + transport envelope, auth // template rendering, removeSpec) are exercised against the v2 surface: // addSpec registers an integration, a connection produces the tools, and the // full `tools.<integration>.<owner>.<connection>.<tool>` address is executed. // --------------------------------------------------------------------------- import { describe, expect, it } from "@effect/vitest"; import { Effect, Option, Predicate, Schema } from "effect"; import { HttpApi, HttpApiBuilder, HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi"; import { FetchHttpClient, HttpServerRequest, HttpServerResponse } from "effect/unstable/http"; import { createExecutor, AuthTemplateSlug, ConnectionName, IntegrationAlreadyExistsError, IntegrationSlug, ToolAddress, makeInMemoryBlobStore, } from "@executor-js/sdk"; import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { makeTestConfig, makeTestWorkspaceHarness, memoryCredentialsPlugin, serveTestHttpApp, typeCheckOutputTypeScript, } from "@executor-js/sdk/testing"; import { openApiPlugin } from "./plugin"; import { type AuthenticationInput } from "./types"; import { addOpenApiTestConnection, makeOpenApiHttpApiTestIntegrationConfig, makeOpenApiTestSpecJson, serveMutableOpenApiSpecTestServer, serveOpenApiEchoTestServer, serveOpenApiHttpApiTestServer, unwrapInvocation, } from "../testing"; const TOOL_ERROR_TYPESCRIPT = "{ code: string; message: string; status?: number; details?: unknown; retryable?: boolean }"; const testPlugins = (httpClientLayer = FetchHttpClient.layer) => [openApiPlugin({ httpClientLayer }), memoryCredentialsPlugin()] as const; const recordingBlobStore = () => { const base = makeInMemoryBlobStore(); let writes = 0; return { store: { ...base, put: (namespace: string, key: string, value: string) => Effect.sync(() => { writes += 1; }).pipe(Effect.andThen(base.put(namespace, key, value))), }, writeCount: () => writes, }; }; // --------------------------------------------------------------------------- // Define a test API with Effect HttpApi // --------------------------------------------------------------------------- const Item = Schema.Struct({ id: Schema.Number, name: Schema.String, }); const EchoHeaders = Schema.Struct({ authorization: Schema.optional(Schema.String), "x-api-key": Schema.optional(Schema.String), }); class QueryValidationError extends Schema.TaggedErrorClass<QueryValidationError>()( "QueryValidationError", { message: Schema.String, }, ) {} const ItemsGroup = HttpApiGroup.make("items") .add(HttpApiEndpoint.get("listItems", "/items", { success: Schema.Array(Item) })) .add( HttpApiEndpoint.post("createItem", "/items", { payload: Schema.Struct({ name: Schema.String }), success: Item, }), ) .add( HttpApiEndpoint.get("getItem", "/items/:itemId", { params: Schema.Struct({ itemId: Schema.NumberFromString }), success: Item, }), ) .add( HttpApiEndpoint.get("echoHeaders", "/echo-headers", { success: EchoHeaders, }), ) .add( HttpApiEndpoint.get("queryRows", "/records/rows/:entryTypeId", { params: Schema.Struct({ entryTypeId: Schema.String }), success: Schema.Unknown, error: QueryValidationError, }), ); const TestApi = HttpApi.make("testApi").add(ItemsGroup); const testApiSpecText = () => { const spec = makeOpenApiHttpApiTestIntegrationConfig(TestApi, {}).spec; if (spec.kind === "blob") return spec.value; return spec.url; }; const MICROSOFT_GRAPH_V1_OPERATION_COUNT = 16_548; const microsoftGraphScaleSpecText = () => { const paths: Record<string, unknown> = {}; for (let index = 0; index < MICROSOFT_GRAPH_V1_OPERATION_COUNT; index += 1) { paths[`/users/{userId}/messages/${index}`] = { get: { operationId: `users_messages_list_${index}`, tags: [`Graph category ${index % 37}`], summary: `List synthetic Graph messages ${index}`, parameters: [ { name: "userId", in: "path", required: true, schema: { type: "string" }, }, { name: "$top", in: "query", schema: { type: "integer", format: "int32" }, }, { name: "$select", in: "query", style: "form", explode: false, schema: { type: "array", items: { type: "string" } }, }, ], responses: { "200": { description: "OK", content: { "application/json": { schema: { $ref: "#/components/schemas/MessageCollectionResponse" }, }, }, }, }, }, }; } return JSON.stringify({ openapi: "3.0.0", info: { title: "Microsoft Graph Scale Fixture", version: "v1.0", description: "Synthetic Graph-scale fixture for generic OpenAPI imports.", }, servers: [{ url: "https://graph.microsoft.com/v1.0" }], security: [{ MicrosoftGraph: ["User.Read", "Mail.Read"] }], components: { securitySchemes: { MicrosoftGraph: { type: "oauth2", flows: { authorizationCode: { authorizationUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize", tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", scopes: { "User.Read": "Read user profile", "Mail.Read": "Read user mail", }, }, clientCredentials: { tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token", scopes: { ".default": "Application permissions", }, }, }, }, }, schemas: { Message: { type: "object", properties: { id: { type: "string" }, subject: { type: "string" }, receivedDateTime: { type: "string", format: "date-time" }, }, }, MessageCollectionResponse: { type: "object", properties: { value: { type: "array", items: { $ref: "#/components/schemas/Message" }, }, }, }, }, }, paths, }); }; // --------------------------------------------------------------------------- // Implement handlers // --------------------------------------------------------------------------- const ITEMS = [ { id: 1, name: "Widget" }, { id: 2, name: "Gadget" }, { id: 3, name: "Doohickey" }, ]; const ItemsGroupLive = HttpApiBuilder.group(TestApi, "items", (handlers) => handlers .handle("listItems", () => Effect.succeed(ITEMS.map((item) => Item.make(item)))) .handle("createItem", (req) => Effect.succeed(Item.make({ id: ITEMS.length + 1, name: req.payload.name })), ) .handle("getItem", (req) => Effect.succeed( Item.make( ITEMS.find((i) => i.id === req.params.itemId) ?? { id: 0, name: "Unknown", }, ), ), ) .handle("echoHeaders", () => Effect.gen(function* () { const req = yield* HttpServerRequest.HttpServerRequest; return EchoHeaders.make({ authorization: req.headers["authorization"], "x-api-key": req.headers["x-api-key"], }); }), ) .handle("queryRows", () => Effect.fail( new QueryValidationError({ message: 'Field with name "DisplayName" does not exist', }), ), ), ); const servePluginTestApi = () => serveOpenApiHttpApiTestServer({ api: TestApi, handlersLayer: ItemsGroupLive, }); const recordValue = (value: unknown): Record<string, unknown> | undefined => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined; const withEchoCookieParameter = (spec: Record<string, unknown>): Record<string, unknown> => { const paths = recordValue(spec.paths); const pathItem = recordValue(paths?.["/echo-headers"]); const get = recordValue(pathItem?.get); if (!paths || !pathItem || !get) return spec; const parameters = Array.isArray(get.parameters) ? get.parameters : []; return { ...spec, paths: { ...paths, "/echo-headers": { ...pathItem, get: { ...get, parameters: [ ...parameters, { name: "sessionId", in: "cookie", required: true, schema: { type: "string" }, }, ], }, }, }, }; }; // An apiKey auth template that places the connection value into `x-api-key`. const apiKeyTemplate: AuthenticationInput = { slug: AuthTemplateSlug.make("apiKey"), type: "apiKey", headers: { "x-api-key": [{ type: "variable" as const, name: "token" }] }, }; // An oauth template — the connection value renders as a bearer token. const oauthTemplate: AuthenticationInput = { slug: AuthTemplateSlug.make("oauth"), kind: "oauth2", authorizationUrl: "https://auth.example.test/authorize", tokenUrl: "https://auth.example.test/token", scopes: ["read"], }; const serveOAuthDiscoverableOpenApiSpec = () => Effect.gen(function* () { let baseUrl = ""; const server = yield* serveTestHttpApp((request) => { if (request.url.includes("/api/schema")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ openapi: "3.0.0", info: { title: "PostHog-like API", version: "1.0.0" }, paths: { "/api/projects/": { get: { operationId: "projects_list", security: [{ PersonalAPIKeyAuth: ["project:read", "wizard_session:write"] }], responses: { "200": { description: "OK" } }, }, }, }, components: { securitySchemes: { PersonalAPIKeyAuth: { type: "http", scheme: "bearer" }, }, }, }), ); } if (request.url.includes("/.well-known/oauth-protected-resource")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ resource: baseUrl, authorization_servers: [baseUrl], scopes_supported: ["project:read"], }), ); } if (request.url.includes("/.well-known/oauth-authorization-server")) { return Effect.succeed( HttpServerResponse.jsonUnsafe({ issuer: baseUrl, authorization_endpoint: `${baseUrl}/oauth/authorize/`, token_endpoint: `${baseUrl}/oauth/token/`, registration_endpoint: `${baseUrl}/oauth/register/`, client_id_metadata_document_supported: true, response_types_supported: ["code"], grant_types_supported: ["authorization_code"], code_challenge_methods_supported: ["S256"], token_endpoint_auth_methods_supported: ["none"], scopes_supported: ["project:read"], }), ); } return Effect.succeed(HttpServerResponse.text("not found", { status: 404 })); }); baseUrl = server.baseUrl; return server; }); // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- describe("OpenAPI Plugin", () => { it.effect("previewSpec returns metadata and header presets", () => Effect.scoped( Effect.gen(function* () { const server = yield* servePluginTestApi(); const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const preview = yield* executor.openapi.previewSpec(server.specJson); expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.servers).toBeDefined(); }), ), ); it.effect("previewSpec discovers OAuth metadata from a URL-hosted bearer spec", () => Effect.scoped( Effect.gen(function* () { const server = yield* serveOAuthDiscoverableOpenApiSpec(); const executor = yield* createExecutor( makeTestConfig({ plugins: testPlugins(server.httpClientLayer) }), ); const preview = yield* executor.openapi.previewSpec(server.url("/api/schema/")); expect(preview.headerPresets.map((preset) => preset.label)).toContain("Bearer Token"); expect(preview.oauth2Presets).toHaveLength(1); const oauth = preview.oauth2Presets[0]!; expect(oauth.securitySchemeName).toBe("DiscoveredOAuth2"); expect(oauth.flow).toBe("authorizationCode"); expect(oauth.tokenUrl).toBe(`${server.baseUrl}/oauth/token/`); expect(Option.getOrNull(oauth.resource)).toBe(server.baseUrl); expect(oauth.supportsClientIdMetadataDocument).toBe(true); expect(oauth.scopes).toEqual({ "project:read": "" }); yield* executor.openapi.addSpec({ spec: { kind: "url", url: server.url("/api/schema/") }, slug: "posthog_like", baseUrl: server.baseUrl, }); const config = yield* executor.openapi.getConfig("posthog_like"); expect( (config?.authenticationTemplate ?? []).map((template) => ({ slug: String(template.slug), kind: template.kind, ...(template.kind === "oauth2" ? { resource: template.resource ?? null, supportsClientIdMetadataDocument: template.supportsClientIdMetadataDocument === true, } : {}), })), ).toEqual([ { slug: "apikey-0", kind: "apikey" }, { slug: "oauth-DiscoveredOAuth2", kind: "oauth2", resource: server.baseUrl, supportsClientIdMetadataDocument: true, }, ]); }), ), ); it.effect("exposes static openapi executor control tools via execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); // v2: static control tools are NOT part of `tools.list()` (that's the // persisted per-connection catalog) and aren't `tools.schema()`-resolvable; // they're dispatched by `execute`. Their presence is observable by a // successful invocation rather than a catalog listing. // removed: tools.list() / getSource / configureSource assertions — those // listed v1 static source rows and credential-slot control tools. const preview = yield* executor.execute(ToolAddress.make("executor.openapi.previewSpec"), { spec: testApiSpecText(), }); expect(preview).toMatchObject({ ok: true }); }), ); it.effect("invokes static previewSpec through executor.execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const preview = unwrapInvocation( yield* executor.execute(ToolAddress.make("executor.openapi.previewSpec"), { spec: testApiSpecText(), }), ).data as { operationCount: number; operations?: unknown }; expect(preview.operationCount).toBeGreaterThanOrEqual(2); expect(preview.operations).toBeUndefined(); }), ); // removed: "describes static previewSpec / addSpec output from Standard Schema" // — `tools.schema(address)` only resolves persisted per-connection tool rows // in v2 (the address must parse to the 5-segment // `tools.<integration>.<owner>.<connection>.<tool>` form). Static control // tools live outside the catalog and have no schema-view surface, so these // schema-introspection assertions no longer apply. it.effect("invokes static addSpec through executor.execute", () => Effect.gen(function* () { const executor = yield* createExecutor(makeTestConfig({ plugins: testPlugins() })); const result = unwrapInvocation(