Unified · Split
| @@ -3,43 +3,58 @@ import { Effect, Option } from "effect"; · 2 unmodified lines | |||
| 3 | import { readFileSync } from "node:fs"; | 3 | import { readFileSync } from "node:fs"; |
| 4 | import { resolve } from "node:path"; | 4 | import { resolve } from "node:path"; |
| 5 | 5 | ||
| 6 | import type { ParsedDocument } from "./parse"; | ||
| 6 | import { parse } from "./parse"; | 7 | import { parse } from "./parse"; |
| 7 | import { extract } from "./extract"; | 8 | import { extract } from "./extract"; |
| 9 | import type { ExtractionResult } from "./types"; | ||
| 8 | import { createExecutor, makeTestConfig } from "@executor/sdk"; | 10 | import { createExecutor, makeTestConfig } from "@executor/sdk"; |
| 9 | import { openApiPlugin } from "./plugin"; | 11 | import { openApiPlugin } from "./plugin"; |
| 10 | 12 | ||
| 11 | // --------------------------------------------------------------------------- | 13 | // --------------------------------------------------------------------------- |
| 12 | // Load the Cloudflare OpenAPI spec (~9MB, 1729 paths) | 14 | // Load + parse once, share across tests |
| 13 | // --------------------------------------------------------------------------- | 15 | // --------------------------------------------------------------------------- |
| 14 | 16 | ||
| 15 | const specPath = resolve(__dirname, "../fixtures/cloudflare.json"); | 17 | const specPath = resolve(__dirname, "../fixtures/cloudflare.json"); |
| 16 | const specText = readFileSync(specPath, "utf-8"); | 18 | const specText = readFileSync(specPath, "utf-8"); |
| 17 | 19 | ||
| 20 | let cachedDoc: ParsedDocument | null = null; | ||
| 21 | let cachedResult: ExtractionResult | null = null; | ||
| 22 | |||
| 23 | const getDoc = () => | ||
| 24 | Effect.gen(function* () { | ||
| 25 | if (!cachedDoc) cachedDoc = yield* parse(specText); | ||
| 26 | return cachedDoc; | ||
| 27 | }); | ||
| 28 | |||
| 29 | const getResult = () => | ||
| 30 | Effect.gen(function* () { | ||
| 31 | if (!cachedResult) { | ||
| 32 | const doc = yield* getDoc(); | ||
| 33 | cachedResult = yield* extract(doc); | ||
| 34 | } | ||
| 35 | return cachedResult; | ||
| 36 | }); | ||
| 37 | |||
| 18 | describe("Real specs: Cloudflare API", () => { | 38 | describe("Real specs: Cloudflare API", () => { |
| 19 | it.effect("parses the full Cloudflare spec", () => | 39 | it.effect("parses the full Cloudflare spec", () => |
| 20 | Effect.gen(function* () { | 40 | Effect.gen(function* () { |
| 21 | const doc = yield* parse(specText); | 41 | const doc = yield* getDoc(); |
| 22 | expect(doc).toBeDefined(); | 42 | expect(doc).toBeDefined(); |
| 23 | }), | 43 | }), |
| 24 | ); | 44 | ); |
| 25 | 45 | ||
| 26 | it.effect("extracts operations from the Cloudflare spec", () => | 46 | it.effect("extracts operations from the Cloudflare spec", () => |
| 27 | Effect.gen(function* () { | 47 | Effect.gen(function* () { |
| 28 | const doc = yield* parse(specText); | 48 | const result = yield* getResult(); |
| 29 | const result = yield* extract(doc); | ||
| 30 | 49 | ||
| 31 | expect(Option.getOrElse(result.title, () => "")).toBe("Cloudflare API"); | 50 | expect(Option.getOrElse(result.title, () => "")).toBe("Cloudflare API"); |
| 32 | expect(Option.getOrElse(result.version, () => "")).toBe("4.0.0"); | 51 | expect(Option.getOrElse(result.version, () => "")).toBe("4.0.0"); |
| 33 | |||
| 34 | // Should extract thousands of operations | ||
| 35 | expect(result.operations.length).toBeGreaterThan(1000); | 52 | expect(result.operations.length).toBeGreaterThan(1000); |
| 36 | 53 | ||
| 37 | // Every operation has an operationId | ||
| 38 | for (const op of result.operations) { | 54 | for (const op of result.operations) { |
| 39 | expect(op.operationId).toBeTruthy(); | 55 | expect(op.operationId).toBeTruthy(); |
| 40 | } | 56 | } |
| 41 | 57 | ||
| 42 | // Every operation has a valid HTTP method | ||
| 43 | const validMethods = new Set([ | 58 | const validMethods = new Set([ |
| 44 | "get", "post", "put", "delete", "patch", "head", "options", "trace", | 59 | "get", "post", "put", "delete", "patch", "head", "options", "trace", |
| 45 | ]); | 60 | ]); |
| @@ -47,13 +62,11 @@ describe("Real specs: Cloudflare API", () => { · 1 unmodified line | |||
| 47 | expect(validMethods.has(op.method)).toBe(true); | 62 | expect(validMethods.has(op.method)).toBe(true); |
| 48 | } | 63 | } |
| 49 | 64 | ||
| 50 | // Spot-check: there should be zone-related operations | ||
| 51 | const zoneOps = result.operations.filter((op) => | 65 | const zoneOps = result.operations.filter((op) => |
| 52 | op.pathTemplate.includes("/zones"), | 66 | op.pathTemplate.includes("/zones"), |
| 53 | ); | 67 | ); |
| 54 | expect(zoneOps.length).toBeGreaterThan(0); | 68 | expect(zoneOps.length).toBeGreaterThan(0); |
| 55 | 69 | ||
| 56 | // Spot-check: there should be DNS record operations | ||
| 57 | const dnsOps = result.operations.filter((op) => | 70 | const dnsOps = result.operations.filter((op) => |
| 58 | op.pathTemplate.includes("/dns_records"), | 71 | op.pathTemplate.includes("/dns_records"), |
| 59 | ); | 72 | ); |
| @@ -63,10 +76,8 @@ describe("Real specs: Cloudflare API", () => { · 3 unmodified lines | |||
| 63 | 76 | ||
| 64 | it.effect("operations have input schemas", () => | 77 | it.effect("operations have input schemas", () => |
| 65 | Effect.gen(function* () { | 78 | Effect.gen(function* () { |
| 66 | const doc = yield* parse(specText); | 79 | const result = yield* getResult(); |
| 67 | const result = yield* extract(doc); | ||
| 68 | 80 | ||
| 69 | // Most operations with parameters or request bodies should have input schemas | ||
| 70 | const opsWithInput = result.operations.filter((op) => | 81 | const opsWithInput = result.operations.filter((op) => |
| 71 | Option.isSome(op.inputSchema), | 82 | Option.isSome(op.inputSchema), |
| 72 | ); | 83 | ); |
| @@ -76,15 +87,12 @@ describe("Real specs: Cloudflare API", () => { · 3 unmodified lines | |||
| 76 | 87 | ||
| 77 | it.effect("operations have output schemas", () => | 88 | it.effect("operations have output schemas", () => |
| 78 | Effect.gen(function* () { | 89 | Effect.gen(function* () { |
| 79 | const doc = yield* parse(specText); | 90 | const result = yield* getResult(); |
| 80 | const result = yield* extract(doc); | ||
| 81 | 91 | ||
| 82 | // Most GET operations should have output schemas | ||
| 83 | const getOps = result.operations.filter((op) => op.method === "get"); | 92 | const getOps = result.operations.filter((op) => op.method === "get"); |
| 84 | const getOpsWithOutput = getOps.filter((op) => | 93 | const getOpsWithOutput = getOps.filter((op) => |
| 85 | Option.isSome(op.outputSchema), | 94 | Option.isSome(op.outputSchema), |
| 86 | ); | 95 | ); |
| 87 | // At least half of GETs should have output schemas | ||
| 88 | expect(getOpsWithOutput.length).toBeGreaterThan(getOps.length / 2); | 96 | expect(getOpsWithOutput.length).toBeGreaterThan(getOps.length / 2); |
| 89 | }), | 97 | }), |
| 90 | ); | 98 | ); |
| @@ -107,13 +115,11 @@ describe("Real specs: Cloudflare API", () => { · 16 unmodified lines | |||
| 107 | const tools = yield* executor.tools.list(); | 115 | const tools = yield* executor.tools.list(); |
| 108 | expect(tools.length).toBe(result.toolCount); | 116 | expect(tools.length).toBe(result.toolCount); |
| 109 | 117 | ||
| 110 | // All tools should be tagged | ||
| 111 | for (const tool of tools) { | 118 | for (const tool of tools) { |
| 112 | expect(tool.tags).toContain("openapi"); | 119 | expect(tool.tags).toContain("openapi"); |
| 113 | expect(tool.tags).toContain("cloudflare"); | 120 | expect(tool.tags).toContain("cloudflare"); |
| 114 | } | 121 | } |
| 115 | 122 | ||
| 116 | // Spot-check: can find zone list tool | ||
| 117 | const zoneTools = yield* executor.tools.list({ query: "zone" }); | 123 | const zoneTools = yield* executor.tools.list({ query: "zone" }); |
| 118 | expect(zoneTools.length).toBeGreaterThan(0); | 124 | expect(zoneTools.length).toBeGreaterThan(0); |
| 119 | }), | 125 | }), |
| @@ -132,27 +138,23 @@ describe("Real specs: Cloudflare API", () => { · 12 unmodified lines | |||
| 132 | namespace: "cloudflare", | 138 | namespace: "cloudflare", |
| 133 | }); | 139 | }); |
| 134 | 140 | ||
| 135 | // Shared definitions store contains all component schemas | ||
| 136 | const definitions = yield* executor.tools.definitions(); | 141 | const definitions = yield* executor.tools.definitions(); |
| 137 | expect(Object.keys(definitions).length).toBeGreaterThan(5000); | 142 | expect(Object.keys(definitions).length).toBeGreaterThan(5000); |
| 138 | expect(definitions["dns-records_dns_response_single"]).toBeDefined(); | 143 | expect(definitions["dns-records_dns_response_single"]).toBeDefined(); |
| 139 | 144 | ||
| 140 | // Tool schemas use $ref pointers with re-attached definitions | ||
| 141 | const tools = yield* executor.tools.list({ query: "dns-record-details" }); | 145 | const tools = yield* executor.tools.list({ query: "dns-record-details" }); |
| 142 | expect(tools.length).toBeGreaterThan(0); | 146 | expect(tools.length).toBeGreaterThan(0); |
| 143 | 147 | ||
| 144 | const schema = yield* executor.tools.schema(tools[0]!.id); | 148 | const schema = yield* executor.tools.schema(tools[0]!.id); |
| 145 | const output = schema.outputSchema as Record<string, unknown>; | 149 | const output = schema.outputSchema as Record<string, unknown>; |
| 146 | 150 | ||
| 147 | // Root is a $ref pointer | ||
| 148 | expect(output["$ref"]).toBeTypeOf("string"); | 151 | expect(output["$ref"]).toBeTypeOf("string"); |
| 149 | expect((output["$ref"] as string).startsWith("#/components/schemas/")).toBe(true); | 152 | expect((output["$ref"] as string).startsWith("#/components/schemas/")).toBe(true); |
| 150 | 153 | ||
| 151 | // Referenced definitions are re-attached for self-contained use | ||
| 152 | expect(output["$defs"]).toBeDefined(); | 154 | expect(output["$defs"]).toBeDefined(); |
| 153 | const defs = output["$defs"] as Record<string, unknown>; | 155 | const defs = output["$defs"] as Record<string, unknown>; |
| 154 | expect(Object.keys(defs).length).toBeGreaterThan(0); | 156 | expect(Object.keys(defs).length).toBeGreaterThan(0); |
| 155 | expect(Object.keys(defs).length).toBeLessThan(100); // Only referenced ones, not all 5611 | 157 | expect(Object.keys(defs).length).toBeLessThan(100); |
| 156 | }), | 158 | }), |
| 157 | ); | 159 | ); |
| 158 | 160 | ||