import { describe, it, expect } from "@effect/vitest";import { Effect } from "effect"; import { createExecutor, makeTestConfig } from "@executor/sdk";import { graphqlPlugin } from "./plugin";import type { IntrospectionResult } from "./introspect"; // ---------------------------------------------------------------------------// 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, }, ], },}; describe("graphqlPlugin", () => { it("registers tools from introspection JSON", async () => { const executor = await Effect.runPromise( createExecutor( makeTestConfig({ plugins: [graphqlPlugin()], }), ), ); const result = await Effect.runPromise( executor.graphql.addSource({ endpoint: "http://localhost:4000/graphql", introspectionJson: JSON.stringify({ data: introspectionResult }), namespace: "test_api", }), ); expect(result.toolCount).toBe(2); const tools = await Effect.runPromise(executor.tools.list()); // +1 for the runtime "addSource" tool const graphqlTools = tools.filter((t) => t.pluginKey === "graphql"); expect(graphqlTools).toHaveLength(3); const queryTool = graphqlTools.find((t) => t.name === "query.hello"); expect(queryTool).toBeDefined(); expect(queryTool!.description).toBe("Say hello"); const mutationTool = graphqlTools.find((t) => t.name === "mutation.setGreeting"); expect(mutationTool).toBeDefined(); expect(mutationTool!.description).toBe("Set greeting message"); await Effect.runPromise(executor.close()); }); it("removes a source and its tools", async () => { const executor = await Effect.runPromise( createExecutor( makeTestConfig({ plugins: [graphqlPlugin()], }), ), ); await Effect.runPromise( executor.graphql.addSource({ endpoint: "http://localhost:4000/graphql", introspectionJson: JSON.stringify({ data: introspectionResult }), namespace: "removable", }), ); let tools = await Effect.runPromise(executor.tools.list()); expect(tools.filter((t) => t.pluginKey === "graphql")).toHaveLength(3); await Effect.runPromise(executor.graphql.removeSource("removable")); tools = await Effect.runPromise(executor.tools.list()); // Runtime addSource tool remains expect(tools.filter((t) => t.pluginKey === "graphql")).toHaveLength(1); await Effect.runPromise(executor.close()); }); it("lists sources", async () => { const executor = await Effect.runPromise( createExecutor( makeTestConfig({ plugins: [graphqlPlugin()], }), ), ); await Effect.runPromise( executor.graphql.addSource({ endpoint: "http://localhost:4000/graphql", introspectionJson: JSON.stringify({ data: introspectionResult }), namespace: "my_gql", }), ); const sources = await Effect.runPromise(executor.sources.list()); // 1 user source + 1 built-in runtime source const gqlSources = sources.filter((s) => s.kind === "graphql"); expect(gqlSources).toHaveLength(1); const userSource = gqlSources.find((s) => s.id === "my_gql"); expect(userSource).toBeDefined(); expect(userSource!.canRemove).toBe(true); const builtIn = sources.find((s) => s.id === "built-in"); expect(builtIn).toBeDefined(); expect(builtIn!.runtime).toBe(true); await Effect.runPromise(executor.close()); }); it("mutations require approval", async () => { const executor = await Effect.runPromise( createExecutor( makeTestConfig({ plugins: [graphqlPlugin()], }), ), ); await Effect.runPromise( executor.graphql.addSource({ endpoint: "http://localhost:4000/graphql", introspectionJson: JSON.stringify({ data: introspectionResult }), namespace: "approval_test", }), ); const tools = await Effect.runPromise(executor.tools.list()); const mutationTool = tools.find((t) => t.name === "mutation.setGreeting"); expect(mutationTool).toBeDefined(); // Verify the mutation requires approval via annotations const schema = await Effect.runPromise(executor.tools.schema(mutationTool!.id)); expect(schema).toBeDefined(); await Effect.runPromise(executor.close()); });});
import { describe, it, expect } from "@effect/vitest";
import { Effect } from "effect";
import { createExecutor, makeTestConfig } from "@executor/sdk";
import { graphqlPlugin } from "./plugin";
import type { IntrospectionResult } from "./introspect";
// ---------------------------------------------------------------------------
// 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,
},
],
},
};
describe("graphqlPlugin", () => {
it("registers tools from introspection JSON", async () => {
const executor = await Effect.runPromise(
createExecutor(
makeTestConfig({
plugins: [graphqlPlugin()],
}),
),
);
const result = await Effect.runPromise(
executor.graphql.addSource({
endpoint: "http://localhost:4000/graphql",
introspectionJson: JSON.stringify({ data: introspectionResult }),
namespace: "test_api",
}),
);
expect(result.toolCount).toBe(2);
const tools = await Effect.runPromise(executor.tools.list());
// +1 for the runtime "addSource" tool
const graphqlTools = tools.filter((t) => t.pluginKey === "graphql");
expect(graphqlTools).toHaveLength(3);
const queryTool = graphqlTools.find((t) => t.name === "query.hello");
expect(queryTool).toBeDefined();
expect(queryTool!.description).toBe("Say hello");
const mutationTool = graphqlTools.find((t) => t.name === "mutation.setGreeting");
expect(mutationTool).toBeDefined();
expect(mutationTool!.description).toBe("Set greeting message");
await Effect.runPromise(executor.close());
});
it("removes a source and its tools", async () => {
const executor = await Effect.runPromise(
createExecutor(
makeTestConfig({
plugins: [graphqlPlugin()],
}),
),
);
await Effect.runPromise(
executor.graphql.addSource({
endpoint: "http://localhost:4000/graphql",
introspectionJson: JSON.stringify({ data: introspectionResult }),
namespace: "removable",
}),
);
let tools = await Effect.runPromise(executor.tools.list());
expect(tools.filter((t) => t.pluginKey === "graphql")).toHaveLength(3);
await Effect.runPromise(executor.graphql.removeSource("removable"));
tools = await Effect.runPromise(executor.tools.list());
// Runtime addSource tool remains
expect(tools.filter((t) => t.pluginKey === "graphql")).toHaveLength(1);
await Effect.runPromise(executor.close());
});
it("lists sources", async () => {
const executor = await Effect.runPromise(
createExecutor(
makeTestConfig({
plugins: [graphqlPlugin()],
}),
),
);
await Effect.runPromise(
executor.graphql.addSource({
endpoint: "http://localhost:4000/graphql",
introspectionJson: JSON.stringify({ data: introspectionResult }),
namespace: "my_gql",
}),
);
const sources = await Effect.runPromise(executor.sources.list());
// 1 user source + 1 built-in runtime source
const gqlSources = sources.filter((s) => s.kind === "graphql");
expect(gqlSources).toHaveLength(1);
const userSource = gqlSources.find((s) => s.id === "my_gql");
expect(userSource).toBeDefined();
expect(userSource!.canRemove).toBe(true);
const builtIn = sources.find((s) => s.id === "built-in");
expect(builtIn).toBeDefined();
expect(builtIn!.runtime).toBe(true);
await Effect.runPromise(executor.close());
});
it("mutations require approval", async () => {
const executor = await Effect.runPromise(
createExecutor(
makeTestConfig({
plugins: [graphqlPlugin()],
}),
),
);
await Effect.runPromise(
executor.graphql.addSource({
endpoint: "http://localhost:4000/graphql",
introspectionJson: JSON.stringify({ data: introspectionResult }),
namespace: "approval_test",
}),
);
const tools = await Effect.runPromise(executor.tools.list());
const mutationTool = tools.find((t) => t.name === "mutation.setGreeting");
expect(mutationTool).toBeDefined();
// Verify the mutation requires approval via annotations
const schema = await Effect.runPromise(executor.tools.schema(mutationTool!.id));
expect(schema).toBeDefined();
await Effect.runPromise(executor.close());
});
});