@kirrosh/zond 0.16.0 → 0.18.0
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/CHANGELOG.md +132 -112
- package/README.md +3 -10
- package/package.json +2 -3
- package/src/cli/commands/export.ts +144 -0
- package/src/cli/commands/generate.ts +32 -0
- package/src/cli/commands/run.ts +22 -5
- package/src/cli/commands/sync.ts +241 -0
- package/src/cli/commands/update.ts +174 -0
- package/src/cli/index.ts +67 -10
- package/src/core/diagnostics/db-analysis.ts +79 -7
- package/src/core/diagnostics/failure-hints.ts +39 -0
- package/src/core/exporter/postman.ts +963 -0
- package/src/core/generator/data-factory.ts +47 -9
- package/src/core/generator/index.ts +1 -1
- package/src/core/generator/openapi-reader.ts +6 -0
- package/src/core/generator/serializer.ts +17 -2
- package/src/core/generator/suite-generator.ts +163 -14
- package/src/core/generator/types.ts +1 -0
- package/src/core/meta/meta-store.ts +78 -0
- package/src/core/meta/types.ts +21 -0
- package/src/core/parser/schema.ts +12 -2
- package/src/core/parser/types.ts +12 -1
- package/src/core/parser/variables.ts +3 -0
- package/src/core/parser/yaml-parser.ts +2 -1
- package/src/core/runner/assertions.ts +44 -20
- package/src/core/runner/execute-run.ts +31 -8
- package/src/core/runner/executor.ts +34 -8
- package/src/core/runner/http-client.ts +1 -1
- package/src/core/runner/types.ts +1 -0
- package/src/core/sync/spec-differ.ts +38 -0
- package/src/web/server.ts +1 -1
- package/src/cli/commands/mcp.ts +0 -16
- package/src/mcp/descriptions.ts +0 -47
- package/src/mcp/server.ts +0 -38
- package/src/mcp/tools/ci-init.ts +0 -54
- package/src/mcp/tools/coverage-analysis.ts +0 -141
- package/src/mcp/tools/describe-endpoint.ts +0 -27
- package/src/mcp/tools/manage-server.ts +0 -86
- package/src/mcp/tools/query-db.ts +0 -84
- package/src/mcp/tools/run-tests.ts +0 -116
- package/src/mcp/tools/send-request.ts +0 -51
- package/src/mcp/tools/setup-api.ts +0 -88
- /package/src/web/static/{htmx.min.js → htmx.min.cjs} +0 -0
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { getDb } from "../../db/schema.ts";
|
|
2
2
|
import { listCollections, listRuns, getRunById, getResultsByRunId, getCollectionById } from "../../db/queries.ts";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
-
import { statusHint, classifyFailure, envHint, envCategory, schemaHint, computeSharedEnvIssue } from "./failure-hints.ts";
|
|
4
|
+
import { statusHint, classifyFailure, envHint, envCategory, schemaHint, computeSharedEnvIssue, recommendedAction, softDeleteHint, type RecommendedAction } from "./failure-hints.ts";
|
|
5
|
+
import { AUTH_PATH_RE } from "../runner/execute-run.ts";
|
|
5
6
|
|
|
6
7
|
export function truncateErrorMessage(raw: string | null | undefined, verbose?: boolean): string | undefined {
|
|
7
8
|
if (!raw) return undefined;
|
|
@@ -116,11 +117,18 @@ export interface FailureGroup {
|
|
|
116
117
|
pattern: string;
|
|
117
118
|
count: number;
|
|
118
119
|
failure_type: string;
|
|
120
|
+
recommended_action: RecommendedAction;
|
|
119
121
|
hint?: string;
|
|
120
122
|
examples: string[];
|
|
121
123
|
response_status: number | null;
|
|
122
124
|
}
|
|
123
125
|
|
|
126
|
+
export interface CascadeSkipGroup {
|
|
127
|
+
capture_var: string;
|
|
128
|
+
count: number;
|
|
129
|
+
examples: string[];
|
|
130
|
+
}
|
|
131
|
+
|
|
124
132
|
export interface DiagnoseResult {
|
|
125
133
|
run: {
|
|
126
134
|
id: number;
|
|
@@ -136,13 +144,17 @@ export interface DiagnoseResult {
|
|
|
136
144
|
assertion_failures: number;
|
|
137
145
|
network_errors: number;
|
|
138
146
|
};
|
|
147
|
+
agent_directive?: string;
|
|
139
148
|
env_issue?: string;
|
|
149
|
+
auth_hint?: string;
|
|
150
|
+
cascade_skips?: CascadeSkipGroup[];
|
|
140
151
|
failures: Array<{
|
|
141
152
|
suite_name: string;
|
|
142
153
|
test_name: string;
|
|
143
154
|
suite_file?: string;
|
|
144
155
|
status: string;
|
|
145
156
|
failure_type: string;
|
|
157
|
+
recommended_action: RecommendedAction;
|
|
146
158
|
error_message?: string;
|
|
147
159
|
request_method: string | null;
|
|
148
160
|
request_url: string | null;
|
|
@@ -174,8 +186,12 @@ export function diagnoseRun(runId: number, verbose?: boolean, dbPath?: string):
|
|
|
174
186
|
const failures = allResults
|
|
175
187
|
.filter(r => r.status === "fail" || r.status === "error")
|
|
176
188
|
.map(r => {
|
|
177
|
-
const
|
|
189
|
+
const parsedBody = parseBodySafe(r.response_body);
|
|
190
|
+
const hint = envHint(r.request_url, r.error_message, envFilePath) ??
|
|
191
|
+
softDeleteHint(r.response_status, r.request_method, parsedBody) ??
|
|
192
|
+
statusHint(r.response_status);
|
|
178
193
|
const failure_type = classifyFailure(r.status, r.response_status);
|
|
194
|
+
const rec_action = recommendedAction(failure_type, r.response_status);
|
|
179
195
|
const sHint = schemaHint(failure_type, r.response_status);
|
|
180
196
|
return {
|
|
181
197
|
suite_name: r.suite_name,
|
|
@@ -183,13 +199,14 @@ export function diagnoseRun(runId: number, verbose?: boolean, dbPath?: string):
|
|
|
183
199
|
...(r.suite_file ? { suite_file: r.suite_file } : {}),
|
|
184
200
|
status: r.status,
|
|
185
201
|
failure_type,
|
|
202
|
+
recommended_action: rec_action,
|
|
186
203
|
error_message: truncateErrorMessage(r.error_message, verbose),
|
|
187
204
|
request_method: r.request_method,
|
|
188
205
|
request_url: r.request_url,
|
|
189
206
|
response_status: r.response_status,
|
|
190
207
|
...(hint ? { hint } : {}),
|
|
191
208
|
...(sHint ? { schema_hint: sHint } : {}),
|
|
192
|
-
response_body:
|
|
209
|
+
response_body: parsedBody,
|
|
193
210
|
response_headers: filterHeaders(r.response_headers),
|
|
194
211
|
assertions: r.assertions,
|
|
195
212
|
duration_ms: r.duration_ms,
|
|
@@ -198,9 +215,60 @@ export function diagnoseRun(runId: number, verbose?: boolean, dbPath?: string):
|
|
|
198
215
|
|
|
199
216
|
const sharedEnvHint = computeSharedEnvIssue(failures, envFilePath);
|
|
200
217
|
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
218
|
+
let apiErrors = 0, assertionFailures = 0, networkErrors = 0;
|
|
219
|
+
let authFailureCount = 0;
|
|
220
|
+
for (const f of failures) {
|
|
221
|
+
if (f.failure_type === "api_error") apiErrors++;
|
|
222
|
+
else if (f.failure_type === "assertion_failed") assertionFailures++;
|
|
223
|
+
else if (f.failure_type === "network_error") networkErrors++;
|
|
224
|
+
if (f.response_status === 401 || f.response_status === 403) authFailureCount++;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let agent_directive: string | undefined;
|
|
228
|
+
if (apiErrors > 0) {
|
|
229
|
+
const fixable = assertionFailures + networkErrors;
|
|
230
|
+
agent_directive =
|
|
231
|
+
`${apiErrors} test${apiErrors === 1 ? "" : "s"} returned 5xx server errors. ` +
|
|
232
|
+
`Do NOT change test expectations to accept 5xx responses. ` +
|
|
233
|
+
`These are backend bugs, not test logic errors. ` +
|
|
234
|
+
`Stop iterating on these tests and report the failures to the API team.` +
|
|
235
|
+
(fixable > 0
|
|
236
|
+
? ` The remaining ${fixable} failure${fixable === 1 ? "" : "s"} may be fixable in test logic.`
|
|
237
|
+
: "");
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Cascade skips: skipped tests due to missing captures from failed create steps
|
|
241
|
+
const CASCADE_RE = /^Depends on missing capture: (.+)$/;
|
|
242
|
+
const groupMap = new Map<string, string[]>();
|
|
243
|
+
for (const r of allResults) {
|
|
244
|
+
if (r.status !== "skip") continue;
|
|
245
|
+
const match = CASCADE_RE.exec(r.error_message ?? "");
|
|
246
|
+
if (!match) continue;
|
|
247
|
+
const captureVar = match[1]!;
|
|
248
|
+
const existing = groupMap.get(captureVar) ?? [];
|
|
249
|
+
existing.push(`${r.suite_name}/${r.test_name}`);
|
|
250
|
+
groupMap.set(captureVar, existing);
|
|
251
|
+
}
|
|
252
|
+
const cascade_skips: CascadeSkipGroup[] | undefined = groupMap.size > 0
|
|
253
|
+
? [...groupMap.entries()].map(([capture_var, examples]) => ({
|
|
254
|
+
capture_var,
|
|
255
|
+
count: examples.length,
|
|
256
|
+
examples: examples.slice(0, 3),
|
|
257
|
+
}))
|
|
258
|
+
: undefined;
|
|
259
|
+
|
|
260
|
+
// Auth hint: when many tests fail with 401/403, suggest auth setup
|
|
261
|
+
let auth_hint: string | undefined;
|
|
262
|
+
if (authFailureCount >= 5 && authFailureCount / diagRun.total >= 0.3) {
|
|
263
|
+
const loginEndpoint = allResults.find(
|
|
264
|
+
r => r.request_method?.toUpperCase() === "POST" && AUTH_PATH_RE.test(r.request_url ?? "")
|
|
265
|
+
);
|
|
266
|
+
if (loginEndpoint) {
|
|
267
|
+
auth_hint = `${authFailureCount} tests failed with 401/403. Found auth endpoint: POST ${loginEndpoint.request_url} — add \`setup: true\` to your auth suite so its captured token is shared with all other suites, or set auth_token manually in .env.yaml`;
|
|
268
|
+
} else {
|
|
269
|
+
auth_hint = `${authFailureCount} tests failed with 401/403 — add \`setup: true\` to your auth suite so its captured token is shared with all other suites, or set auth_token in .env.yaml`;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
204
272
|
|
|
205
273
|
const { grouped_failures, compactFailures } = verbose
|
|
206
274
|
? { grouped_failures: undefined, compactFailures: failures }
|
|
@@ -221,13 +289,16 @@ export function diagnoseRun(runId: number, verbose?: boolean, dbPath?: string):
|
|
|
221
289
|
assertion_failures: assertionFailures,
|
|
222
290
|
network_errors: networkErrors,
|
|
223
291
|
},
|
|
292
|
+
...(agent_directive ? { agent_directive } : {}),
|
|
224
293
|
...(sharedEnvHint ? { env_issue: sharedEnvHint } : {}),
|
|
294
|
+
...(auth_hint ? { auth_hint } : {}),
|
|
295
|
+
...(cascade_skips ? { cascade_skips } : {}),
|
|
225
296
|
failures: compactFailures,
|
|
226
297
|
...(grouped_failures ? { grouped_failures } : {}),
|
|
227
298
|
};
|
|
228
299
|
}
|
|
229
300
|
|
|
230
|
-
type FailureItem = { suite_name: string; test_name: string; failure_type: string; hint?: string; response_status: number | null };
|
|
301
|
+
type FailureItem = { suite_name: string; test_name: string; failure_type: string; recommended_action: RecommendedAction; hint?: string; response_status: number | null };
|
|
231
302
|
|
|
232
303
|
/** Group similar failures for compact output. Exported for testing. */
|
|
233
304
|
export function groupFailures<T extends FailureItem>(failures: T[]): { grouped_failures?: FailureGroup[]; compactFailures: T[] } {
|
|
@@ -268,6 +339,7 @@ export function groupFailures<T extends FailureItem>(failures: T[]): { grouped_f
|
|
|
268
339
|
pattern,
|
|
269
340
|
count: group.items.length,
|
|
270
341
|
failure_type: group.failure_type,
|
|
342
|
+
recommended_action: group.items[0]!.recommended_action,
|
|
271
343
|
hint: group.hint,
|
|
272
344
|
examples: group.items.slice(0, 2).map(f => `${f.suite_name}/${f.test_name}`),
|
|
273
345
|
response_status: group.response_status,
|
|
@@ -37,6 +37,26 @@ export function envHint(url: string | null, errorMessage: string | null, envFile
|
|
|
37
37
|
return null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export type RecommendedAction =
|
|
41
|
+
| "report_backend_bug"
|
|
42
|
+
| "fix_auth_config"
|
|
43
|
+
| "fix_test_logic"
|
|
44
|
+
| "fix_network_config";
|
|
45
|
+
|
|
46
|
+
export function recommendedAction(
|
|
47
|
+
failureType: "api_error" | "assertion_failed" | "network_error",
|
|
48
|
+
responseStatus: number | null,
|
|
49
|
+
): RecommendedAction {
|
|
50
|
+
if (failureType === "api_error") return "report_backend_bug";
|
|
51
|
+
if (failureType === "network_error") {
|
|
52
|
+
if (responseStatus === 401 || responseStatus === 403) return "fix_auth_config";
|
|
53
|
+
return "fix_network_config";
|
|
54
|
+
}
|
|
55
|
+
// assertion_failed
|
|
56
|
+
if (responseStatus === 401 || responseStatus === 403) return "fix_auth_config";
|
|
57
|
+
return "fix_test_logic";
|
|
58
|
+
}
|
|
59
|
+
|
|
40
60
|
export function envCategory(hint: string | undefined): string | null {
|
|
41
61
|
if (!hint) return null;
|
|
42
62
|
if (hint.includes("base_url is not set") || hint.includes("base_url is missing") || hint.includes("base_url is not configured")) return "base_url_missing";
|
|
@@ -55,6 +75,25 @@ export function schemaHint(
|
|
|
55
75
|
return null;
|
|
56
76
|
}
|
|
57
77
|
|
|
78
|
+
export function softDeleteHint(
|
|
79
|
+
actualStatus: number | null | undefined,
|
|
80
|
+
requestMethod: string | null | undefined,
|
|
81
|
+
responseBody: unknown,
|
|
82
|
+
): string | null {
|
|
83
|
+
if (actualStatus !== 200 || requestMethod?.toUpperCase() !== "GET") return null;
|
|
84
|
+
if (responseBody && typeof responseBody === "object") {
|
|
85
|
+
const hasStatusField =
|
|
86
|
+
"status" in (responseBody as object) ||
|
|
87
|
+
"state" in (responseBody as object) ||
|
|
88
|
+
"deleted" in (responseBody as object) ||
|
|
89
|
+
"is_deleted" in (responseBody as object);
|
|
90
|
+
if (hasStatusField) {
|
|
91
|
+
return 'GET returned 200 with a status/state field after DELETE — likely soft delete. Update the test: remove the "Verify deleted → 404" step and instead assert the status field value (e.g. status: "cancelled")';
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
58
97
|
export function computeSharedEnvIssue(
|
|
59
98
|
failures: Array<{ hint?: string }>,
|
|
60
99
|
envFilePath?: string,
|