History

perf: cache parsed doc in real-specs tests (15s → 9s)

Rhys Sullivan committed 7 months ago

Commit a2e062d1 parent af627af1 file · +29 −27Browse files

Unified · Split

packages/plugins/openapi/src/real-specs.test.ts+29−27
@@ -3,43 +3,58 @@ import { Effect, Option } from "effect"; · 2 unmodified lines
3import { readFileSync } from "node:fs";3import { readFileSync } from "node:fs";
4import { resolve } from "node:path";4import { resolve } from "node:path";
5 5
6import type { ParsedDocument } from "./parse";
6import { parse } from "./parse";7import { parse } from "./parse";
7import { extract } from "./extract";8import { extract } from "./extract";
9import type { ExtractionResult } from "./types";
8import { createExecutor, makeTestConfig } from "@executor/sdk";10import { createExecutor, makeTestConfig } from "@executor/sdk";
9import { openApiPlugin } from "./plugin";11import { 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
15const specPath = resolve(__dirname, "../fixtures/cloudflare.json");17const specPath = resolve(__dirname, "../fixtures/cloudflare.json");
16const specText = readFileSync(specPath, "utf-8");18const specText = readFileSync(specPath, "utf-8");
17 19
20let cachedDoc: ParsedDocument | null = null;
21let cachedResult: ExtractionResult | null = null;
22
23const getDoc = () =>
24 Effect.gen(function* () {
25 if (!cachedDoc) cachedDoc = yield* parse(specText);
26 return cachedDoc;
27 });
28
29const getResult = () =>
30 Effect.gen(function* () {
31 if (!cachedResult) {
32 const doc = yield* getDoc();
33 cachedResult = yield* extract(doc);
34 }
35 return cachedResult;
36 });
37
18describe("Real specs: Cloudflare API", () => {38describe("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 5611157 expect(Object.keys(defs).length).toBeLessThan(100);
156 }),158 }),
157 );159 );
158 160