import { describe, it, expect } from "@effect/vitest";import { Effect, Layer } from "effect";import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerRequest, HttpServerResponse,} from "effect/unstable/http"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, createExecutor,} from "@executor-js/sdk";import { makeTestConfig, serveTestHttpApp, memoryCredentialsPlugin,} from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin";import { endpointForTelemetry } from "./invoke";import { introspect } from "./introspect";import type { IntrospectionResult } from "./introspect";import { makeGitlab1146Schema, makeGreetingGraphqlSchema, serveGraphqlFailureTestServer, serveGraphqlTestServer, waitForRecordedRequests,} from "../testing"; // removed: v1 secret browser-handoff, credential-binding scopes, usagesForSecret/// usagesForConnection, multi-scope shadowing, and `executor.sources.*` /// `executor.secrets.*` flows — those surfaces no longer exist in the v2 model// (secrets / sources / scope stack / credential bindings are gone). Coverage is// ported to the v2 surface: integrations.register via `graphql.addIntegration`,// per-connection tool production via `connections.create` -> resolveTools, and// auth-template rendering in `invokeTool`. // ---------------------------------------------------------------------------// Mock introspection response// --------------------------------------------------------------------------- const introspectionResult: IntrospectionResult = { __schema: { queryType: { name: "Query" }, mutationType: { name: "Mutation" }, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "hello", description: "Say hello", args: [ { name: "name", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "Mutation", description: null, fields: [ { name: "setGreeting", description: "Set greeting message", args: [ { name: "message", description: null, type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null }, }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], },}; const introspectionJson = JSON.stringify({ data: introspectionResult });const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() }); const makeExecutor = () => createExecutor( makeTestConfig({ plugins: [memoryCredentialsPlugin(), graphqlPlugin()] as const }), ); const toolAddr = (integration: string, connection: string, tool: string): ToolAddress => ToolAddress.make(`tools.${integration}.org.${connection}.${tool}`); const createOrgConnection = ( executor: Awaited<ReturnType<typeof makeExecutor>> extends Effect.Effect<infer A> ? A : never, input: { readonly integration: string; readonly name: string; readonly template: string; readonly value: string; },) => executor.connections.create({ owner: "org", name: ConnectionName.make(input.name), integration: IntegrationSlug.make(input.integration), template: AuthTemplateSlug.make(input.template), value: input.value, }); describe("graphqlPlugin real protocol server", () => { it("uses query-free endpoints for invocation attributes", () => { expect(endpointForTelemetry("https://api.example.test/graphql?token=secret#section")).toBe( "https://api.example.test/graphql", ); }); it.effect("includes redacted upstream text in introspection status errors", () => Effect.gen(function* () { const server = yield* serveGraphqlFailureTestServer({ status: 500, body: 'upstream failed {"access_token":"secret-value"} token=another-secret', }); const error = yield* introspect(server.endpoint).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", 'Introspection failed with status 500: upstream failed {"access_token":"[redacted]"} token=[redacted]', ); expect(error).not.toHaveProperty("message", expect.stringContaining("secret-value")); expect(error).not.toHaveProperty("message", expect.stringContaining("another-secret")); }), ); it.effect("includes safe upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Resource protected by organization SSO" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Resource protected by organization SSO", ); }), ); it.effect("redacts secrets from upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Authorization: Bearer github-secret-token" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Authorization: [redacted]", ); expect(error).not.toHaveProperty("message", expect.stringContaining("github-secret-token")); }), ); it.effect("accepts standard introspection responses with omitted deepest ofType", () => Effect.gen(function* () { const deepType = { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", }, }, }, }, }, }; const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe({ data: { __schema: { queryType: { name: "Query" }, mutationType: null, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "deep", description: null, args: [], type: deepType, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }, }), ), ); const result = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), ); expect(result.__schema.queryType?.name).toBe("Query"); }), ); it.effect("registers without a network call and introspects at connection-create", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); // Registering a source is a catalog statement, not a network call: with no // pre-supplied schema, add makes zero requests and yields zero tools. const result = yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "live_graph", name: "Live Graph", }); expect(result).toMatchObject({ slug: "live_graph", toolCount: 0 }); const addRequests = yield* server.requests; expect(addRequests.length).toBe(0); // Creating a connection is where introspection happens (like MCP defers // discovery to connect time) and materializes the per-connection tools. yield* createOrgConnection(executor, { integration: "live_graph", name: "default", template: "none", value: "unused", }); yield* waitForRecordedRequests(server.requests, (requests) => requests.some((request) => request.payload.query?.includes("__schema")), ); const tools = yield* executor.tools.list(); expect(tools.map((tool) => String(tool.name))).toEqual( expect.arrayContaining(["query.hello", "mutation.setGreeting"]), ); }), ); it.effect("uses the executor HttpClient layer for connection-time introspection", () => Effect.gen(function* () { const seen: string[] = []; const httpClientLayer = Layer.succeed(HttpClient.HttpClient)( HttpClient.make((request: HttpClientRequest.HttpClientRequest) => { seen.push(request.url); return Effect.succeed( HttpClientResponse.fromWeb( request, new Response(JSON.stringify({ data: introspectionResult }), { status: 200, headers: { "content-type": "application/json" }, }), ), ); }), ); const config = makeTestConfig({ plugins: [memoryCredentialsPlugin(), graphqlPlugin()] as const, }); const executor = yield* createExecutor({ ...config, httpClientLayer }); yield* executor.graphql.addIntegration({ endpoint: "https://internal.example/graphql", slug: "guarded_graph", name: "Guarded Graph", }); yield* createOrgConnection(executor, { integration: "guarded_graph", name: "default", template: "none", value: "unused", }); const tools = yield* executor.tools.list(); expect(seen).toEqual(["https://internal.example/graphql"]); expect(tools.map((tool) => String(tool.name))).toEqual( expect.arrayContaining(["query.hello", "mutation.setGreeting"]), ); }), ); it.effect("invokes a live query through an apiKey header template", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "live_invoke", queryParams: { trace: "on" }, authenticationTemplate: [ { slug: "header", kind: "apikey", placements: [{ carrier: "header", name: "x-static" }] }, ], }); yield* createOrgConnection(executor, { integration: "live_invoke", name: "main", template: "header", value: "abc", }); // First invoke materializes operation bindings (one introspection) and runs // the query. Drive a second invoke against the now-warm cache so the query // request is the only thing on the wire and the assertions stay precise. yield* executor.execute(toolAddr("live_invoke", "main", "query.hello"), { name: "Ada" }); yield* server.clearRequests; const result = yield* executor.execute(toolAddr("live_invoke", "main", "query.hello"), { name: "Ada", }); expect(result).toEqual({ ok: true, data: { hello: "Hello Ada" } }); const requests = yield* server.requests; expect(requests.length).toBe(1); expect(requests[0]?.headers["x-static"]).toBe("abc"); expect(new URL(requests[0]!.url).searchParams.get("trace")).toBe("on"); expect(requests[0]?.payload.variables).toEqual({ name: "Ada" }); }), ); it.effect("sends named operations derived from the field name", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "named_ops", }); yield* createOrgConnection(executor, { integration: "named_ops", name: "main", template: "none", value: "unused", }); yield* executor.execute(toolAddr("named_ops", "main", "query.hello"), { name: "Ada" }); yield* server.clearRequests; yield* executor.execute(toolAddr("named_ops", "main", "query.hello"), { name: "Ada" }); yield* executor.execute(toolAddr("named_ops", "main", "mutation.setGreeting"), { message: "hi", }); const requests = yield* server.requests; expect(requests[0]?.payload.query).toMatch(/^query Hello\b/); expect(requests[1]?.payload.query).toMatch(/^mutation SetGreeting\b/); }), ); it.effect("surfaces non-2xx invocation responses as ToolResult.fail", () => Effect.gen(function* () { const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const webRequest = yield* HttpServerRequest.toWeb(request); const body = yield* Effect.promise(() => webRequest.text()); if (body.includes("__schema")) { return HttpServerResponse.jsonUnsafe({ data: introspectionResult }); } return HttpServerResponse.text("temporary upstream outage", { status: 503, contentType: "text/plain", }); }), ); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.url("/graphql"), slug: "http_error_graph", }); yield* createOrgConnection(executor, { integration: "http_error_graph", name: "main", template: "none", value: "unused", }); const result = yield* executor.execute(toolAddr("http_error_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "graphql_http_error", status: 503, message: "GraphQL request failed with HTTP 503", }, }); }), ); it.effect("invokes OAuth-backed integrations with a rendered bearer token", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer secret-token"), }, }); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "oauth_graph", introspectionJson, authenticationTemplate: [{ kind: "oauth2", slug: "oauth2" }], }); yield* createOrgConnection(executor, {Lines 1–500 of 1018. Load more
import { describe, it, expect } from "@effect/vitest"; import { Effect, Layer } from "effect"; import { HttpClient, HttpClientRequest, HttpClientResponse, HttpServerRequest, HttpServerResponse, } from "effect/unstable/http"; import { AuthTemplateSlug, ConnectionName, IntegrationSlug, ProviderItemId, ProviderKey, ToolAddress, createExecutor, } from "@executor-js/sdk"; import { makeTestConfig, serveTestHttpApp, memoryCredentialsPlugin, } from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin"; import { endpointForTelemetry } from "./invoke"; import { introspect } from "./introspect"; import type { IntrospectionResult } from "./introspect"; import { makeGitlab1146Schema, makeGreetingGraphqlSchema, serveGraphqlFailureTestServer, serveGraphqlTestServer, waitForRecordedRequests, } from "../testing"; // removed: v1 secret browser-handoff, credential-binding scopes, usagesForSecret/ // usagesForConnection, multi-scope shadowing, and `executor.sources.*` / // `executor.secrets.*` flows — those surfaces no longer exist in the v2 model // (secrets / sources / scope stack / credential bindings are gone). Coverage is // ported to the v2 surface: integrations.register via `graphql.addIntegration`, // per-connection tool production via `connections.create` -> resolveTools, and // auth-template rendering in `invokeTool`. // --------------------------------------------------------------------------- // Mock introspection response // --------------------------------------------------------------------------- const introspectionResult: IntrospectionResult = { __schema: { queryType: { name: "Query" }, mutationType: { name: "Mutation" }, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "hello", description: "Say hello", args: [ { name: "name", description: null, type: { kind: "SCALAR", name: "String", ofType: null }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "OBJECT", name: "Mutation", description: null, fields: [ { name: "setGreeting", description: "Set greeting message", args: [ { name: "message", description: null, type: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", ofType: null }, }, defaultValue: null, }, ], type: { kind: "SCALAR", name: "String", ofType: null }, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }; const introspectionJson = JSON.stringify({ data: introspectionResult }); const serveGreetingServer = serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema() }); const makeExecutor = () => createExecutor( makeTestConfig({ plugins: [memoryCredentialsPlugin(), graphqlPlugin()] as const }), ); const toolAddr = (integration: string, connection: string, tool: string): ToolAddress => ToolAddress.make(`tools.${integration}.org.${connection}.${tool}`); const createOrgConnection = ( executor: Awaited<ReturnType<typeof makeExecutor>> extends Effect.Effect<infer A> ? A : never, input: { readonly integration: string; readonly name: string; readonly template: string; readonly value: string; }, ) => executor.connections.create({ owner: "org", name: ConnectionName.make(input.name), integration: IntegrationSlug.make(input.integration), template: AuthTemplateSlug.make(input.template), value: input.value, }); describe("graphqlPlugin real protocol server", () => { it("uses query-free endpoints for invocation attributes", () => { expect(endpointForTelemetry("https://api.example.test/graphql?token=secret#section")).toBe( "https://api.example.test/graphql", ); }); it.effect("includes redacted upstream text in introspection status errors", () => Effect.gen(function* () { const server = yield* serveGraphqlFailureTestServer({ status: 500, body: 'upstream failed {"access_token":"secret-value"} token=another-secret', }); const error = yield* introspect(server.endpoint).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", 'Introspection failed with status 500: upstream failed {"access_token":"[redacted]"} token=[redacted]', ); expect(error).not.toHaveProperty("message", expect.stringContaining("secret-value")); expect(error).not.toHaveProperty("message", expect.stringContaining("another-secret")); }), ); it.effect("includes safe upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Resource protected by organization SSO" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Resource protected by organization SSO", ); }), ); it.effect("redacts secrets from upstream JSON messages in introspection status errors", () => Effect.gen(function* () { const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe( { message: "Authorization: Bearer github-secret-token" }, { status: 403 }, ), ), ); const error = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), Effect.flip, ); expect(error).toHaveProperty( "message", "Introspection failed with status 403: Authorization: [redacted]", ); expect(error).not.toHaveProperty("message", expect.stringContaining("github-secret-token")); }), ); it.effect("accepts standard introspection responses with omitted deepest ofType", () => Effect.gen(function* () { const deepType = { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "LIST", name: null, ofType: { kind: "NON_NULL", name: null, ofType: { kind: "SCALAR", name: "String", }, }, }, }, }, }; const server = yield* serveTestHttpApp(() => Effect.succeed( HttpServerResponse.jsonUnsafe({ data: { __schema: { queryType: { name: "Query" }, mutationType: null, types: [ { kind: "OBJECT", name: "Query", description: null, fields: [ { name: "deep", description: null, args: [], type: deepType, }, ], inputFields: null, enumValues: null, }, { kind: "SCALAR", name: "String", description: null, fields: null, inputFields: null, enumValues: null, }, ], }, }, }), ), ); const result = yield* introspect(server.url("/graphql")).pipe( Effect.provide(server.httpClientLayer), ); expect(result.__schema.queryType?.name).toBe("Query"); }), ); it.effect("registers without a network call and introspects at connection-create", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); // Registering a source is a catalog statement, not a network call: with no // pre-supplied schema, add makes zero requests and yields zero tools. const result = yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "live_graph", name: "Live Graph", }); expect(result).toMatchObject({ slug: "live_graph", toolCount: 0 }); const addRequests = yield* server.requests; expect(addRequests.length).toBe(0); // Creating a connection is where introspection happens (like MCP defers // discovery to connect time) and materializes the per-connection tools. yield* createOrgConnection(executor, { integration: "live_graph", name: "default", template: "none", value: "unused", }); yield* waitForRecordedRequests(server.requests, (requests) => requests.some((request) => request.payload.query?.includes("__schema")), ); const tools = yield* executor.tools.list(); expect(tools.map((tool) => String(tool.name))).toEqual( expect.arrayContaining(["query.hello", "mutation.setGreeting"]), ); }), ); it.effect("uses the executor HttpClient layer for connection-time introspection", () => Effect.gen(function* () { const seen: string[] = []; const httpClientLayer = Layer.succeed(HttpClient.HttpClient)( HttpClient.make((request: HttpClientRequest.HttpClientRequest) => { seen.push(request.url); return Effect.succeed( HttpClientResponse.fromWeb( request, new Response(JSON.stringify({ data: introspectionResult }), { status: 200, headers: { "content-type": "application/json" }, }), ), ); }), ); const config = makeTestConfig({ plugins: [memoryCredentialsPlugin(), graphqlPlugin()] as const, }); const executor = yield* createExecutor({ ...config, httpClientLayer }); yield* executor.graphql.addIntegration({ endpoint: "https://internal.example/graphql", slug: "guarded_graph", name: "Guarded Graph", }); yield* createOrgConnection(executor, { integration: "guarded_graph", name: "default", template: "none", value: "unused", }); const tools = yield* executor.tools.list(); expect(seen).toEqual(["https://internal.example/graphql"]); expect(tools.map((tool) => String(tool.name))).toEqual( expect.arrayContaining(["query.hello", "mutation.setGreeting"]), ); }), ); it.effect("invokes a live query through an apiKey header template", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "live_invoke", queryParams: { trace: "on" }, authenticationTemplate: [ { slug: "header", kind: "apikey", placements: [{ carrier: "header", name: "x-static" }] }, ], }); yield* createOrgConnection(executor, { integration: "live_invoke", name: "main", template: "header", value: "abc", }); // First invoke materializes operation bindings (one introspection) and runs // the query. Drive a second invoke against the now-warm cache so the query // request is the only thing on the wire and the assertions stay precise. yield* executor.execute(toolAddr("live_invoke", "main", "query.hello"), { name: "Ada" }); yield* server.clearRequests; const result = yield* executor.execute(toolAddr("live_invoke", "main", "query.hello"), { name: "Ada", }); expect(result).toEqual({ ok: true, data: { hello: "Hello Ada" } }); const requests = yield* server.requests; expect(requests.length).toBe(1); expect(requests[0]?.headers["x-static"]).toBe("abc"); expect(new URL(requests[0]!.url).searchParams.get("trace")).toBe("on"); expect(requests[0]?.payload.variables).toEqual({ name: "Ada" }); }), ); it.effect("sends named operations derived from the field name", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "named_ops", }); yield* createOrgConnection(executor, { integration: "named_ops", name: "main", template: "none", value: "unused", }); yield* executor.execute(toolAddr("named_ops", "main", "query.hello"), { name: "Ada" }); yield* server.clearRequests; yield* executor.execute(toolAddr("named_ops", "main", "query.hello"), { name: "Ada" }); yield* executor.execute(toolAddr("named_ops", "main", "mutation.setGreeting"), { message: "hi", }); const requests = yield* server.requests; expect(requests[0]?.payload.query).toMatch(/^query Hello\b/); expect(requests[1]?.payload.query).toMatch(/^mutation SetGreeting\b/); }), ); it.effect("surfaces non-2xx invocation responses as ToolResult.fail", () => Effect.gen(function* () { const server = yield* serveTestHttpApp((request) => Effect.gen(function* () { const webRequest = yield* HttpServerRequest.toWeb(request); const body = yield* Effect.promise(() => webRequest.text()); if (body.includes("__schema")) { return HttpServerResponse.jsonUnsafe({ data: introspectionResult }); } return HttpServerResponse.text("temporary upstream outage", { status: 503, contentType: "text/plain", }); }), ); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.url("/graphql"), slug: "http_error_graph", }); yield* createOrgConnection(executor, { integration: "http_error_graph", name: "main", template: "none", value: "unused", }); const result = yield* executor.execute(toolAddr("http_error_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "graphql_http_error", status: 503, message: "GraphQL request failed with HTTP 503", }, }); }), ); it.effect("invokes OAuth-backed integrations with a rendered bearer token", () => Effect.gen(function* () { const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer secret-token"), }, }); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "oauth_graph", introspectionJson, authenticationTemplate: [{ kind: "oauth2", slug: "oauth2" }], }); yield* createOrgConnection(executor, {