304 lines · 8.5 KB · TypeScript
RawDownload
import { describe, it, expect } from "@effect/vitest";import { Effect, Schema } from "effect";import * as NodeFileSystem from "@effect/platform-node/NodeFileSystem";import { FileSystem } from "effect";import { join } from "node:path"; import { ExecutorFileConfig } from "./schema";import { ConfigParseError, loadConfig } from "./load";import {  addIntegrationToConfig,  removeIntegrationFromConfig,  writeConfig,  addSecretToConfig,  removeSecretFromConfig,} from "./write"; const decodeExecutorFileConfig = Schema.decodeUnknownSync(ExecutorFileConfig); const withTmpDir = <A, E>(fn: (dir: string) => Effect.Effect<A, E, FileSystem.FileSystem>) =>  Effect.gen(function* () {    const fs = yield* FileSystem.FileSystem;    const dir = yield* fs.makeTempDirectoryScoped({      prefix: "executor-config-test-",    });    return yield* fn(dir);  }).pipe(Effect.scoped, Effect.provide(NodeFileSystem.layer)); describe("ExecutorFileConfig schema", () => {  it("decodes a minimal config", () => {    const raw = { integrations: [] };    const result = decodeExecutorFileConfig(raw);    expect(result.integrations).toEqual([]);  });   it("decodes a full config with all integration types", () => {    const raw = {      name: "test",      integrations: [        {          kind: "openapi",          spec: "https://example.com/openapi.json",          namespace: "example",          headers: {            Authorization: { value: "secret-public-ref:my-token", prefix: "Bearer " },          },        },        {          kind: "graphql",          endpoint: "https://example.com/graphql",          namespace: "gql",        },        {          kind: "mcp",          transport: "remote",          name: "Remote MCP",          endpoint: "https://mcp.example.com/sse",        },        {          kind: "mcp",          transport: "stdio",          name: "Local MCP",          command: "npx",          args: ["-y", "some-server"],        },      ],      secrets: {        "my-token": {          name: "My Token",          provider: "keychain",          purpose: "Auth",        },      },    };     const result = decodeExecutorFileConfig(raw);    expect(result.integrations).toHaveLength(4);    expect(result.name).toBe("test");    expect(result.secrets!["my-token"]!.name).toBe("My Token");  });   it("rejects invalid integration kind", () => {    const raw = {      integrations: [{ kind: "invalid", endpoint: "http://example.com" }],    };    expect(() => decodeExecutorFileConfig(raw)).toThrow();  });}); describe("loadConfig", () => {  it.effect("returns null for missing file", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const result = yield* loadConfig(join(dir, "nonexistent.jsonc"));        expect(result).toBeNull();      }),    ),  );   it.effect("loads a valid JSONC file with comments", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = join(dir, "executor.jsonc");        yield* fs.writeFileString(          path,          `{  // This is a comment  "name": "test",  "integrations": [    {      "kind": "openapi",      "spec": "https://example.com/spec.json"    }  ]}`,        );         const result = yield* loadConfig(path);        expect(result).not.toBeNull();        expect(result!.name).toBe("test");        expect(result!.integrations).toHaveLength(1);      }),    ),  );   it.effect("fails on invalid JSON", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = join(dir, "executor.jsonc");        yield* fs.writeFileString(path, "{ invalid json }");         const result = yield* loadConfig(path).pipe(          Effect.catchTag("ConfigParseError", (error) => Effect.succeed(error)),        );        expect(result).toBeInstanceOf(ConfigParseError);      }),    ),  );}); describe("write operations", () => {  it.effect("addIntegrationToConfig creates file and adds integration", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = join(dir, "executor.jsonc");         yield* addIntegrationToConfig(path, {          kind: "openapi",          spec: "https://example.com/spec.json",          namespace: "example",        });         const content = yield* fs.readFileString(path);        expect(content).toContain("openapi");        expect(content).toContain("example");         const config = yield* loadConfig(path);        expect(config!.integrations).toHaveLength(1);        expect(config!.integrations![0]!.kind).toBe("openapi");      }),    ),  );   it.effect("addIntegrationToConfig appends to existing integrations", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* addIntegrationToConfig(path, {          kind: "openapi",          spec: "https://example.com/spec.json",          namespace: "first",        });         yield* addIntegrationToConfig(path, {          kind: "graphql",          endpoint: "https://example.com/graphql",          namespace: "second",        });         const config = yield* loadConfig(path);        expect(config!.integrations).toHaveLength(2);        expect(config!.integrations![0]!.kind).toBe("openapi");        expect(config!.integrations![1]!.kind).toBe("graphql");      }),    ),  );   it.effect("addIntegrationToConfig preserves comments", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = join(dir, "executor.jsonc");         yield* fs.writeFileString(          path,          `{  // My project config  "name": "test",  "integrations": [    {      "kind": "openapi",      "spec": "https://example.com/spec.json",      "namespace": "first"    }  ]}`,        );         yield* addIntegrationToConfig(path, {          kind: "graphql",          endpoint: "https://example.com/graphql",          namespace: "second",        });         const content = yield* fs.readFileString(path);        expect(content).toContain("// My project config");        expect(content).toContain("graphql");      }),    ),  );   it.effect("addIntegrationToConfig replaces existing integration with same namespace", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* addIntegrationToConfig(path, {          kind: "openapi",          spec: "https://example.com/v1.json",          namespace: "example",        });         // Add again with same namespace but different spec        yield* addIntegrationToConfig(path, {          kind: "openapi",          spec: "https://example.com/v2.json",          namespace: "example",        });         const config = yield* loadConfig(path);        // Should have 1, not 2        expect(config!.integrations).toHaveLength(1);        expect((config!.integrations![0] as { spec: string }).spec).toBe(          "https://example.com/v2.json",        );      }),    ),  );   it.effect("removeIntegrationFromConfig removes by namespace", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* writeConfig(path, {          integrations: [            {              kind: "openapi",              spec: "https://example.com/spec.json",              namespace: "keep",            },            {              kind: "graphql",              endpoint: "https://example.com/graphql",              namespace: "remove-me",            },          ],        });         yield* removeIntegrationFromConfig(path, "remove-me");         const config = yield* loadConfig(path);        expect(config!.integrations).toHaveLength(1);        expect(config!.integrations![0]!.kind).toBe("openapi");      }),    ),  );   it.effect("addSecretToConfig and removeSecretFromConfig", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* writeConfig(path, { integrations: [] });         yield* addSecretToConfig(path, "my-token", {          name: "My Token",          provider: "keychain",        });         let config = yield* loadConfig(path);        expect(config!.secrets!["my-token"]!.name).toBe("My Token");         yield* removeSecretFromConfig(path, "my-token");         config = yield* loadConfig(path);        expect(config!.secrets?.["my-token"]).toBeUndefined();      }),    ),  );});