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