@kirrosh/zond 0.8.0 → 0.9.1

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.8.0",
3
+ "version": "0.9.1",
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",
@@ -26,11 +26,13 @@
26
26
  "scripts": {
27
27
  "zond": "bun run src/cli/index.ts",
28
28
  "test": "bun run test:unit && bun run test:mocked",
29
- "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/cli/args.test.ts tests/cli/chat.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/doctor.test.ts tests/cli/init.test.ts tests/cli/runs.test.ts tests/cli/safe-run.test.ts tests/cli/update.test.ts tests/integration/ tests/web/ tests/mcp/tools.test.ts tests/mcp/save-test-suite.test.ts tests/agent/agent-loop.test.ts tests/agent/context-manager.test.ts tests/agent/system-prompt.test.ts tests/reporter/",
29
+ "test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/cli/args.test.ts tests/cli/chat.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/doctor.test.ts tests/cli/init.test.ts tests/cli/runs.test.ts tests/cli/safe-run.test.ts tests/cli/update.test.ts tests/integration/ tests/web/ tests/mcp/tools.test.ts tests/mcp/save-test-suite.test.ts tests/agent/agent-loop.test.ts tests/agent/context-manager.test.ts tests/agent/system-prompt.test.ts tests/reporter/ tests/version-sync.test.ts",
30
30
  "test:mocked": "bun run scripts/run-mocked-tests.ts",
31
31
  "test:ai": "bun test tests/ai/",
32
32
  "check": "tsc --noEmit --project tsconfig.json",
33
- "build": "bun build --compile src/cli/index.ts --outfile zond"
33
+ "build": "bun build --compile src/cli/index.ts --outfile zond",
34
+ "version:sync": "bun run scripts/sync-version.ts",
35
+ "postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json .claude-plugin/marketplace.json"
34
36
  },
35
37
  "devDependencies": {
36
38
  "@types/bun": "latest"
@@ -44,6 +44,16 @@ export function envCategory(hint: string | undefined): string | null {
44
44
  return null;
45
45
  }
46
46
 
47
+ export function schemaHint(
48
+ failureType: string,
49
+ responseStatus: number | null | undefined,
50
+ ): string | null {
51
+ if (failureType === "assertion_failed" || responseStatus === 400 || responseStatus === 422) {
52
+ return "Use describe_endpoint(specPath, method, path) to verify expected request/response schema";
53
+ }
54
+ return null;
55
+ }
56
+
47
57
  export function computeSharedEnvIssue(
48
58
  failures: Array<{ hint?: string }>,
49
59
  envFilePath?: string,
@@ -72,269 +72,15 @@ export interface GuideOptions {
72
72
  export function buildGenerationGuide(opts: GuideOptions): string {
73
73
  const hasAuth = opts.securitySchemes.length > 0;
74
74
 
75
+ const securitySummary = hasAuth
76
+ ? `Security: ${opts.securitySchemes.map(s => `${s.name} (${s.type}${s.scheme ? `/${s.scheme}` : ""})`).join(", ")}`
77
+ : "Security: none";
78
+
75
79
  return `# Test Generation Guide for ${opts.title}
76
80
  ${opts.coverageHeader ? `\n${opts.coverageHeader}\n` : ""}
77
81
  ## API Specification (${opts.endpointCount} endpoints)
78
82
  ${opts.baseUrl ? `Base URL: ${opts.baseUrl}` : "Base URL: use {{base_url}} environment variable"}
83
+ ${securitySummary}
79
84
 
80
- ${opts.apiContext}
81
-
82
- ${hasAuth ? `---
83
-
84
- ## Environment Setup (Required for Authentication)
85
-
86
- This API uses authentication. Before running tests, set up your credentials:
87
-
88
- ### Edit the env file
89
- After \`setup_api\`, the collection directory contains \`.env.yaml\`. Edit it to add your credentials:
90
- \`\`\`yaml
91
- base_url: "https://api.example.com"
92
- api_key: "your-actual-api-key-here"
93
- auth_token: "your-token-here"
94
- \`\`\`
95
-
96
- ### How it works
97
- - Tests **automatically** load the \`"default"\` environment — no need to pass \`envName\` to \`run_tests\`
98
- - If the env file is in the collection root and tests are in a \`tests/\` subdirectory, the file is still found automatically
99
- - Use \`{{api_key}}\`, \`{{auth_token}}\`, \`{{base_url}}\` etc. in test headers/bodies
100
- - **Never hardcode credentials** in YAML files — always use \`{{variable}}\` references
101
-
102
- ` : ""}---
103
-
104
- ## YAML Test Suite Format Reference
105
-
106
- \`\`\`yaml
107
- name: "Suite Name"
108
- description: "What this suite tests" # optional
109
- tags: [smoke, crud] # optional — used for filtering with --tag
110
- base_url: "{{base_url}}"
111
- headers: # optional suite-level headers
112
- Authorization: "Bearer {{auth_token}}"
113
- Content-Type: "application/json"
114
- config: # optional
115
- timeout: 30000
116
- retries: 0
117
- follow_redirects: true
118
- tests:
119
- - name: "Test step name"
120
- POST: "/path/{{variable}}" # exactly ONE method key: GET, POST, PUT, PATCH, DELETE
121
- json: # request body (object)
122
- field: "value"
123
- query: # query parameters
124
- limit: "10"
125
- headers: # step-level headers (override suite)
126
- X-Custom: "value"
127
- expect:
128
- status: 200 # expected HTTP status: integer OR array [200, 204]
129
- body: # field-level assertions
130
- id: { type: "integer", capture: "item_id" }
131
- name: { equals: "expected" }
132
- email: { contains: "@", type: "string" }
133
- count: { gt: 0, lt: 100 }
134
- items: { exists: true } # exists must be boolean, NEVER string
135
- pattern: { matches: "^[A-Z]+" }
136
- headers:
137
- Content-Type: "application/json"
138
- duration: 5000 # max response time in ms
139
- \`\`\`
140
-
141
- ### Assertion Rules
142
- - \`capture: "var_name"\` — SAVES the value into a variable (use in later steps as {{var_name}})
143
- - \`equals: value\` — exact match COMPARISON (NEVER use equals to save a value!)
144
- - \`type: "string"|"number"|"integer"|"boolean"|"array"|"object"\`
145
- - \`contains: "substring"\` — string substring match
146
- - \`matches: "regex"\` — regex pattern match
147
- - \`gt: N\` / \`lt: N\` — numeric comparison
148
- - \`exists: true|false\` — field presence check (MUST be boolean, not string)
149
-
150
- ### Nested Body Assertions
151
- Both forms are equivalent and supported:
152
-
153
- **Dot-notation (flat):**
154
- \`\`\`yaml
155
- body:
156
- "category.name": { equals: "Dogs" }
157
- "address.city": { type: "string" }
158
- \`\`\`
159
-
160
- **Nested YAML (auto-flattened):**
161
- \`\`\`yaml
162
- body:
163
- category:
164
- name: { equals: "Dogs" }
165
- address:
166
- city: { type: "string" }
167
- \`\`\`
168
-
169
- ### Root Body Assertions (\`_body\`)
170
- Use \`_body\` to assert on the response body itself (not a field inside it):
171
-
172
- \`\`\`yaml
173
- body:
174
- _body: { type: "array" } # check that response body IS an array
175
- _body: { type: "object" } # check that response body IS an object
176
- _body: { exists: true } # check that body is not null/undefined
177
- \`\`\`
178
-
179
- ### Built-in Generators
180
- Use in string values: \`{{$randomInt}}\`, \`{{$uuid}}\`, \`{{$timestamp}}\`, \`{{$randomEmail}}\`, \`{{$randomString}}\`, \`{{$randomName}}\`
181
- These are the ONLY generators — do NOT invent others.
182
-
183
- ### Variable Interpolation
184
- - \`{{variable}}\` in paths, bodies, headers, query params
185
- - Captured values from previous steps are available in subsequent steps
186
- - Environment variables from .env.yaml files: \`{{base_url}}\`, \`{{auth_username}}\`, etc.
187
-
188
- ---
189
-
190
- ## Step-by-Step Generation Algorithm
191
-
192
- ### Step 0: Register the API (REQUIRED FIRST)
193
- **Always call \`setup_api\` before generating any tests.** This registers the collection in the database so WebUI, coverage tracking, and env loading all work.
194
- \`\`\`
195
- setup_api(name: "myapi", specPath: "/path/to/openapi.json", dir: "/path/to/project/apis/myapi")
196
- \`\`\`
197
- If you skip this step, WebUI will show "No API collections registered yet" and env variables won't auto-load.
198
-
199
- ${hasAuth ? `**Then set credentials immediately after setup_api** — edit the \`.env.yaml\` file in the API directory to add your credentials:
200
- \`\`\`yaml
201
- api_key: "<actual-key>"
202
- base_url: "https://..."
203
- \`\`\`
204
- Never put actual key values directly in YAML test files.
205
-
206
- ` : ""}\
207
- ### Step 1: Analyze the API
208
- - Identify authentication method (${hasAuth ? opts.securitySchemes.map(s => `${s.name}: ${s.type}${s.scheme ? `/${s.scheme}` : ""}`).join(", ") : "none detected"})
209
- - Group endpoints by resource (e.g., /users/*, /pets/*, /orders/*)
210
- - Identify CRUD patterns: POST (create) → GET (read) → PUT/PATCH (update) → DELETE
211
- - Note required fields in request bodies
212
-
213
- ### Step 2: Plan Test Suites
214
- Before generating, check coverage with \`coverage_analysis\` to avoid duplicating existing tests. Use \`generate_and_save(testsDir=...)\` for incremental generation of uncovered endpoints only.
215
-
216
- > **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.
217
-
218
- Create separate files for each concern:
219
- ${hasAuth ? `- \`${opts.outputDir}auth.yaml\` — Authentication flow\n` : ""}\
220
- - \`${opts.outputDir}{resource}-crud.yaml\` — CRUD lifecycle per resource
221
- - \`${opts.outputDir}{resource}-validation.yaml\` — Error cases per resource
222
-
223
- ### Step 3: Generate Each Suite
224
-
225
- ${hasAuth ? `**Auth suite** (\`auth.yaml\`):
226
- 1. Login with valid credentials → capture token
227
- 2. Access protected endpoint with token → 200
228
- 3. Login with invalid credentials → 401/403
229
- 4. Access protected endpoint without token → 401
230
-
231
- ` : ""}\
232
- **CRUD lifecycle** (\`{resource}-crud.yaml\`):
233
- 1. Create resource (POST) → 201, **always verify key fields in response body** (at minimum: id, name/title)
234
- 2. Read created resource (GET /resource/{{id}}) → 200, verify fields match what was sent
235
- 3. List resources (GET /resource) → 200, verify \`_body: { type: "array" }\` AND \`_body.length: { gt: 0 }\`
236
- 4. Update resource (PUT/PATCH /resource/{{id}}) → 200
237
- 5. Read updated resource → verify changes applied
238
- 6. Delete resource (DELETE /resource/{{id}}) → 200/204
239
- 7. Verify deleted (GET /resource/{{id}}) → 404
240
- 8. For bulk create endpoints (createWithArray/List): create → then GET each to verify they exist
241
-
242
- **Validation suite** (\`{resource}-validation.yaml\`):
243
- 1. Create with missing required fields → 400/422, verify \`message: { exists: true }\` in error body
244
- 2. Create with invalid field types → 400/422
245
- 3. Get non-existent resource (e.g. id=999999) → 404
246
- 4. Delete non-existent resource → 404
247
- 5. For error responses: always assert error body has meaningful content, not just status code
248
-
249
- ### Step 4: Save, Run, Debug
250
- 1. Use \`save_test_suite\` to save each file — it validates YAML before writing
251
- 2. Use \`run_tests\` to execute — review pass/fail summary
252
- 3. If failures: use \`query_db\` with \`action: "diagnose_failure"\` and the runId to see full request/response details
253
- 4. Fix issues and re-save with \`overwrite: true\`
254
-
255
- ---
256
-
257
- ## Tag Conventions
258
-
259
- Use standard tags to enable safe filtering:
260
-
261
- | Tag | HTTP Methods | Safe for |
262
- |-----|-------------|---------|
263
- | \`smoke\` | GET only | Production (read-only, zero risk) |
264
- | \`crud\` | POST/PUT/PATCH | Staging only (state-changing) |
265
- | \`destructive\` | DELETE | Explicit opt-in, run last |
266
- | \`auth\` | Any (auth flows) | Run first to capture tokens |
267
-
268
- Example: \`zond run --tag smoke --safe\` → reads-only, safe against production.
269
-
270
- ---
271
-
272
- ## Practical Tips
273
-
274
- - **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.
275
- - **Nested assertions**: Use dot-notation or nested YAML — both work identically.
276
- - **Root body type**: Use \`_body: { type: "array" }\` to verify the response body type itself.
277
- - **List endpoints**: Always check both type AND non-emptiness: \`_body: { type: "array" }\` + \`_body.length: { gt: 0 }\`
278
- - **Create responses**: Always verify at least the key identifying fields (id, name) in the response body — don't just check status.
279
- - **Error responses**: Assert that error bodies contain useful info (\`message: { exists: true }\`), not just status codes.
280
- - **Bulk operations**: After bulk create (createWithArray, createWithList), add GET steps to verify resources were actually created.
281
- - **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.
282
- - **Cleanup pattern**: Always delete test data in the same suite. Use a create → read → delete lifecycle so tests are idempotent:
283
- \`\`\`yaml
284
- tests:
285
- - name: Create test resource
286
- POST: /users
287
- json: { name: "zond-test-{{$randomString}}" }
288
- expect:
289
- status: 201
290
- body:
291
- id: { capture: user_id }
292
- - name: Read created resource
293
- GET: /users/{{user_id}}
294
- expect:
295
- status: 200
296
- - name: Cleanup - delete test resource
297
- DELETE: /users/{{user_id}}
298
- expect:
299
- status: 204
300
- \`\`\`
301
- - **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.
302
-
303
- ---
304
-
305
- ## Common Mistakes to Avoid
306
-
307
- 1. **equals vs capture**: \`capture\` SAVES a value, \`equals\` COMPARES. To extract a token: \`{ capture: "token" }\` NOT \`{ equals: "{{token}}" }\`
308
- 2. **exists must be boolean**: \`exists: true\` NOT \`exists: "true"\`
309
- 3. **Status must be integer or array**: \`status: 200\` or \`status: [200, 204]\` NOT \`status: "200"\`
310
- 4. **One method per step**: Each test step has exactly ONE of GET/POST/PUT/PATCH/DELETE
311
- 5. **Don't hardcode base URL**: Use \`{{base_url}}\` — set it in environment or suite base_url
312
- 6. **Auth credentials**: Use environment variables \`{{auth_username}}\`, \`{{auth_password}}\` — NOT generators
313
- 7. **String query params**: Query parameter values must be strings: \`limit: "10"\` not \`limit: 10\`
314
- 8. **Hardcoded credentials**: NEVER put actual API keys/tokens in YAML — use \`{{api_key}}\` from env instead
315
- 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.
316
-
317
- ---
318
-
319
- ## Tools to Use
320
-
321
- | Tool | When |
322
- |------|------|
323
- | \`setup_api\` | Register a new API (creates dirs, reads spec, sets up env) |
324
- | \`generate_and_save\` | Get test generation guide (with auto-chunking for large APIs) |
325
- | \`save_test_suite\` | Save generated YAML (validates before writing) |
326
- | \`save_test_suites\` | Save multiple YAML files in one call |
327
- | \`run_tests\` | Execute saved test suites |
328
- | \`query_db\` | Query runs, collections, results, diagnose failures |
329
- | \`coverage_analysis\` | Find untested endpoints for incremental generation |
330
- | \`describe_endpoint\` | Get full details for one endpoint when debugging |
331
- | \`ci_init\` | Generate CI/CD workflow (GitHub Actions / GitLab CI) to run tests on push |
332
-
333
- ## Workflow After Tests Pass
334
-
335
- After tests are saved and running successfully, ask the user if they want to set up CI/CD:
336
- 1. Use \`ci_init\` to generate a CI workflow (auto-detects platform or use platform param)
337
- 2. Help them commit and push to their repository
338
- 3. Tests will run automatically on push, PR, and on schedule
339
- `;
85
+ ${opts.apiContext}`;
340
86
  }
@@ -4,7 +4,7 @@ import { getDb } from "../../db/schema.ts";
4
4
  import { listCollections, listRuns, getRunById, getResultsByRunId, getCollectionById } from "../../db/queries.ts";
5
5
  import { join } from "node:path";
6
6
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
- import { statusHint, classifyFailure, envHint, envCategory } from "../../core/diagnostics/failure-hints.ts";
7
+ import { statusHint, classifyFailure, envHint, envCategory, schemaHint } from "../../core/diagnostics/failure-hints.ts";
8
8
 
9
9
  function parseBodySafe(raw: string | null | undefined): unknown {
10
10
  if (!raw) return undefined;
@@ -124,6 +124,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
124
124
  // env issues take priority over generic status hints
125
125
  const hint = envHint(r.request_url, r.error_message, envFilePath) ?? statusHint(r.response_status);
126
126
  const failure_type = classifyFailure(r.status, r.response_status);
127
+ const sHint = schemaHint(failure_type, r.response_status);
127
128
  return {
128
129
  suite_name: r.suite_name,
129
130
  test_name: r.test_name,
@@ -134,6 +135,7 @@ export function registerQueryDbTool(server: McpServer, dbPath?: string) {
134
135
  request_url: r.request_url,
135
136
  response_status: r.response_status,
136
137
  ...(hint ? { hint } : {}),
138
+ ...(sHint ? { schema_hint: sHint } : {}),
137
139
  response_body: parseBodySafe(r.response_body),
138
140
  response_headers: r.response_headers
139
141
  ? JSON.parse(r.response_headers)
@@ -48,6 +48,12 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
48
48
  const hints: string[] = [];
49
49
  if (failedSteps.length > 0) {
50
50
  hints.push("Use query_db(action: 'diagnose_failure', runId: " + runId + ") for detailed failure analysis");
51
+ const hasAssertionFailures = failedSteps.some(s => s.assertions.length > 0);
52
+ if (hasAssertionFailures) {
53
+ hints.push(
54
+ "Some tests have assertion failures — use describe_endpoint(specPath, method, path) to verify expected schemas"
55
+ );
56
+ }
51
57
  }
52
58
  hints.push("Use manage_server(action: 'start') to launch the Web UI and view results visually in a browser at http://localhost:8080");
53
59
  hints.push("Ask the user if they want to set up CI/CD to run these tests automatically on push. If yes, use ci_init to generate a workflow and help them push to GitHub/GitLab.");