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, } 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 toolAddr = (integration: string, connection: string, tool: string): ToolAddress => ToolAddress.make(`tools.${integration}.org.${connection}.${tool}`); const createOrgConnection = ( executor: Awaited> extends Effect.Effect ? 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("classifies a 401 as connection_rejected even when the body is GraphQL-shaped", () => Effect.gen(function* () { // An auth gateway may answer with a GraphQL-style errors array AND a // 401 status. The transport status is authoritative: the failure must // reach the agent as a credential problem, not as graphql_errors. 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.jsonUnsafe( { errors: [{ message: "Not authenticated" }] }, { status: 401 }, ); }), ); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.url("/graphql"), slug: "auth_wall_graph", }); yield* createOrgConnection(executor, { integration: "auth_wall_graph", name: "main", template: "none", value: "unused", }); const result = yield* executor.execute(toolAddr("auth_wall_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "connection_rejected", status: 401 }, }); }), ); it.effect( "classifies a scope-insufficient 403 (OAuth error body) as oauth_scope_insufficient", () => Effect.gen(function* () { // The 403 body is an OAuth error object, not GraphQL-shaped at all — // exactly what an auth gateway in front of a GraphQL API returns. 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.jsonUnsafe( { error: "insufficient_scope", error_description: "needs repo scope" }, { status: 403 }, ); }), ); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.url("/graphql"), slug: "scope_graph", }); yield* createOrgConnection(executor, { integration: "scope_graph", name: "main", template: "none", value: "unused", }); const result = yield* executor.execute(toolAddr("scope_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "oauth_scope_insufficient", status: 403 }, }); const recovery = (result as { error: { details: { recovery: Record } } }) .error.details.recovery; expect( recovery.startOAuthTool, "no oauth.start hint: re-running the identical grant cannot satisfy the scope", ).toBeUndefined(); }), ); it.effect( "classifies a scope-insufficient 403 challenge header as oauth_scope_insufficient", () => 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("forbidden", { status: 403, headers: { "www-authenticate": 'Bearer realm="api", error="insufficient_scope"', }, }); }), ); const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.url("/graphql"), slug: "scope_hdr_graph", }); yield* createOrgConnection(executor, { integration: "scope_hdr_graph", name: "main", template: "none", value: "unused", }); const result = yield* executor.execute(toolAddr("scope_hdr_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "oauth_scope_insufficient", status: 403 }, }); }), ); 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, { integration: "oauth_graph", name: "main", template: "oauth2", value: "secret-token", }); yield* server.clearRequests; const result = yield* executor.execute(toolAddr("oauth_graph", "main", "query.hello"), { name: "Ada", }); expect(result).toEqual({ ok: true, data: { hello: "Hello Ada" } }); const requests = yield* server.requests; expect(requests[0]?.headers.authorization).toBe("Bearer secret-token"); }), ); it.effect("defers introspection: add makes no network call, connect introspects", () => Effect.gen(function* () { // Model an auth-required endpoint (e.g. GitHub): introspection without a // credential is rejected. Registering must NOT introspect, so add cannot // fail on auth; the credentialed introspection happens at connect time. const server = yield* serveGraphqlTestServer({ schema: makeGreetingGraphqlSchema(), auth: { validateAuthorization: (authorization) => Effect.succeed(authorization === "Bearer connect-token"), }, }); const executor = yield* makeExecutor(); // 1) Add to catalog with no add-time credential → no network call, 0 tools. const added = yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "deferred_auth", authenticationTemplate: [{ kind: "oauth2", slug: "oauth2" }], }); expect(added).toMatchObject({ slug: "deferred_auth", toolCount: 0 }); const afterAdd = yield* server.requests; expect(afterAdd.length).toBe(0); // 2) Connection-create introspects WITH the connection's credential. The // introspection request carries the rendered bearer and is accepted. yield* createOrgConnection(executor, { integration: "deferred_auth", name: "main", template: "oauth2", value: "connect-token", }); const afterConnect = yield* waitForRecordedRequests(server.requests, (requests) => requests.some((request) => request.payload.query?.includes("__schema")), ); const introspectionRequests = afterConnect.filter((request) => request.payload.query?.includes("__schema"), ); expect(introspectionRequests[0]?.headers.authorization).toBe("Bearer connect-token"); // The introspected operations become per-connection tools. const tools = yield* executor.tools.list(); const names = tools .filter((tool) => String(tool.integration) === "deferred_auth") .map((tool) => String(tool.name)); expect(names).toContain("query.hello"); expect(names).toContain("mutation.setGreeting"); }), ); }); describe("graphqlPlugin", () => { it.effect("registers tools per-connection from introspection JSON", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const result = yield* executor.graphql.addIntegration({ endpoint: "http://localhost:4000/graphql", slug: "test_api", introspectionJson, }); expect(result.toolCount).toBe(2); expect(result.slug).toBe("test_api"); yield* createOrgConnection(executor, { integration: "test_api", name: "main", template: "none", value: "unused", }); const tools = yield* executor.tools.list(); const names = tools .filter((t) => String(t.integration) === "test_api") .map((t) => String(t.name)); expect(names).toContain("query.hello"); expect(names).toContain("mutation.setGreeting"); // removed: v1 asserted the static `executor.graphql.*` tool was part of // `tools.list` / `tools.schema`. In v2 those surfaces return only the // per-connection catalog; static management tools are invoked by fqid via // `execute` and are not schema-introspectable. const queryTool = tools.find( (t) => String(t.integration) === "test_api" && String(t.name) === "query.hello", ); expect(queryTool?.description).toBe("Say hello"); const mutationTool = tools.find( (t) => String(t.integration) === "test_api" && String(t.name) === "mutation.setGreeting", ); expect(mutationTool?.description).toBe("Set greeting message"); }), ); it.effect("removes an integration and its connections drop its tools", () => Effect.gen(function* () { const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: "http://localhost:4000/graphql", slug: "removable", introspectionJson, }); yield* createOrgConnection(executor, { integration: "removable", name: "main", template: "none", value: "unused", }); let tools = yield* executor.tools.list(); expect(tools.filter((t) => String(t.integration) === "removable").length).toBe(2); yield* executor.connections.remove({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make("removable"), }); yield* executor.graphql.removeIntegration("removable"); tools = yield* executor.tools.list(); expect(tools.filter((t) => String(t.integration) === "removable").length).toBe(0); const integration = yield* executor.integrations.get(IntegrationSlug.make("removable")); expect(integration).toBeNull(); }), ); it.effect("lists the registered integration in the catalog", () => Effect.gen(function* () { const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: "http://localhost:4000/graphql", slug: "my_gql", name: "My GraphQL", introspectionJson, }); const integrations = yield* executor.integrations.list(); const dynamic = integrations.find((s) => String(s.slug) === "my_gql"); expect(dynamic).toBeDefined(); expect(dynamic!.kind).toBe("graphql"); expect(dynamic!.canRemove).toBe(true); expect(dynamic!.canRefresh).toBe(true); }), ); it.effect("mutations require approval via resolveTools annotations", () => Effect.gen(function* () { const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: "http://localhost:4000/graphql", slug: "approval_test", introspectionJson, }); yield* createOrgConnection(executor, { integration: "approval_test", name: "main", template: "none", value: "unused", }); const tools = yield* executor.tools.list(); const mutationTool = tools.find( (t) => String(t.integration) === "approval_test" && String(t.name) === "mutation.setGreeting", ); expect(mutationTool).toBeDefined(); expect(mutationTool!.annotations?.requiresApproval).toBe(true); expect(mutationTool!.annotations?.approvalDescription).toBe("mutation setGreeting"); const queryTool = tools.find( (t) => String(t.integration) === "approval_test" && String(t.name) === "query.hello", ); expect(queryTool).toBeDefined(); expect(queryTool!.annotations?.requiresApproval).toBeFalsy(); }), ); it.effect("graphql.configure patches the endpoint without re-registering", () => Effect.gen(function* () { const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: "http://localhost:4000/graphql", slug: "patched", introspectionJson, }); yield* executor.graphql.configure("patched", { endpoint: "http://localhost:5000/graphql", headers: { "x-custom": "abc" }, }); const config = yield* executor.graphql.getIntegration("patched"); expect(config).toMatchObject({ endpoint: "http://localhost:5000/graphql", headers: { "x-custom": "abc" }, }); }), ); it.effect("static executor.graphql.addIntegration delegates to the extension", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const result = yield* executor.execute( ToolAddress.make("executor.graphql.addIntegration"), { endpoint: "http://localhost:4000/graphql", slug: "via_static", name: "Via Static", introspectionJson, }, { onElicitation: "accept-all" }, ); expect(result).toMatchObject({ ok: true, data: { slug: "via_static", name: "Via Static" }, }); const integration = yield* executor.integrations.get(IntegrationSlug.make("via_static")); expect(integration).not.toBeNull(); }), ); it.effect("static executor.graphql.addIntegration registers an unreachable endpoint", () => Effect.gen(function* () { const config = makeTestConfig({ plugins: [memoryCredentialsPlugin(), graphqlPlugin()] as const, }); const executor = yield* createExecutor(config); // Registering a source must not introspect — so an unreachable endpoint // (no add-time credential, e.g. GitHub) registers cleanly instead of // 4xx-ing on a network call. Introspection is deferred to connect/invoke. const result = yield* executor.execute( ToolAddress.make("executor.graphql.addIntegration"), { endpoint: "http://127.0.0.1:1/graphql", slug: "deferred_graphql", name: "Deferred GraphQL", }, { onElicitation: "accept-all" }, ); expect(result).toMatchObject({ ok: true, data: { slug: "deferred_graphql", name: "Deferred GraphQL" }, }); yield* executor.close(); yield* Effect.promise(() => config.testDb.close()); }), ); it.effect("static executor.graphql.addIntegration surfaces malformed introspection JSON", () => Effect.gen(function* () { const executor = yield* makeExecutor(); // The offline path (caller supplies `introspectionJson`) still validates // the schema without a network call, surfacing an actionable failure. const result = yield* executor.execute( ToolAddress.make("executor.graphql.addIntegration"), { endpoint: "http://127.0.0.1:1/graphql", slug: "malformed_graphql", name: "Malformed GraphQL", introspectionJson: "{ not valid json", }, { onElicitation: "accept-all" }, ); expect(result).toMatchObject({ ok: false, error: { code: "graphql_introspection_failed" }, }); }), ); // removed: v1 "describes static addSource parameters from Standard Schema" // asserted `executor.tools.schema("executor.graphql.addSource")` returned a // TypeScript preview. In v2 `tools.schema` only resolves per-connection tool // rows (5-segment `tools.*` addresses); static management tools are no longer // schema-introspectable, so this case no longer applies. it.effect("returns an auth failure when an apiKey connection has no value", () => Effect.gen(function* () { const server = yield* serveGreetingServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug: "auth_required", introspectionJson, authenticationTemplate: [ { slug: "header", kind: "apikey", placements: [{ carrier: "header", name: "Authorization", prefix: "Bearer " }], }, ], }); // Create a connection that resolves to no value: reference a provider item // id the writable store never set. yield* executor.connections.create({ owner: "org", name: ConnectionName.make("main"), integration: IntegrationSlug.make("auth_required"), template: AuthTemplateSlug.make("header"), from: { provider: ProviderKey.make("memory"), id: ProviderItemId.make("never-set") }, }); const result = yield* executor.execute(toolAddr("auth_required", "main", "query.hello"), { name: "Ada", }); expect(result).toMatchObject({ ok: false, error: { code: "connection_value_missing", details: { category: "authentication" }, }, }); }), ); }); describe("graphqlPlugin detect URL-token fallback", () => { // Port 1 connection-refuses immediately, so introspection always fails and // the URL-token fallback is the only thing that can produce a candidate. it.effect("returns low-confidence candidate when path has /graphql segment", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const results = yield* executor.integrations.detect("http://127.0.0.1:1/api/graphql"); const gql = results.find((r) => r.kind === "graphql"); expect(gql).toBeDefined(); expect(gql?.confidence).toBe("low"); }), ); it.effect("matches graphql on hostname label", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const results = yield* executor.integrations.detect("http://graphql.127.0.0.1.nip.io:1/"); const gql = results.find((r) => r.kind === "graphql"); expect(gql?.confidence).toBe("low"); }), ); it.effect("does not match graphql as a substring", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const results = yield* executor.integrations.detect("http://127.0.0.1:1/graphqlite"); expect(results.find((r) => r.kind === "graphql")).toBeUndefined(); }), ); it.effect("returns null when no token match and introspection fails", () => Effect.gen(function* () { const executor = yield* makeExecutor(); const results = yield* executor.integrations.detect("http://127.0.0.1:1/api/v1"); expect(results.find((r) => r.kind === "graphql")).toBeUndefined(); }), ); }); // Issue #1146: against a large, real-world schema (GitLab) the auto-generated // operations were invalid GraphQL and every call against a rich object type // failed validation. The plugin now defaults to a scalar-leaf selection (always // valid, always cheap) and lets the caller pass an explicit `select` for nested // or list data. These drive the real plugin (live introspection -> generation -> // invocation) against a GitLab-shaped graphql-yoga server, which validates with // graphql-js exactly like the @emulators/gitlab surface, so a clean `ok: true` // proves the operation that went over the wire is valid GraphQL. describe("graphqlPlugin generates valid operations against rich schemas (#1146)", () => { const gitlabServer = serveGraphqlTestServer({ schema: makeGitlab1146Schema() }); const lastQuery = (requests: { readonly payload: { readonly query?: string } }[]): string => requests[requests.length - 1]?.payload.query ?? ""; const setup = (slug: string) => Effect.gen(function* () { const server = yield* gitlabServer; const executor = yield* makeExecutor(); yield* executor.graphql.addIntegration({ endpoint: server.endpoint, slug }); yield* createOrgConnection(executor, { integration: slug, name: "main", template: "none", value: "unused", }); yield* server.clearRequests; return { server, executor }; }); it.effect("default selection is scalar leaves only: valid, and never bare composites", () => Effect.gen(function* () { const { server, executor } = yield* setup("gitlab_default"); // metadata: scalar leaves only. `featureFlags` (required arg) and `kas` // (composite) are omitted rather than emitted invalidly. const meta = yield* executor.execute( toolAddr("gitlab_default", "main", "query.metadata"), {}, ); expect(meta).toMatchObject({ ok: true }); const metaQuery = lastQuery(yield* server.requests); expect(metaQuery).toContain("version"); expect(metaQuery).not.toContain("featureFlags"); expect(metaQuery).not.toContain("kas"); // currentUser: scalar leaves only. No `mergeRequests` (composite) bare. yield* server.clearRequests; const user = yield* executor.execute( toolAddr("gitlab_default", "main", "query.currentUser"), {}, ); expect(user).toMatchObject({ ok: true }); const userQuery = lastQuery(yield* server.requests); expect(userQuery).toContain("active"); expect(userQuery).not.toContain("mergeRequests"); }), ); it.effect("a caller-supplied `select` fetches nested/list data and stays valid", () => Effect.gen(function* () { const { server, executor } = yield* setup("gitlab_select"); const result = yield* executor.execute( toolAddr("gitlab_select", "main", "query.currentUser"), { select: "active mergeRequests { count nodes { id title author { name } } }" }, ); expect(result).toMatchObject({ ok: true }); const query = lastQuery(yield* server.requests); expect(query).toContain("nodes {"); expect(query).toContain("author {"); }), ); it.effect("`select` can supply a nested field's required argument", () => Effect.gen(function* () { const { server, executor } = yield* setup("gitlab_ff"); const result = yield* executor.execute(toolAddr("gitlab_ff", "main", "query.metadata"), { select: 'version featureFlags(names: ["flag_a"]) { name enabled }', }); expect(result).toMatchObject({ ok: true }); expect(lastQuery(yield* server.requests)).toContain("featureFlags(names:"); }), ); it.effect("an invalid `select` surfaces the server's validation error verbatim", () => Effect.gen(function* () { const { executor } = yield* setup("gitlab_bad"); // `author` is a composite emitted bare: the plugin passes the selection // through and surfaces the server's rejection rather than silently fixing // or swallowing it. const result = yield* executor.execute(toolAddr("gitlab_bad", "main", "query.currentUser"), { select: "mergeRequests { nodes { id author } }", }); expect(result).toMatchObject({ ok: false, error: { code: "graphql_errors" }, }); }), ); it.effect("rejects a malformed `select` locally, before any network call", () => Effect.gen(function* () { const { server, executor } = yield* setup("gitlab_syntax"); // Warm the binding cache (the first invoke introspects to materialize // bindings) so the malformed call below has no legitimate reason to hit the // wire: if it does, validation failed to short-circuit. yield* executor.execute(toolAddr("gitlab_syntax", "main", "query.currentUser"), {}); yield* server.clearRequests; const result = yield* executor.execute( toolAddr("gitlab_syntax", "main", "query.currentUser"), { select: "active mergeRequests { nodes {" }, ); expect(result).toMatchObject({ ok: false, error: { code: "graphql_invalid_selection" }, }); // Parse-check happens before the request is built, so nothing reached the server. expect((yield* server.requests).length).toBe(0); }), ); });