@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +132 -112
  2. package/README.md +3 -10
  3. package/package.json +2 -3
  4. package/src/cli/commands/export.ts +144 -0
  5. package/src/cli/commands/generate.ts +32 -0
  6. package/src/cli/commands/run.ts +22 -5
  7. package/src/cli/commands/sync.ts +241 -0
  8. package/src/cli/commands/update.ts +174 -0
  9. package/src/cli/index.ts +67 -10
  10. package/src/core/diagnostics/db-analysis.ts +79 -7
  11. package/src/core/diagnostics/failure-hints.ts +39 -0
  12. package/src/core/exporter/postman.ts +963 -0
  13. package/src/core/generator/data-factory.ts +47 -9
  14. package/src/core/generator/index.ts +1 -1
  15. package/src/core/generator/openapi-reader.ts +6 -0
  16. package/src/core/generator/serializer.ts +17 -2
  17. package/src/core/generator/suite-generator.ts +163 -14
  18. package/src/core/generator/types.ts +1 -0
  19. package/src/core/meta/meta-store.ts +78 -0
  20. package/src/core/meta/types.ts +21 -0
  21. package/src/core/parser/schema.ts +12 -2
  22. package/src/core/parser/types.ts +12 -1
  23. package/src/core/parser/variables.ts +3 -0
  24. package/src/core/parser/yaml-parser.ts +2 -1
  25. package/src/core/runner/assertions.ts +44 -20
  26. package/src/core/runner/execute-run.ts +31 -8
  27. package/src/core/runner/executor.ts +34 -8
  28. package/src/core/runner/http-client.ts +1 -1
  29. package/src/core/runner/types.ts +1 -0
  30. package/src/core/sync/spec-differ.ts +38 -0
  31. package/src/web/server.ts +1 -1
  32. package/src/cli/commands/mcp.ts +0 -16
  33. package/src/mcp/descriptions.ts +0 -47
  34. package/src/mcp/server.ts +0 -38
  35. package/src/mcp/tools/ci-init.ts +0 -54
  36. package/src/mcp/tools/coverage-analysis.ts +0 -141
  37. package/src/mcp/tools/describe-endpoint.ts +0 -27
  38. package/src/mcp/tools/manage-server.ts +0 -86
  39. package/src/mcp/tools/query-db.ts +0 -84
  40. package/src/mcp/tools/run-tests.ts +0 -116
  41. package/src/mcp/tools/send-request.ts +0 -51
  42. package/src/mcp/tools/setup-api.ts +0 -88
  43. /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 hint = envHint(r.request_url, r.error_message, envFilePath) ?? statusHint(r.response_status);
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: parseBodySafe(r.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
- const apiErrors = failures.filter(f => f.failure_type === "api_error").length;
202
- const assertionFailures = failures.filter(f => f.failure_type === "assertion_failed").length;
203
- const networkErrors = failures.filter(f => f.failure_type === "network_error").length;
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,