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, endpointForTelemetry, makeInMemoryBlobStore,} from "@executor-js/sdk";import { makeTestConfig, serveTestHttpApp, memoryCredentialsPlugin,} from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin";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 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, };}; 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), ...(input.value === undefined ? { values: {} } : { value: input.value }), }); describe("graphqlPlugin real protocol server", () => { it.effect("denies member schema persistence before writing an org blob", () => Effect.gen(function* () { const blobs = recordingBlobStore(); const config = makeTestConfig({ plugins: [graphqlPlugin()] as const }); const member = yield* createExecutor({ ...config, blobs: blobs.store, orgWrites: "denied", }); const error = yield* member.graphql .addIntegration({ endpoint: "https://example.test/graphql", slug: "denied-graphql", introspectionJson, }) .pipe(Effect.flip); expect(error).toMatchObject({ _tag: "OrgWriteDeniedError" }); expect(blobs.writeCount()).toBe(0); }), ); 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: "", }); 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("rejects credential input for no-auth GraphQL and accepts an empty input map", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); const integration = IntegrationSlug.make("no_auth_create"); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: String(integration), }); const error = yield* executor.connections .create({ owner: "org", name: ConnectionName.make("with-secret"), integration, template: AuthTemplateSlug.make("none"), value: "must-not-be-stored", }) .pipe(Effect.flip); expect(error).toMatchObject({ _tag: "InvalidConnectionInputError", message: "A no-auth connection cannot accept credential inputs.", }); expect(yield* executor.connections.list({ integration })).toEqual([]); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("public"), integration, template: AuthTemplateSlug.make("none"), values: {}, }); expect(String(connection.address)).toBe("tools.no_auth_create.org.public"); expect(yield* executor.connections.list({ integration })).toHaveLength(1); }), ); 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", }); 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", }); Lines 1–500 of 1503. 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, endpointForTelemetry, makeInMemoryBlobStore, } from "@executor-js/sdk"; import { makeTestConfig, serveTestHttpApp, memoryCredentialsPlugin, } from "@executor-js/sdk/testing"; import { graphqlPlugin } from "./plugin"; 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 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, }; }; 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), ...(input.value === undefined ? { values: {} } : { value: input.value }), }); describe("graphqlPlugin real protocol server", () => { it.effect("denies member schema persistence before writing an org blob", () => Effect.gen(function* () { const blobs = recordingBlobStore(); const config = makeTestConfig({ plugins: [graphqlPlugin()] as const }); const member = yield* createExecutor({ ...config, blobs: blobs.store, orgWrites: "denied", }); const error = yield* member.graphql .addIntegration({ endpoint: "https://example.test/graphql", slug: "denied-graphql", introspectionJson, }) .pipe(Effect.flip); expect(error).toMatchObject({ _tag: "OrgWriteDeniedError" }); expect(blobs.writeCount()).toBe(0); }), ); 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: "", }); 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("rejects credential input for no-auth GraphQL and accepts an empty input map", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); const integration = IntegrationSlug.make("no_auth_create"); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: String(integration), }); const error = yield* executor.connections .create({ owner: "org", name: ConnectionName.make("with-secret"), integration, template: AuthTemplateSlug.make("none"), value: "must-not-be-stored", }) .pipe(Effect.flip); expect(error).toMatchObject({ _tag: "InvalidConnectionInputError", message: "A no-auth connection cannot accept credential inputs.", }); expect(yield* executor.connections.list({ integration })).toEqual([]); const connection = yield* executor.connections.create({ owner: "org", name: ConnectionName.make("public"), integration, template: AuthTemplateSlug.make("none"), values: {}, }); expect(String(connection.address)).toBe("tools.no_auth_create.org.public"); expect(yield* executor.connections.list({ integration })).toHaveLength(1); }), ); 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", }); 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", });