@kirrosh/zond 0.9.0 → 0.9.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kirrosh/zond",
3
- "version": "0.9.0",
3
+ "version": "0.9.2",
4
4
  "description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
5
5
  "license": "MIT",
6
6
  "module": "index.ts",
@@ -59,301 +59,91 @@ export function compressEndpointsWithSchemas(
59
59
  return lines.join("\n");
60
60
  }
61
61
 
62
- export interface GuideOptions {
63
- title: string;
64
- baseUrl?: string;
65
- apiContext: string;
66
- outputDir: string;
67
- securitySchemes: SecuritySchemeInfo[];
68
- endpointCount: number;
69
- coverageHeader?: string;
70
- compact?: boolean;
71
- }
72
-
73
- export function buildGenerationGuide(opts: GuideOptions): string {
74
- const hasAuth = opts.securitySchemes.length > 0;
75
-
76
- if (opts.compact) {
77
- const securitySummary = hasAuth
78
- ? `Security: ${opts.securitySchemes.map(s => `${s.name} (${s.type}${s.scheme ? `/${s.scheme}` : ""})`).join(", ")}`
79
- : "Security: none";
80
-
81
- return `# Test Generation Guide for ${opts.title}
82
- ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
83
- ## API Specification (${opts.endpointCount} endpoints)
84
- ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
85
- ${securitySummary}
86
-
87
- ${opts.apiContext}
88
-
89
- ---
90
-
91
- > **Note:** Refer to the full YAML format reference, generation algorithm, tag conventions, and practical tips from the initial \`generate_and_save\` call (without \`tag\`). Only the API endpoints above are unique to this chunk.`;
92
- }
93
-
94
- return `# Test Generation Guide for ${opts.title}
95
- ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
96
- ## API Specification (${opts.endpointCount} endpoints)
97
- ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
98
-
99
- ${opts.apiContext}
100
-
101
- ${hasAuth ? `---
102
-
103
- ## Environment Setup (Required for Authentication)
104
-
105
- This API uses authentication. Before running tests, set up your credentials:
106
-
107
- ### Edit the env file
108
- After \`setup_api\`, the collection directory contains \`.env.yaml\`. Edit it to add your credentials:
109
- \`\`\`yaml
110
- base_url: "https://api.example.com"
111
- api_key: "your-actual-api-key-here"
112
- auth_token: "your-token-here"
113
- \`\`\`
114
-
115
- ### How it works
116
- - Tests **automatically** load the \`"default"\` environment — no need to pass \`envName\` to \`run_tests\`
117
- - If the env file is in the collection root and tests are in a \`tests/\` subdirectory, the file is still found automatically
118
- - Use \`{{api_key}}\`, \`{{auth_token}}\`, \`{{base_url}}\` etc. in test headers/bodies
119
- - **Never hardcode credentials** in YAML files — always use \`{{variable}}\` references
120
-
121
- ` : ""}---
122
-
123
- ## YAML Test Suite Format Reference
62
+ const YAML_FORMAT_CHEATSHEET = `
63
+ ## YAML Test Format Reference
124
64
 
65
+ ### Suite structure
125
66
  \`\`\`yaml
126
- name: "Suite Name"
127
- description: "What this suite tests" # optional
128
- tags: [smoke, crud] # optional used for filtering with --tag
129
- base_url: "{{base_url}}"
130
- headers: # optional suite-level headers
131
- Authorization: "Bearer {{auth_token}}"
132
- Content-Type: "application/json"
133
- config: # optional
134
- timeout: 30000
135
- retries: 0
136
- follow_redirects: true
67
+ name: Suite Name # required
68
+ base_url: "{{base_url}}" # or hardcoded URL
69
+ tags: [smoke] # optional: smoke | crud | destructive | auth
137
70
  tests:
138
- - name: "Test step name"
139
- POST: "/path/{{variable}}" # exactly ONE method key: GET, POST, PUT, PATCH, DELETE
140
- json: # request body (object)
141
- field: "value"
142
- query: # query parameters
143
- limit: "10"
144
- headers: # step-level headers (override suite)
145
- X-Custom: "value"
146
- expect:
147
- status: 200 # expected HTTP status: integer OR array [200, 204]
148
- body: # field-level assertions
149
- id: { type: "integer", capture: "item_id" }
150
- name: { equals: "expected" }
151
- email: { contains: "@", type: "string" }
152
- count: { gt: 0, lt: 100 }
153
- items: { exists: true } # exists must be boolean, NEVER string
154
- pattern: { matches: "^[A-Z]+" }
155
- headers:
156
- Content-Type: "application/json"
157
- duration: 5000 # max response time in ms
158
- \`\`\`
159
-
160
- ### Assertion Rules
161
- - \`capture: "var_name"\` — SAVES the value into a variable (use in later steps as {{var_name}})
162
- - \`equals: value\` — exact match COMPARISON (NEVER use equals to save a value!)
163
- - \`type: "string"|"number"|"integer"|"boolean"|"array"|"object"\`
164
- - \`contains: "substring"\` — string substring match
165
- - \`matches: "regex"\` — regex pattern match
166
- - \`gt: N\` / \`lt: N\` — numeric comparison
167
- - \`exists: true|false\` — field presence check (MUST be boolean, not string)
168
-
169
- ### Nested Body Assertions
170
- Both forms are equivalent and supported:
171
-
172
- **Dot-notation (flat):**
173
- \`\`\`yaml
174
- body:
175
- "category.name": { equals: "Dogs" }
176
- "address.city": { type: "string" }
177
- \`\`\`
178
-
179
- **Nested YAML (auto-flattened):**
180
- \`\`\`yaml
181
- body:
182
- category:
183
- name: { equals: "Dogs" }
184
- address:
185
- city: { type: "string" }
71
+ - GET /endpoint: # method + path as YAML key
72
+ query: { limit: 10 }
73
+ expect:
74
+ status: 200
75
+ _body: { type: array }
186
76
  \`\`\`
187
77
 
188
- ### Root Body Assertions (\`_body\`)
189
- Use \`_body\` to assert on the response body itself (not a field inside it):
190
-
78
+ ### Assertion operators
79
+ | Operator | Example |
80
+ |----------|---------|
81
+ | equals (default) | \`status: 200\` |
82
+ | not_equals | \`status: { not_equals: 500 }\` |
83
+ | contains | \`name: { contains: "john" }\` |
84
+ | not_contains | \`body: { not_contains: "error" }\` |
85
+ | exists / not_exists | \`id: { exists: true }\` |
86
+ | gt / gte / lt / lte | \`count: { gte: 1 }\` |
87
+ | matches (regex) | \`email: { matches: "^.+@.+$" }\` |
88
+ | type | \`items: { type: array }\` |
89
+ | length | \`items: { length: 5 }\` |
90
+ | length_gt/gte/lt/lte | \`items: { length_gt: 0 }\` |
91
+
92
+ ### Body assertions
93
+ - \`_body\` — assert on entire response body: \`_body: { type: array }\`
94
+ - Dot-notation for nested: \`data.user.id: { exists: true }\`
95
+ - Array item access: \`items.0.name: { exists: true }\`
96
+
97
+ ### Request body (JSON)
191
98
  \`\`\`yaml
192
- body:
193
- _body: { type: "array" } # check that response body IS an array
194
- _body: { type: "object" } # check that response body IS an object
195
- _body: { exists: true } # check that body is not null/undefined
99
+ - POST /resource:
100
+ json: { name: "test", email: "a@b.com" }
101
+ expect:
102
+ status: 201
196
103
  \`\`\`
197
104
 
198
- ### Built-in Generators
199
- Use in string values: \`{{$randomInt}}\`, \`{{$uuid}}\`, \`{{$timestamp}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`, \`{{$randomName}}\`
200
- These are the ONLY generators — do NOT invent others.
201
-
202
- ### Variable Interpolation
203
- - \`{{variable}}\` in paths, bodies, headers, query params
204
- - Captured values from previous steps are available in subsequent steps
205
- - Environment variables from .env.yaml files: \`{{base_url}}\`, \`{{auth_username}}\`, etc.
206
-
207
- ---
208
-
209
- ## Step-by-Step Generation Algorithm
210
-
211
- ### Step 0: Register the API (REQUIRED FIRST)
212
- **Always call \`setup_api\` before generating any tests.** This registers the collection in the database so WebUI, coverage tracking, and env loading all work.
213
- \`\`\`
214
- setup_api(name: "myapi", specPath: "/path/to/openapi.json", dir: "/path/to/project/apis/myapi")
215
- \`\`\`
216
- If you skip this step, WebUI will show "No API collections registered yet" and env variables won't auto-load.
105
+ ### Built-in generators
106
+ \`{{$uuid}}\`, \`{{$randomInt}}\`, \`{{$timestamp}}\`, \`{{$isoTimestamp}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`
217
107
 
218
- ${hasAuth ? `**Then set credentials immediately after setup_api** — edit the \`.env.yaml\` file in the API directory to add your credentials:
108
+ ### Variable capture & interpolation
219
109
  \`\`\`yaml
220
- api_key: "<actual-key>"
221
- base_url: "https://..."
222
- \`\`\`
223
- Never put actual key values directly in YAML test files.
224
-
225
- ` : ""}\
226
- ### Step 1: Analyze the API
227
- - Identify authentication method (${hasAuth ? opts.securitySchemes.map(s => `${s.name}: ${s.type}${s.scheme ? `/${s.scheme}` : ""}`).join(", ") : "none detected"})
228
- - Group endpoints by resource (e.g., /users/*, /pets/*, /orders/*)
229
- - Identify CRUD patterns: POST (create) → GET (read) → PUT/PATCH (update) → DELETE
230
- - Note required fields in request bodies
231
-
232
- ### Step 2: Plan Test Suites
233
- Before generating, check coverage with \`coverage_analysis\` to avoid duplicating existing tests. Use \`generate_and_save(testsDir=...)\` for incremental generation of uncovered endpoints only.
234
-
235
- > **Coverage note**: coverage is a static scan of YAML files — an endpoint is "covered" if a test file contains a matching METHOD + path line, regardless of whether tests pass or actually run.
236
-
237
- Create separate files for each concern:
238
- ${hasAuth ? `- \`${opts.outputDir}auth.yaml\` — Authentication flow\n` : ""}\
239
- - \`${opts.outputDir}{resource}-crud.yaml\` — CRUD lifecycle per resource
240
- - \`${opts.outputDir}{resource}-validation.yaml\` — Error cases per resource
241
-
242
- ### Step 3: Generate Each Suite
243
-
244
- ${hasAuth ? `**Auth suite** (\`auth.yaml\`):
245
- 1. Login with valid credentials → capture token
246
- 2. Access protected endpoint with token → 200
247
- 3. Login with invalid credentials → 401/403
248
- 4. Access protected endpoint without token → 401
249
-
250
- ` : ""}\
251
- **CRUD lifecycle** (\`{resource}-crud.yaml\`):
252
- 1. Create resource (POST) → 201, **always verify key fields in response body** (at minimum: id, name/title)
253
- 2. Read created resource (GET /resource/{{id}}) → 200, verify fields match what was sent
254
- 3. List resources (GET /resource) → 200, verify \`_body: { type: "array" }\` AND \`_body.length: { gt: 0 }\`
255
- 4. Update resource (PUT/PATCH /resource/{{id}}) → 200
256
- 5. Read updated resource → verify changes applied
257
- 6. Delete resource (DELETE /resource/{{id}}) → 200/204
258
- 7. Verify deleted (GET /resource/{{id}}) → 404
259
- 8. For bulk create endpoints (createWithArray/List): create → then GET each to verify they exist
260
-
261
- **Validation suite** (\`{resource}-validation.yaml\`):
262
- 1. Create with missing required fields → 400/422, verify \`message: { exists: true }\` in error body
263
- 2. Create with invalid field types → 400/422
264
- 3. Get non-existent resource (e.g. id=999999) → 404
265
- 4. Delete non-existent resource → 404
266
- 5. For error responses: always assert error body has meaningful content, not just status code
267
-
268
- ### Step 4: Save, Run, Debug
269
- 1. Use \`save_test_suite\` to save each file — it validates YAML before writing
270
- 2. Use \`run_tests\` to execute — review pass/fail summary
271
- 3. If failures: use \`query_db\` with \`action: "diagnose_failure"\` and the runId to see full request/response details
272
- 4. Fix issues and re-save with \`overwrite: true\`
273
-
274
- ---
275
-
276
- ## Tag Conventions
277
-
278
- Use standard tags to enable safe filtering:
279
-
280
- | Tag | HTTP Methods | Safe for |
281
- |-----|-------------|---------|
282
- | \`smoke\` | GET only | Production (read-only, zero risk) |
283
- | \`crud\` | POST/PUT/PATCH | Staging only (state-changing) |
284
- | \`destructive\` | DELETE | Explicit opt-in, run last |
285
- | \`auth\` | Any (auth flows) | Run first to capture tokens |
286
-
287
- Example: \`zond run --tag smoke --safe\` → reads-only, safe against production.
288
-
289
- ---
290
-
291
- ## Practical Tips
292
-
293
- - **int64 IDs**: For APIs returning large auto-generated IDs (int64), prefer setting fixed IDs in request bodies rather than capturing auto-generated ones, as JSON number precision may cause mismatches.
294
- - **Nested assertions**: Use dot-notation or nested YAML — both work identically.
295
- - **Root body type**: Use \`_body: { type: "array" }\` to verify the response body type itself.
296
- - **List endpoints**: Always check both type AND non-emptiness: \`_body: { type: "array" }\` + \`_body.length: { gt: 0 }\`
297
- - **Create responses**: Always verify at least the key identifying fields (id, name) in the response body — don't just check status.
298
- - **Error responses**: Assert that error bodies contain useful info (\`message: { exists: true }\`), not just status codes.
299
- - **Bulk operations**: After bulk create (createWithArray, createWithList), add GET steps to verify resources were actually created.
300
- - **204 No Content**: When an endpoint returns 204, omit \`body:\` assertions entirely — an empty response IS the correct behavior. Adding body assertions on 204 will always fail.
301
- - **Cleanup pattern**: Always delete test data in the same suite. Use a create → read → delete lifecycle so tests are idempotent:
302
- \`\`\`yaml
303
- tests:
304
- - name: Create test resource
305
- POST: /users
306
- json: { name: "zond-test-{{$randomString}}" }
110
+ - POST /items:
111
+ json: { name: "test-{{$uuid}}" }
112
+ capture:
113
+ created_id: id # saves response.id
307
114
  expect:
308
115
  status: 201
309
- body:
310
- id: { capture: user_id }
311
- - name: Read created resource
312
- GET: /users/{{user_id}}
116
+ - GET /items/{{created_id}}:
313
117
  expect:
314
118
  status: 200
315
- - name: Cleanup - delete test resource
316
- DELETE: /users/{{user_id}}
317
- expect:
318
- status: 204
319
- \`\`\`
320
- - **Identifiable test data**: Prefix test data with \`zond-test-\` or use \`{{$uuid}}\` / \`zond-test-{{$randomString}}\` so you can identify and clean up leftover test data if needed.
321
-
322
- ---
323
-
324
- ## Common Mistakes to Avoid
119
+ \`\`\`
120
+ `;
325
121
 
326
- 1. **equals vs capture**: \`capture\` SAVES a value, \`equals\` COMPARES. To extract a token: \`{ capture: "token" }\` NOT \`{ equals: "{{token}}" }\`
327
- 2. **exists must be boolean**: \`exists: true\` NOT \`exists: "true"\`
328
- 3. **Status must be integer or array**: \`status: 200\` or \`status: [200, 204]\` NOT \`status: "200"\`
329
- 4. **One method per step**: Each test step has exactly ONE of GET/POST/PUT/PATCH/DELETE
330
- 5. **Don't hardcode base URL**: Use \`{{base_url}}\` — set it in environment or suite base_url
331
- 6. **Auth credentials**: Use environment variables \`{{auth_username}}\`, \`{{auth_password}}\` — NOT generators
332
- 7. **String query params**: Query parameter values must be strings: \`limit: "10"\` not \`limit: 10\`
333
- 8. **Hardcoded credentials**: NEVER put actual API keys/tokens in YAML — use \`{{api_key}}\` from env instead
334
- 9. **Body assertions on 204**: Don't add \`body:\` checks for DELETE or other endpoints that return 204 No Content — the body is empty by design.
122
+ export interface GuideOptions {
123
+ title: string;
124
+ baseUrl?: string;
125
+ apiContext: string;
126
+ outputDir: string;
127
+ securitySchemes: SecuritySchemeInfo[];
128
+ endpointCount: number;
129
+ coverageHeader?: string;
130
+ includeFormat?: boolean;
131
+ }
335
132
 
336
- ---
133
+ export function buildGenerationGuide(opts: GuideOptions): string {
134
+ const hasAuth = opts.securitySchemes.length > 0;
337
135
 
338
- ## Tools to Use
136
+ const securitySummary = hasAuth
137
+ ? `Security: ${opts.securitySchemes.map(s => `${s.name} (${s.type}${s.scheme ? `/${s.scheme}` : ""})`).join(", ")}`
138
+ : "Security: none";
339
139
 
340
- | Tool | When |
341
- |------|------|
342
- | \`setup_api\` | Register a new API (creates dirs, reads spec, sets up env) |
343
- | \`generate_and_save\` | Get test generation guide (with auto-chunking for large APIs) |
344
- | \`save_test_suite\` | Save generated YAML (validates before writing) |
345
- | \`save_test_suites\` | Save multiple YAML files in one call |
346
- | \`run_tests\` | Execute saved test suites |
347
- | \`query_db\` | Query runs, collections, results, diagnose failures |
348
- | \`coverage_analysis\` | Find untested endpoints for incremental generation |
349
- | \`describe_endpoint\` | Get full details for one endpoint when debugging |
350
- | \`ci_init\` | Generate CI/CD workflow (GitHub Actions / GitLab CI) to run tests on push |
140
+ const formatSection = opts.includeFormat !== false ? YAML_FORMAT_CHEATSHEET : "";
351
141
 
352
- ## Workflow After Tests Pass
142
+ return `# Test Generation Guide for ${opts.title}
143
+ ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
144
+ ## API Specification (${opts.endpointCount} endpoints)
145
+ ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
146
+ ${securitySummary}
353
147
 
354
- After tests are saved and running successfully, ask the user if they want to set up CI/CD:
355
- 1. Use \`ci_init\` to generate a CI workflow (auto-detects platform or use platform param)
356
- 2. Help them commit and push to their repository
357
- 3. Tests will run automatically on push, PR, and on schedule
358
- `;
148
+ ${opts.apiContext}${formatSection}`;
359
149
  }
@@ -56,7 +56,8 @@ export const TOOL_DESCRIPTIONS = {
56
56
  "Read an OpenAPI spec, auto-chunk by tags if large (>30 endpoints), " +
57
57
  "and return a focused test generation guide. For large APIs returns a chunking plan — " +
58
58
  "call again with tag parameter for each chunk. Use testsDir param to only generate for uncovered endpoints. " +
59
- "After generating YAML, use save_test_suites to save files, then run_tests to verify.",
59
+ "After generating YAML, use save_test_suites to save files, then run_tests to verify. " +
60
+ "Includes YAML format cheatsheet by default; pass includeFormat: false for subsequent tag chunks to save tokens.",
60
61
 
61
62
  ci_init:
62
63
  "Generate a CI/CD workflow file for running API tests automatically on push, PR, and schedule. " +
@@ -21,8 +21,9 @@ export function registerGenerateAndSaveTool(server: McpServer) {
21
21
  methodFilter: z.optional(z.array(z.string())).describe("Only include endpoints with these HTTP methods (e.g. [\"GET\"] for smoke tests)"),
22
22
  testsDir: z.optional(z.string()).describe("Path to existing tests directory — filters to uncovered endpoints only"),
23
23
  overwrite: z.optional(z.boolean()).describe("Hint for save_test_suites overwrite behavior (default: false)"),
24
+ includeFormat: z.optional(z.boolean()).describe("Include YAML format reference (default: true, set false for subsequent tag chunks)"),
24
25
  },
25
- }, async ({ specPath, outputDir, tag, methodFilter, testsDir, overwrite }) => {
26
+ }, async ({ specPath, outputDir, tag, methodFilter, testsDir, overwrite, includeFormat }) => {
26
27
  try {
27
28
  const doc = await readOpenApiSpec(specPath);
28
29
  let endpoints = extractEndpoints(doc);
@@ -82,6 +83,7 @@ export function registerGenerateAndSaveTool(server: McpServer) {
82
83
  instruction:
83
84
  `This API has ${plan.totalEndpoints} endpoints across ${plan.chunks.length} tags. ` +
84
85
  `Call generate_and_save with tag parameter for each chunk sequentially. ` +
86
+ `Pass includeFormat: false for subsequent chunks to save tokens. ` +
85
87
  `Example: generate_and_save(specPath: '${specPath}', tag: '${plan.chunks[0].tag}')`,
86
88
  };
87
89
  if (coverageInfo) {
@@ -106,7 +108,7 @@ export function registerGenerateAndSaveTool(server: McpServer) {
106
108
  securitySchemes,
107
109
  endpointCount: endpoints.length,
108
110
  coverageHeader,
109
- compact: !!tag,
111
+ includeFormat: includeFormat ?? true,
110
112
  });
111
113
 
112
114
  const saveInstructions = `