302 lines · 8.3 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 {  addSourceToConfig,  removeSourceFromConfig,  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 = { sources: [] };    const result = decodeExecutorFileConfig(raw);    expect(result.sources).toEqual([]);  });   it("decodes a full config with all source types", () => {    const raw = {      name: "test",      sources: [        {          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.sources).toHaveLength(4);    expect(result.name).toBe("test");    expect(result.secrets!["my-token"]!.name).toBe("My Token");  });   it("rejects invalid source kind", () => {    const raw = {      sources: [{ 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",  "sources": [    {      "kind": "openapi",      "spec": "https://example.com/spec.json"    }  ]}`,        );         const result = yield* loadConfig(path);        expect(result).not.toBeNull();        expect(result!.name).toBe("test");        expect(result!.sources).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("addSourceToConfig creates file and adds source", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const fs = yield* FileSystem.FileSystem;        const path = join(dir, "executor.jsonc");         yield* addSourceToConfig(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!.sources).toHaveLength(1);        expect(config!.sources![0]!.kind).toBe("openapi");      }),    ),  );   it.effect("addSourceToConfig appends to existing sources", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* addSourceToConfig(path, {          kind: "openapi",          spec: "https://example.com/spec.json",          namespace: "first",        });         yield* addSourceToConfig(path, {          kind: "graphql",          endpoint: "https://example.com/graphql",          namespace: "second",        });         const config = yield* loadConfig(path);        expect(config!.sources).toHaveLength(2);        expect(config!.sources![0]!.kind).toBe("openapi");        expect(config!.sources![1]!.kind).toBe("graphql");      }),    ),  );   it.effect("addSourceToConfig 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",  "sources": [    {      "kind": "openapi",      "spec": "https://example.com/spec.json",      "namespace": "first"    }  ]}`,        );         yield* addSourceToConfig(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("addSourceToConfig replaces existing source with same namespace", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* addSourceToConfig(path, {          kind: "openapi",          spec: "https://example.com/v1.json",          namespace: "example",        });         // Add again with same namespace but different spec        yield* addSourceToConfig(path, {          kind: "openapi",          spec: "https://example.com/v2.json",          namespace: "example",        });         const config = yield* loadConfig(path);        // Should have 1, not 2        expect(config!.sources).toHaveLength(1);        expect((config!.sources![0] as { spec: string }).spec).toBe("https://example.com/v2.json");      }),    ),  );   it.effect("removeSourceFromConfig removes by namespace", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* writeConfig(path, {          sources: [            {              kind: "openapi",              spec: "https://example.com/spec.json",              namespace: "keep",            },            {              kind: "graphql",              endpoint: "https://example.com/graphql",              namespace: "remove-me",            },          ],        });         yield* removeSourceFromConfig(path, "remove-me");         const config = yield* loadConfig(path);        expect(config!.sources).toHaveLength(1);        expect(config!.sources![0]!.kind).toBe("openapi");      }),    ),  );   it.effect("addSecretToConfig and removeSecretFromConfig", () =>    withTmpDir((dir) =>      Effect.gen(function* () {        const path = join(dir, "executor.jsonc");         yield* writeConfig(path, { sources: [] });         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();      }),    ),  );});