@kirrosh/zond 0.12.0 → 0.12.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.12.0",
3
+ "version": "0.12.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",
@@ -117,6 +117,14 @@ function getCaptureField(ep: EndpointInfo): string {
117
117
  return "id";
118
118
  }
119
119
 
120
+ const AUTH_PATH_PATTERNS = /\/(auth|login|signin|signup|register|token|oauth)\b/i;
121
+
122
+ function isAuthEndpoint(ep: EndpointInfo): boolean {
123
+ if (AUTH_PATH_PATTERNS.test(ep.path)) return true;
124
+ if (ep.tags?.some(t => /^auth/i.test(t))) return true;
125
+ return false;
126
+ }
127
+
120
128
  // ──────────────────────────────────────────────
121
129
  // Public API
122
130
  // ──────────────────────────────────────────────
@@ -292,9 +300,10 @@ export function generateCrudSuite(
292
300
  return suite;
293
301
  }
294
302
 
295
- /** Find unresolved template variables in a suite (excluding known globals and captured vars) */
296
- export function findUnresolvedVars(suite: RawSuite): string[] {
303
+ /** Find unresolved template variables in a suite (excluding known globals, captured vars, and env keys) */
304
+ export function findUnresolvedVars(suite: RawSuite, envKeys?: Set<string>): string[] {
297
305
  const KNOWN = new Set(["base_url", "auth_token", "api_key"]);
306
+ if (envKeys) for (const k of envKeys) KNOWN.add(k);
298
307
  const captured = new Set<string>();
299
308
  for (const step of suite.tests) {
300
309
  if (step.expect?.body) {
@@ -327,8 +336,12 @@ export function generateSuites(opts: {
327
336
  // Filter deprecated
328
337
  const active = endpoints.filter(ep => !ep.deprecated);
329
338
 
339
+ // Separate auth endpoints
340
+ const authEndpoints = active.filter(isAuthEndpoint);
341
+ const nonAuth = active.filter(ep => !isAuthEndpoint(ep));
342
+
330
343
  // 1. Detect CRUD groups
331
- const crudGroups = detectCrudGroups(active);
344
+ const crudGroups = detectCrudGroups(nonAuth);
332
345
 
333
346
  // Collect endpoints consumed by CRUD groups
334
347
  const crudEndpointKeys = new Set<string>();
@@ -340,8 +353,8 @@ export function generateSuites(opts: {
340
353
  if (g.delete) crudEndpointKeys.add(`${g.delete.method.toUpperCase()} ${g.delete.path}`);
341
354
  }
342
355
 
343
- // Remaining endpoints (not in any CRUD group)
344
- const remaining = active.filter(ep => !crudEndpointKeys.has(`${ep.method.toUpperCase()} ${ep.path}`));
356
+ // Remaining endpoints (not in any CRUD group, not auth)
357
+ const remaining = nonAuth.filter(ep => !crudEndpointKeys.has(`${ep.method.toUpperCase()} ${ep.path}`));
345
358
 
346
359
  const suites: RawSuite[] = [];
347
360
 
@@ -409,5 +422,30 @@ export function generateSuites(opts: {
409
422
  suites.push(generateCrudSuite(group, securitySchemes));
410
423
  }
411
424
 
425
+ // 4. Auth suite (separate — requires real credentials)
426
+ if (authEndpoints.length > 0) {
427
+ const tests = authEndpoints.map(ep => generateStep(ep, securitySchemes));
428
+ const headers = getSuiteHeaders(authEndpoints, securitySchemes);
429
+
430
+ const suite: RawSuite = {
431
+ name: "auth",
432
+ tags: ["auth"],
433
+ fileStem: "auth",
434
+ base_url: "{{base_url}}",
435
+ tests,
436
+ };
437
+
438
+ if (headers) {
439
+ suite.headers = headers;
440
+ for (const t of tests) {
441
+ if (t.headers && JSON.stringify(t.headers) === JSON.stringify(headers)) {
442
+ delete (t as any).headers;
443
+ }
444
+ }
445
+ }
446
+
447
+ suites.push(suite);
448
+ }
449
+
412
450
  return suites;
413
451
  }
@@ -11,7 +11,9 @@ import {
11
11
  generateSuites,
12
12
  findUnresolvedVars,
13
13
  } from "../../core/generator/index.ts";
14
+ import { loadEnvironment } from "../../core/parser/variables.ts";
14
15
  import { compressEndpointsWithSchemas, buildGenerationGuide } from "../../core/generator/guide-builder.ts";
16
+ import { findCollectionBySpec } from "../../db/queries.ts";
15
17
  import { planChunks, filterByTag } from "../../core/generator/chunker.ts";
16
18
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
17
19
  import { validateAndSave } from "./save-test-suite.ts";
@@ -38,7 +40,11 @@ export function registerGenerateAndSaveTool(server: McpServer) {
38
40
  const securitySchemes = extractSecuritySchemes(doc);
39
41
  const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
40
42
  const title = (doc as any).info?.title as string | undefined;
41
- const effectiveOutputDir = outputDir ?? "./tests/";
43
+ let effectiveOutputDir = outputDir;
44
+ if (!effectiveOutputDir) {
45
+ const collection = findCollectionBySpec(specPath);
46
+ effectiveOutputDir = collection?.test_path ?? "./tests/";
47
+ }
42
48
  const effectiveMode = mode ?? "generate";
43
49
 
44
50
  // Apply method filter
@@ -132,8 +138,10 @@ export function registerGenerateAndSaveTool(server: McpServer) {
132
138
  }
133
139
 
134
140
  const warnings: string[] = [];
141
+ const env = await loadEnvironment(undefined, effectiveOutputDir);
142
+ const envKeys = new Set(Object.keys(env));
135
143
  for (const suite of suites) {
136
- const unresolved = findUnresolvedVars(suite);
144
+ const unresolved = findUnresolvedVars(suite, envKeys);
137
145
  if (unresolved.length > 0)
138
146
  warnings.push(`${suite.fileStem ?? suite.name}.yaml: unresolved [${unresolved.join(", ")}]`);
139
147
  }
@@ -1,8 +1,10 @@
1
1
  import { z } from "zod";
2
+ import { resolve } from "node:path";
2
3
  import type { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3
4
  import { executeRun } from "../../core/runner/execute-run.ts";
4
5
  import { getDb } from "../../db/schema.ts";
5
- import { getResultsByRunId } from "../../db/queries.ts";
6
+ import { getResultsByRunId, findCollectionByTestPath } from "../../db/queries.ts";
7
+ import { readOpenApiSpec, extractEndpoints, scanCoveredEndpoints, filterUncoveredEndpoints } from "../../core/generator/index.ts";
6
8
  import { TOOL_DESCRIPTIONS } from "../descriptions.js";
7
9
 
8
10
  export function registerRunTestsTool(server: McpServer, dbPath?: string) {
@@ -64,6 +66,25 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
64
66
  }))
65
67
  );
66
68
 
69
+ // Best-effort coverage calculation
70
+ let coverage: { covered: number; total: number; percentage: number } | undefined;
71
+ try {
72
+ const resolvedPath = resolve(testPath);
73
+ const collection = findCollectionByTestPath(resolvedPath);
74
+ if (collection?.openapi_spec) {
75
+ const doc = await readOpenApiSpec(collection.openapi_spec);
76
+ const allEndpoints = extractEndpoints(doc);
77
+ const coveredEps = await scanCoveredEndpoints(collection.test_path);
78
+ const uncovered = filterUncoveredEndpoints(allEndpoints, coveredEps);
79
+ const coveredCount = allEndpoints.length - uncovered.length;
80
+ coverage = {
81
+ covered: coveredCount,
82
+ total: allEndpoints.length,
83
+ percentage: allEndpoints.length > 0 ? Math.round((coveredCount / allEndpoints.length) * 100) : 100,
84
+ };
85
+ }
86
+ } catch { /* coverage is best-effort, don't fail run */ }
87
+
67
88
  const hints: string[] = [];
68
89
  if (failedSteps.length > 0) {
69
90
  hints.push("Use query_db(action: 'diagnose_failure', runId: " + runId + ") for detailed failure analysis");
@@ -83,6 +104,7 @@ export function registerRunTestsTool(server: McpServer, dbPath?: string) {
83
104
  suites: results.length,
84
105
  status: failed === 0 ? "all_passed" : "has_failures",
85
106
  ...(failedSteps.length > 0 ? { failures: failedSteps } : {}),
107
+ ...(coverage ? { coverage } : {}),
86
108
  hints,
87
109
  };
88
110