@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
|
@@ -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
|
|
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(
|
|
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 =
|
|
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
|
-
|
|
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
|
|