408 lines · 12.4 KB · TypeScript
RawDownload
import { describe, it, expect } from "@effect/vitest";import { Effect } from "effect"; import {  createExecutor,  makeTestConfig,  Scope,  ScopeId,} from "@executor/sdk"; import { graphqlPlugin } from "./plugin";import type { IntrospectionResult } from "./introspect"; const TEST_SCOPE = "test-scope"; // ---------------------------------------------------------------------------// 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 }); describe("graphqlPlugin", () => {  it.effect("registers tools from introspection JSON", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       const result = yield* executor.graphql.addSource({        endpoint: "http://localhost:4000/graphql",        scope: "test-scope",        introspectionJson,        namespace: "test_api",      });      expect(result.toolCount).toBe(2);       const tools = yield* executor.tools.list();      const ids = tools.map((t) => t.id);      expect(ids).toContain("test_api.query.hello");      expect(ids).toContain("test_api.mutation.setGreeting");      // static control tool also present      expect(ids).toContain("graphql.addSource");       const queryTool = tools.find((t) => t.id === "test_api.query.hello");      expect(queryTool?.description).toBe("Say hello");       const mutationTool = tools.find(        (t) => t.id === "test_api.mutation.setGreeting",      );      expect(mutationTool?.description).toBe("Set greeting message");    }),  );   it.effect("removes a source and its tools", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       yield* executor.graphql.addSource({        endpoint: "http://localhost:4000/graphql",        scope: "test-scope",        introspectionJson,        namespace: "removable",      });       let tools = yield* executor.tools.list();      expect(        tools.filter((t) => t.sourceId === "removable").length,      ).toBe(2);       yield* executor.graphql.removeSource("removable", TEST_SCOPE);       tools = yield* executor.tools.list();      expect(        tools.filter((t) => t.sourceId === "removable").length,      ).toBe(0);       const source = yield* executor.graphql.getSource(        "removable",        TEST_SCOPE,      );      expect(source).toBeNull();    }),  );   it.effect("lists sources with the static control source", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       yield* executor.graphql.addSource({        endpoint: "http://localhost:4000/graphql",        scope: "test-scope",        introspectionJson,        namespace: "my_gql",      });       const sources = yield* executor.sources.list();      const dynamic = sources.find((s) => s.id === "my_gql");      expect(dynamic).toBeDefined();      expect(dynamic!.kind).toBe("graphql");      expect(dynamic!.canRemove).toBe(true);      expect(dynamic!.canEdit).toBe(true);      expect(dynamic!.runtime).toBe(false);       const control = sources.find((s) => s.id === "graphql");      expect(control).toBeDefined();      expect(control!.runtime).toBe(true);    }),  );   it.effect("mutations require approval via resolveAnnotations", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       yield* executor.graphql.addSource({        endpoint: "http://localhost:4000/graphql",        scope: "test-scope",        introspectionJson,        namespace: "approval_test",      });       const tools = yield* executor.tools.list();      const mutationTool = tools.find(        (t) => t.id === "approval_test.mutation.setGreeting",      );      expect(mutationTool).toBeDefined();      expect(mutationTool!.annotations?.requiresApproval).toBe(true);      expect(mutationTool!.annotations?.approvalDescription).toBe(        "mutation setGreeting",      );       const queryTool = tools.find(        (t) => t.id === "approval_test.query.hello",      );      expect(queryTool).toBeDefined();      expect(queryTool!.annotations?.requiresApproval).toBeFalsy();    }),  );   it.effect("updateSource patches endpoint/headers without re-registering", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       yield* executor.graphql.addSource({        endpoint: "http://localhost:4000/graphql",        scope: "test-scope",        introspectionJson,        namespace: "patched",      });       yield* executor.graphql.updateSource("patched", TEST_SCOPE, {        endpoint: "http://localhost:5000/graphql",        headers: { "x-custom": "abc" },      });       const source = yield* executor.graphql.getSource("patched", TEST_SCOPE);      expect(source?.endpoint).toBe("http://localhost:5000/graphql");      expect(source?.headers).toEqual({ "x-custom": "abc" });       // Tools still present (no re-register happened, but they were      // already there from addSource and haven't been removed).      const tools = yield* executor.tools.list();      expect(        tools.filter((t) => t.sourceId === "patched").length,      ).toBe(2);    }),  );   it.effect("static graphql.addSource delegates to extension", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({ plugins: [graphqlPlugin()] as const }),      );       const result = yield* executor.tools.invoke("graphql.addSource", {        endpoint: "http://localhost:4000/graphql",        introspectionJson,        namespace: "via_static",      });      expect(result).toEqual({ toolCount: 2 });       const tools = yield* executor.tools.list();      expect(        tools.filter((t) => t.sourceId === "via_static").length,      ).toBe(2);    }),  );   // -------------------------------------------------------------------------  // Multi-scope shadowing — regression suite covering the bug class where  // store reads/writes that don't pin scope_id collapse onto whichever row  // the scoped adapter's `scope_id IN (stack)` filter sees first. Each  // scenario is reproducible against the pre-fix store.  // -------------------------------------------------------------------------   const ORG_SCOPE = ScopeId.make("org-scope");  const USER_SCOPE = ScopeId.make("user-scope");   const stackedScopes = [    new Scope({ id: USER_SCOPE, name: "user", createdAt: new Date() }),    new Scope({ id: ORG_SCOPE, name: "org", createdAt: new Date() }),  ] as const;   it.effect("shadowed addSource does not wipe the outer-scope source", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          scopes: stackedScopes,          plugins: [graphqlPlugin()] as const,        }),      );       // Org-level base source      yield* executor.graphql.addSource({        endpoint: "http://org.example.com/graphql",        scope: ORG_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "Org Source",      });       // Per-user shadow with the same namespace      yield* executor.graphql.addSource({        endpoint: "http://user.example.com/graphql",        scope: USER_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "User Source",      });       const userView = yield* executor.graphql.getSource(        "shared",        USER_SCOPE as string,      );      const orgView = yield* executor.graphql.getSource(        "shared",        ORG_SCOPE as string,      );       // Both rows must coexist — innermost-wins reads come from the      // executor; the store's scope-pinned getters return the exact row.      expect(userView?.name).toBe("User Source");      expect(userView?.scope).toBe(USER_SCOPE as string);      expect(userView?.endpoint).toBe("http://user.example.com/graphql");      expect(orgView?.name).toBe("Org Source");      expect(orgView?.scope).toBe(ORG_SCOPE as string);      expect(orgView?.endpoint).toBe("http://org.example.com/graphql");    }),  );   it.effect("removeSource on user shadow leaves the org row intact", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          scopes: stackedScopes,          plugins: [graphqlPlugin()] as const,        }),      );       yield* executor.graphql.addSource({        endpoint: "http://org.example.com/graphql",        scope: ORG_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "Org Source",      });      yield* executor.graphql.addSource({        endpoint: "http://user.example.com/graphql",        scope: USER_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "User Source",      });       yield* executor.graphql.removeSource("shared", USER_SCOPE as string);       const userView = yield* executor.graphql.getSource(        "shared",        USER_SCOPE as string,      );      const orgView = yield* executor.graphql.getSource(        "shared",        ORG_SCOPE as string,      );       expect(userView).toBeNull();      expect(orgView?.name).toBe("Org Source");      expect(orgView?.endpoint).toBe("http://org.example.com/graphql");    }),  );   it.effect("updateSource on user shadow does not mutate the org row", () =>    Effect.gen(function* () {      const executor = yield* createExecutor(        makeTestConfig({          scopes: stackedScopes,          plugins: [graphqlPlugin()] as const,        }),      );       yield* executor.graphql.addSource({        endpoint: "http://org.example.com/graphql",        scope: ORG_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "Org Source",      });      yield* executor.graphql.addSource({        endpoint: "http://user.example.com/graphql",        scope: USER_SCOPE as string,        introspectionJson,        namespace: "shared",        name: "User Source",      });       yield* executor.graphql.updateSource("shared", USER_SCOPE as string, {        name: "User Renamed",        endpoint: "http://user-new.example.com/graphql",      });       const userView = yield* executor.graphql.getSource(        "shared",        USER_SCOPE as string,      );      const orgView = yield* executor.graphql.getSource(        "shared",        ORG_SCOPE as string,      );       expect(userView?.name).toBe("User Renamed");      expect(userView?.endpoint).toBe("http://user-new.example.com/graphql");      expect(orgView?.name).toBe("Org Source");      expect(orgView?.endpoint).toBe("http://org.example.com/graphql");    }),  );});