@kirrosh/zond 0.22.0 → 0.23.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 (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -0,0 +1,39 @@
1
+ /**
2
+ * ARV-55: single source of truth for classifying a run by *what kind of
3
+ * suites it executed*. Before this module the answer was inferred from
4
+ * `suite_file` paths in three places (coverage's `isProbeOnlyRun`,
5
+ * `db diagnose` recommendation branching, and a handful of skill prompts).
6
+ *
7
+ * We resolve the kind once at INSERT-time and persist it in `runs.run_kind`;
8
+ * downstream filters become a column compare instead of a per-result regex.
9
+ *
10
+ * Encoding:
11
+ * - `probe` — every suite path lives under `apis/<api>/probes/` (or a
12
+ * bare `probes/` segment for ad-hoc setups). Coverage hides
13
+ * these by default — probe runs deliberately exercise a
14
+ * subset of endpoints and would otherwise read as a
15
+ * regression vs the prior smoke/CRUD run.
16
+ * - `check` — every suite path lives under `apis/<api>/checks/`. Mirrors
17
+ * the same logic: conformance checks don't reflect endpoint
18
+ * coverage breadth.
19
+ * - `regular` — anything else, including mixed runs (probe + smoke). A
20
+ * mixed run is treated as regular because at least one
21
+ * suite contributed real coverage signal.
22
+ */
23
+
24
+ export type RunKind = "regular" | "probe" | "check";
25
+
26
+ const PROBE_SEGMENT_RE = /(^|\/)probes(\/|$)/;
27
+ const CHECK_SEGMENT_RE = /(^|\/)checks(\/|$)/;
28
+
29
+ export function detectRunKind(suiteFiles: ReadonlyArray<string | null | undefined>): RunKind {
30
+ // Empty / all-empty arrays default to 'regular' — the DB CHECK constraint
31
+ // refuses NULL so callers always receive a concrete kind.
32
+ const paths = suiteFiles
33
+ .filter((p): p is string => typeof p === "string" && p.length > 0);
34
+ if (paths.length === 0) return "regular";
35
+
36
+ if (paths.every((p) => PROBE_SEGMENT_RE.test(p))) return "probe";
37
+ if (paths.every((p) => CHECK_SEGMENT_RE.test(p))) return "check";
38
+ return "regular";
39
+ }
@@ -0,0 +1,312 @@
1
+ import Ajv2020 from "ajv/dist/2020.js";
2
+ import Ajv from "ajv";
3
+ import addFormats from "ajv-formats";
4
+ import type { ErrorObject, ValidateFunction, AnySchema } from "ajv";
5
+ import type { OpenAPIV3 } from "openapi-types";
6
+ import { specPathToRegex } from "../generator/coverage-scanner.ts";
7
+ import type { AssertionResult } from "./types.ts";
8
+
9
+ export interface SchemaValidator {
10
+ validate(method: string, path: string, status: number, body: unknown): AssertionResult[];
11
+ /** TASK-142: surface whether an endpoint and a response branch matched.
12
+ * Lets ad-hoc callers (`zond request --validate-schema`) distinguish
13
+ * "no spec entry for this URL" from "spec entry exists, body is valid". */
14
+ inspect(method: string, path: string, status: number): {
15
+ matchedEndpoint: { method: string; path: string } | null;
16
+ matchedResponseStatus: string | null;
17
+ hasJsonSchema: boolean;
18
+ };
19
+ }
20
+
21
+ interface EndpointEntry {
22
+ method: string;
23
+ path: string;
24
+ regex: RegExp;
25
+ responses: OpenAPIV3.ResponsesObject;
26
+ }
27
+
28
+ const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"] as const;
29
+
30
+ export function createSchemaValidator(doc: OpenAPIV3.Document): SchemaValidator {
31
+ const isV31 = typeof doc.openapi === "string" && doc.openapi.startsWith("3.1");
32
+ // OpenAPI 3.1 → JSON Schema Draft 2020-12; 3.0 → Draft 4/7-ish.
33
+ // verbose:true exposes parentSchema on each error so humanize() can render
34
+ // the full required-set alongside the missing field name (TASK-277).
35
+ const ajv = isV31
36
+ ? new (Ajv2020 as unknown as typeof Ajv)({ strict: false, allErrors: true, verbose: true })
37
+ : new Ajv({ strict: false, allErrors: true, verbose: true });
38
+ addFormats(ajv);
39
+ applyStrictFormats(ajv);
40
+
41
+ const endpoints: EndpointEntry[] = [];
42
+ if (doc.paths) {
43
+ for (const [pathTpl, pathItem] of Object.entries(doc.paths)) {
44
+ if (!pathItem) continue;
45
+ const regex = specPathToRegex(pathTpl);
46
+ for (const m of HTTP_METHODS) {
47
+ const op = (pathItem as Record<string, unknown>)[m] as OpenAPIV3.OperationObject | undefined;
48
+ if (!op || !op.responses) continue;
49
+ endpoints.push({ method: m.toUpperCase(), path: pathTpl, regex, responses: op.responses });
50
+ }
51
+ }
52
+ // Sort by specificity so concrete paths (e.g. /users/me) win over templated
53
+ // ones (/users/{id}) regardless of spec declaration order. Tie-breaker is
54
+ // stable insertion order (Array.sort is stable in modern engines).
55
+ endpoints.sort((a, b) => paramCount(a.path) - paramCount(b.path));
56
+ }
57
+
58
+ // ARV-214: per-schema compile is the dominant cost of --validate-schema
59
+ // on big dereferenced specs (github 14 MiB → minutes per response).
60
+ // The cache below is keyed by schema *object reference*, so endpoints
61
+ // that share a $ref source after dereference hit it on the second
62
+ // access. The slow_compile budget warns the user the first time a
63
+ // schema crosses 1s — the same compile only happens once per schema
64
+ // anyway thanks to the cache, so the warning is per-source, not
65
+ // per-call.
66
+ const SLOW_COMPILE_MS = Number(process.env.ZOND_VALIDATE_SCHEMA_SLOW_COMPILE_MS ?? "1000");
67
+ // Hard byte cap stops the run from hanging minutes on a pathological
68
+ // single response schema (post-deref github Repository / kubernetes
69
+ // pod-spec). Returning a no-op validator + a single warning is much
70
+ // friendlier than blocking on `ajv.compile` for an unbounded time.
71
+ // Default chosen from the bench in /tmp/bench-convert.ts: a 572 KiB
72
+ // schema compiles in ~800 ms; 1 MiB ≈ 1.4 s; beyond that we'd rather
73
+ // skip and tell the user.
74
+ const MAX_SCHEMA_BYTES = Number(process.env.ZOND_VALIDATE_SCHEMA_MAX_BYTES ?? String(1_048_576));
75
+ const compiled = new Map<unknown, ValidateFunction | null>();
76
+ const warnedSlow = new WeakSet<object>();
77
+ const warnedTooLarge = new WeakSet<object>();
78
+
79
+ function tooLargeSentinel(): null {
80
+ return null;
81
+ }
82
+
83
+ function compile(schema: AnySchema): ValidateFunction | null {
84
+ if (compiled.has(schema)) return compiled.get(schema) ?? null;
85
+ // Cheap pre-check: a schema whose JSON serialisation exceeds
86
+ // MAX_SCHEMA_BYTES is almost certainly going to take seconds-to-
87
+ // minutes to compile and produce noisy mid-body errors that aren't
88
+ // worth the wait. Skip it with a warning. JSON.stringify itself is
89
+ // O(n) but at MiB-scale completes in tens of ms — orders of
90
+ // magnitude cheaper than ajv.compile on the same payload.
91
+ if (MAX_SCHEMA_BYTES > 0) {
92
+ try {
93
+ const sz = JSON.stringify(schema)?.length ?? 0;
94
+ if (sz > MAX_SCHEMA_BYTES) {
95
+ if (typeof schema === "object" && schema && !warnedTooLarge.has(schema as object)) {
96
+ warnedTooLarge.add(schema as object);
97
+ const kib = Math.round(sz / 1024);
98
+ process.stderr.write(
99
+ `[zond] schema too large for --validate-schema (${kib} KiB > ${Math.round(MAX_SCHEMA_BYTES / 1024)} KiB) — skipping; raise via ZOND_VALIDATE_SCHEMA_MAX_BYTES.\n`,
100
+ );
101
+ }
102
+ compiled.set(schema, tooLargeSentinel());
103
+ return null;
104
+ }
105
+ } catch { /* circular or non-serialisable — let ajv try */ }
106
+ }
107
+ const prepared = isV31 ? schema : convertOpenApi30(schema);
108
+ const t0 = performance.now();
109
+ const fn = ajv.compile(prepared as AnySchema);
110
+ const elapsed = performance.now() - t0;
111
+ if (elapsed >= SLOW_COMPILE_MS && typeof schema === "object" && schema && !warnedSlow.has(schema as object)) {
112
+ warnedSlow.add(schema as object);
113
+ process.stderr.write(
114
+ `[zond] schema validator compile took ${elapsed.toFixed(0)} ms (warn ≥ ${SLOW_COMPILE_MS} ms) — see ZOND_VALIDATE_SCHEMA_MAX_BYTES if --validate-schema runs feel slow.\n`,
115
+ );
116
+ }
117
+ compiled.set(schema, fn);
118
+ return fn;
119
+ }
120
+
121
+ function findResponseSchema(method: string, path: string, status: number): OpenAPIV3.SchemaObject | undefined {
122
+ const upper = method.toUpperCase();
123
+ // Endpoints are pre-sorted by specificity (concrete paths first), so the
124
+ // first regex match is the most specific — /users/me beats /users/{id}.
125
+ const match = endpoints.find(e => e.method === upper && e.regex.test(path));
126
+ if (!match) return undefined;
127
+ const responses = match.responses;
128
+ const exact = responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
129
+ const wildcard = responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
130
+ const fallback = responses.default as OpenAPIV3.ResponseObject | undefined;
131
+ const response = exact ?? wildcard ?? fallback;
132
+ if (!response || !response.content) return undefined;
133
+ const json = response.content["application/json"];
134
+ return (json?.schema as OpenAPIV3.SchemaObject | undefined) ?? undefined;
135
+ }
136
+
137
+ function inspectMatch(method: string, path: string, status: number) {
138
+ const upper = method.toUpperCase();
139
+ const ep = endpoints.find(e => e.method === upper && e.regex.test(path));
140
+ if (!ep) {
141
+ return { matchedEndpoint: null, matchedResponseStatus: null, hasJsonSchema: false };
142
+ }
143
+ const exact = ep.responses[String(status)] as OpenAPIV3.ResponseObject | undefined;
144
+ const wildcard = ep.responses[`${Math.floor(status / 100)}XX`] as OpenAPIV3.ResponseObject | undefined;
145
+ const fallback = ep.responses.default as OpenAPIV3.ResponseObject | undefined;
146
+ const matchedKey = exact ? String(status) : wildcard ? `${Math.floor(status / 100)}XX` : fallback ? "default" : null;
147
+ const response = exact ?? wildcard ?? fallback;
148
+ const hasJsonSchema = !!(response?.content?.["application/json"]?.schema);
149
+ return {
150
+ matchedEndpoint: { method: ep.method, path: ep.path },
151
+ matchedResponseStatus: matchedKey,
152
+ hasJsonSchema,
153
+ };
154
+ }
155
+
156
+ return {
157
+ validate(method, path, status, body) {
158
+ const schema = findResponseSchema(method, path, status);
159
+ if (!schema) return [];
160
+ let validator: ValidateFunction | null;
161
+ try {
162
+ validator = compile(schema);
163
+ } catch (err) {
164
+ return [{
165
+ field: "body",
166
+ rule: "schema.compile_error",
167
+ passed: false,
168
+ actual: undefined,
169
+ expected: err instanceof Error ? err.message : String(err),
170
+ }];
171
+ }
172
+ // ARV-214: schema crossed ZOND_VALIDATE_SCHEMA_MAX_BYTES — surface
173
+ // the skip as a passing assertion so the run stays green and the
174
+ // user knows validation was bypassed for this body.
175
+ if (validator === null) {
176
+ return [{
177
+ field: "body",
178
+ rule: "schema.skipped_too_large",
179
+ passed: true,
180
+ actual: undefined,
181
+ expected: "schema exceeded ZOND_VALIDATE_SCHEMA_MAX_BYTES — see stderr warning",
182
+ kind: "schema",
183
+ }];
184
+ }
185
+ const ok = validator(body);
186
+ if (ok) return [];
187
+ const errors = validator.errors ?? [];
188
+ return errors.map(e => ajvErrorToAssertion(e, body));
189
+ },
190
+ inspect: inspectMatch,
191
+ };
192
+ }
193
+
194
+ function paramCount(path: string): number {
195
+ let n = 0;
196
+ for (const seg of path.split("/")) if (/^\{[^}]+\}$/.test(seg)) n++;
197
+ return n;
198
+ }
199
+
200
+ function ajvErrorToAssertion(err: ErrorObject, body: unknown): AssertionResult {
201
+ const ptr = err.instancePath || "";
202
+ // Field key like "body" or "body.user.email" for parity with checkAssertions.
203
+ const field = ptr ? `body${ptr.replace(/\//g, ".")}` : "body";
204
+ const actual = ptr ? getByJsonPointer(body, ptr) : body;
205
+ return {
206
+ field,
207
+ rule: `schema.${err.keyword}`,
208
+ passed: false,
209
+ actual,
210
+ expected: humanize(err),
211
+ kind: "schema",
212
+ };
213
+ }
214
+
215
+ function humanize(err: ErrorObject): string {
216
+ switch (err.keyword) {
217
+ case "required": {
218
+ const missing = (err.params as { missingProperty: string }).missingProperty;
219
+ // verbose:true gives us the parent schema; surface the full required-set
220
+ // so the user can see drift at a glance instead of decoding the message
221
+ // one missing-field-error at a time (TASK-277).
222
+ const parent = (err as ErrorObject & { parentSchema?: unknown }).parentSchema;
223
+ const required =
224
+ parent && typeof parent === "object" && Array.isArray((parent as { required?: unknown }).required)
225
+ ? ((parent as { required: string[] }).required)
226
+ : undefined;
227
+ const tail = required ? `; expected required: [${required.join(", ")}]` : "";
228
+ return `missing required field "${missing}"${tail}`;
229
+ }
230
+ case "type":
231
+ return `type ${(err.params as { type: string | string[] }).type}`;
232
+ case "enum":
233
+ return `one of ${JSON.stringify((err.params as { allowedValues: unknown[] }).allowedValues)}`;
234
+ case "format":
235
+ return `format "${(err.params as { format: string }).format}"`;
236
+ case "additionalProperties":
237
+ return `no additional property "${(err.params as { additionalProperty: string }).additionalProperty}"`;
238
+ case "const":
239
+ return `const ${JSON.stringify((err.params as { allowedValue: unknown }).allowedValue)}`;
240
+ case "minLength":
241
+ case "maxLength":
242
+ case "minimum":
243
+ case "maximum":
244
+ case "exclusiveMinimum":
245
+ case "exclusiveMaximum":
246
+ case "multipleOf":
247
+ case "pattern":
248
+ return `${err.keyword} ${JSON.stringify((err.params as Record<string, unknown>)[err.keyword] ?? "")}`.trim();
249
+ default:
250
+ return err.message ?? err.keyword;
251
+ }
252
+ }
253
+
254
+ function getByJsonPointer(obj: unknown, pointer: string): unknown {
255
+ if (!pointer) return obj;
256
+ const segments = pointer.split("/").slice(1).map(s => s.replace(/~1/g, "/").replace(/~0/g, "~"));
257
+ let cur: unknown = obj;
258
+ for (const seg of segments) {
259
+ if (cur && typeof cur === "object") {
260
+ cur = (cur as Record<string, unknown>)[seg];
261
+ } else {
262
+ return undefined;
263
+ }
264
+ }
265
+ return cur;
266
+ }
267
+
268
+ // RFC3339 §5.6: date-time = full-date "T" full-time. T (or t) is required as
269
+ // separator; offset is "Z" or "[+-]HH:MM" with explicit colon. ajv-formats
270
+ // accepts " " as separator and "+HH" without colon, which lets PostgreSQL-style
271
+ // timestamps ("2026-04-29 07:10:44.674675+00") slip through.
272
+ export const STRICT_RFC3339_DATE_TIME =
273
+ /^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])[Tt](?:[01]\d|2[0-3]):[0-5]\d:(?:[0-5]\d|60)(?:\.\d+)?(?:[Zz]|[+-](?:[01]\d|2[0-3]):[0-5]\d)$/;
274
+
275
+ function applyStrictFormats(ajv: Ajv): void {
276
+ // Override ajv-formats' lax date-time with strict RFC3339.
277
+ ajv.addFormat("date-time", { type: "string", validate: STRICT_RFC3339_DATE_TIME });
278
+ }
279
+
280
+ /**
281
+ * Convert OpenAPI 3.0 schema to JSON Schema Draft 7-compatible:
282
+ * - `nullable: true` → add "null" to `type`.
283
+ * - Drop unsupported `example`, `xml`, `discriminator` keywords (ajv tolerates with strict:false).
284
+ */
285
+ function convertOpenApi30(schema: AnySchema): AnySchema {
286
+ if (!schema || typeof schema !== "object") return schema;
287
+ if (Array.isArray(schema)) {
288
+ return schema.map(s => convertOpenApi30(s as AnySchema)) as unknown as AnySchema;
289
+ }
290
+ const src = schema as Record<string, unknown>;
291
+ const out: Record<string, unknown> = {};
292
+ for (const [k, v] of Object.entries(src)) {
293
+ if (k === "nullable") continue;
294
+ if (k === "type" && src.nullable === true) {
295
+ if (Array.isArray(v)) {
296
+ out.type = [...v as unknown[], "null"];
297
+ } else if (typeof v === "string") {
298
+ out.type = [v, "null"];
299
+ } else {
300
+ out.type = v;
301
+ }
302
+ continue;
303
+ }
304
+ if (v && typeof v === "object") {
305
+ out[k] = convertOpenApi30(v as AnySchema);
306
+ } else {
307
+ out[k] = v;
308
+ }
309
+ }
310
+ // Standalone nullable: true with no explicit type → leave as-is (ajv accepts any).
311
+ return out as AnySchema;
312
+ }
@@ -2,23 +2,61 @@ import { executeRequest } from "./http-client.ts";
2
2
  import { loadEnvironment, substituteString, substituteDeep } from "../parser/variables.ts";
3
3
  import { getDb } from "../../db/schema.ts";
4
4
  import { findCollectionByNameOrId } from "../../db/queries.ts";
5
+ import { encodeFormBody } from "./form-encode.ts";
5
6
 
6
- function extractByPath(obj: unknown, path: string): unknown {
7
- const segments = path.replace(/\[(\d+)\]/g, '.$1').split('.').filter(Boolean);
7
+ function hasHeaderCI(headers: Record<string, string>, name: string): boolean {
8
+ const lower = name.toLowerCase();
9
+ return Object.keys(headers).some((k) => k.toLowerCase() === lower);
10
+ }
11
+
12
+ export function extractByPath(obj: unknown, path: string): unknown {
13
+ return extractByPathWithDiagnostic(obj, path).value;
14
+ }
15
+
16
+ /** ARV-70 (feedback round-01 / F11): same extractor as above but also
17
+ * reports *why* the path failed. The CLI surfaces this on stderr when
18
+ * `--json-path` returns undefined so users don't lose minutes debugging
19
+ * "empty stdout despite data[0].id is right there in the JSON" — the
20
+ * hint pinpoints the segment that didn't resolve (e.g. "body is a string
21
+ * — content-type was not application/json"). */
22
+ export function extractByPathWithDiagnostic(
23
+ obj: unknown,
24
+ path: string,
25
+ ): { value: unknown; resolved: string[]; failedAt?: string; reason?: string } {
26
+ const segments = path.replace(/\[(\d+)\]/g, ".$1").split(".").filter(Boolean);
27
+ const resolved: string[] = [];
8
28
  let current: unknown = obj;
9
29
  for (const seg of segments) {
10
- if (current === null || current === undefined) return undefined;
30
+ if (current === null || current === undefined) {
31
+ return { value: undefined, resolved, failedAt: seg, reason: "previous segment resolved to null/undefined" };
32
+ }
11
33
  if (Array.isArray(current)) {
12
34
  const idx = parseInt(seg, 10);
13
- if (isNaN(idx)) return undefined;
35
+ if (isNaN(idx)) {
36
+ return { value: undefined, resolved, failedAt: seg, reason: `expected an array index, got non-numeric segment "${seg}"` };
37
+ }
38
+ if (idx < 0 || idx >= current.length) {
39
+ return { value: undefined, resolved, failedAt: seg, reason: `array index ${idx} out of bounds (length ${current.length})` };
40
+ }
14
41
  current = current[idx];
15
- } else if (typeof current === 'object') {
16
- current = (current as Record<string, unknown>)[seg];
42
+ } else if (typeof current === "object") {
43
+ const obj = current as Record<string, unknown>;
44
+ if (!(seg in obj)) {
45
+ const keys = Object.keys(obj).slice(0, 8).join(", ");
46
+ return { value: undefined, resolved, failedAt: seg, reason: `key "${seg}" not in object (keys: ${keys || "<empty>"})` };
47
+ }
48
+ current = obj[seg];
17
49
  } else {
18
- return undefined;
50
+ return {
51
+ value: undefined,
52
+ resolved,
53
+ failedAt: seg,
54
+ reason: `cannot traverse "${seg}" — body is a ${typeof current === "string" ? "string (content-type may not be application/json)" : typeof current}`,
55
+ };
19
56
  }
57
+ resolved.push(seg);
20
58
  }
21
- return current;
59
+ return { value: current, resolved };
22
60
  }
23
61
 
24
62
  export interface SendAdHocRequestOptions {
@@ -33,6 +71,15 @@ export interface SendAdHocRequestOptions {
33
71
  maxResponseChars?: number;
34
72
  dbPath?: string;
35
73
  searchDir?: string;
74
+ /** Extra vars merged on top of env (e.g. captured values from a stored run). */
75
+ extraVars?: Record<string, unknown>;
76
+ /** When true, resolve interpolation but do not actually send the request. */
77
+ dryRun?: boolean;
78
+ /** ARV-149: when true, send the body as `application/x-www-form-urlencoded`.
79
+ * Parses `body` as JSON to lift fields, then re-encodes with bracket notation
80
+ * (Stripe-style nested keys). If `body` isn't JSON-parseable it's passed
81
+ * through verbatim, and only the Content-Type header is set. */
82
+ form?: boolean;
36
83
  }
37
84
 
38
85
  export interface SendAdHocRequestResult {
@@ -40,25 +87,75 @@ export interface SendAdHocRequestResult {
40
87
  headers: Record<string, string>;
41
88
  body: unknown;
42
89
  duration_ms: number;
90
+ /** ARV-70: when --json-path failed to resolve, this carries which
91
+ * segment broke and why so the CLI can surface a hint on stderr. */
92
+ jsonPathDiagnostic?: { resolved: string[]; failedAt?: string; reason?: string };
43
93
  }
44
94
 
45
- export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promise<SendAdHocRequestResult> {
95
+ export interface ResolvedRequest {
96
+ method: string;
97
+ url: string;
98
+ headers: Record<string, string>;
99
+ body?: string;
100
+ }
101
+
102
+ export async function resolveAdHocRequest(options: SendAdHocRequestOptions): Promise<ResolvedRequest> {
46
103
  let searchDir = options.searchDir ?? process.cwd();
47
104
  if (options.collectionName) {
48
105
  getDb(options.dbPath);
49
106
  const col = findCollectionByNameOrId(options.collectionName);
50
- if (col?.base_dir) searchDir = col.base_dir;
107
+ if (!col) {
108
+ throw new Error(`API '${options.collectionName}' is not registered. Run \`zond add api <name> --base-url <url>\` first, or check the name with \`zond db collections\`.`);
109
+ }
110
+ if (col.base_dir) searchDir = col.base_dir;
111
+ }
112
+ const envVars = await loadEnvironment(options.envName, searchDir);
113
+ const vars = options.extraVars ? { ...envVars, ...options.extraVars } : envVars;
114
+
115
+ // Auto-prefix base_url for relative paths when --api is in play.
116
+ // Mirror the YAML-runner ergonomics: `zond request --api jp GET /users/1`
117
+ // should work the same as `... GET '{{base_url}}/users/1'`. We touch the URL
118
+ // only when it's clearly relative (leading "/") and has no scheme/template
119
+ // already, so absolute URLs and explicit {{var}} templates pass through.
120
+ let urlToResolve = options.url;
121
+ if (
122
+ options.collectionName
123
+ && typeof vars.base_url === "string"
124
+ && vars.base_url.length > 0
125
+ && urlToResolve.startsWith("/")
126
+ && !urlToResolve.startsWith("//")
127
+ ) {
128
+ const base = vars.base_url.replace(/\/+$/, "");
129
+ urlToResolve = `${base}${urlToResolve}`;
51
130
  }
52
- const vars = await loadEnvironment(options.envName, searchDir);
53
131
 
54
- const resolvedUrl = substituteString(options.url, vars) as string;
132
+ const resolvedUrl = substituteString(urlToResolve, vars) as string;
55
133
  const parsedHeaders = options.headers ?? {};
56
134
  const resolvedHeaders = Object.keys(parsedHeaders).length > 0 ? substituteDeep(parsedHeaders, vars) : {};
57
- const resolvedBody = options.body ? substituteString(options.body, vars) as string : undefined;
135
+ let resolvedBody = options.body ? substituteString(options.body, vars) as string : undefined;
58
136
 
59
- // Auto-detect Content-Type for body if not explicitly set
60
137
  const finalHeaders: Record<string, string> = { ...resolvedHeaders };
61
- if (resolvedBody && !finalHeaders["Content-Type"] && !finalHeaders["content-type"]) {
138
+ const hasContentType =
139
+ finalHeaders["Content-Type"] !== undefined || finalHeaders["content-type"] !== undefined;
140
+
141
+ // ARV-149: `--form` (or auto-detection from spec content type) re-encodes
142
+ // the JSON body as `application/x-www-form-urlencoded` with bracket
143
+ // notation. Stripe v1 and other Rails/PHP-style APIs declare ONLY form
144
+ // bodies on their mutating endpoints — sending JSON yields a 400
145
+ // "check that your POST content type is application/x-www-form-urlencoded".
146
+ if (resolvedBody && options.form) {
147
+ try {
148
+ const parsed = JSON.parse(resolvedBody);
149
+ if (parsed && typeof parsed === "object" && !Array.isArray(parsed)) {
150
+ resolvedBody = encodeFormBody(parsed as Record<string, unknown>);
151
+ }
152
+ } catch {
153
+ // Body isn't JSON — assume it's already urlencoded; pass through verbatim.
154
+ }
155
+ if (!hasContentType) {
156
+ finalHeaders["Content-Type"] = "application/x-www-form-urlencoded";
157
+ }
158
+ } else if (resolvedBody && !hasContentType) {
62
159
  try {
63
160
  JSON.parse(resolvedBody);
64
161
  finalHeaders["Content-Type"] = "application/json";
@@ -67,20 +164,55 @@ export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promis
67
164
  }
68
165
  }
69
166
 
167
+ // TASK-231: when --api resolved an env with `auth_token`, auto-inject the
168
+ // standard `Authorization: Bearer …` header. This mirrors the YAML runner's
169
+ // behaviour (probes/suites template the same header from the security
170
+ // scheme) so `zond request --api X GET /…` doesn't 401 just because the
171
+ // user didn't repeat `--header "Authorization: Bearer {{auth_token}}"`.
172
+ // User-supplied Authorization always wins.
173
+ if (
174
+ options.collectionName
175
+ && typeof vars.auth_token === "string"
176
+ && vars.auth_token.length > 0
177
+ && !hasHeaderCI(finalHeaders, "Authorization")
178
+ ) {
179
+ finalHeaders["Authorization"] = `Bearer ${vars.auth_token}`;
180
+ }
181
+
182
+ return {
183
+ method: options.method,
184
+ url: resolvedUrl,
185
+ headers: finalHeaders,
186
+ ...(resolvedBody !== undefined ? { body: resolvedBody } : {}),
187
+ };
188
+ }
189
+
190
+ // Note: `options.form` is consumed inside `resolveAdHocRequest` itself —
191
+ // the encoded `body` string and Content-Type are baked into `finalHeaders`.
192
+ // `sendAdHocRequest` doesn't need to forward the flag separately.
193
+
194
+ export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promise<SendAdHocRequestResult> {
195
+ const resolved = await resolveAdHocRequest(options);
196
+
70
197
  const response = await executeRequest(
71
198
  {
72
- method: options.method,
73
- url: resolvedUrl,
74
- headers: finalHeaders,
75
- body: resolvedBody,
199
+ method: resolved.method,
200
+ url: resolved.url,
201
+ headers: resolved.headers,
202
+ body: resolved.body,
76
203
  },
77
204
  options.timeout ? { timeout: options.timeout } : undefined,
78
205
  );
79
206
 
80
207
  let responseBody: unknown = response.body_parsed ?? response.body;
208
+ let jsonPathDiagnostic: SendAdHocRequestResult["jsonPathDiagnostic"];
81
209
 
82
210
  if (options.jsonPath && responseBody !== undefined) {
83
- responseBody = extractByPath(responseBody, options.jsonPath);
211
+ const diag = extractByPathWithDiagnostic(responseBody, options.jsonPath);
212
+ responseBody = diag.value;
213
+ if (diag.value === undefined && diag.failedAt) {
214
+ jsonPathDiagnostic = { resolved: diag.resolved, failedAt: diag.failedAt, reason: diag.reason };
215
+ }
84
216
  }
85
217
 
86
218
  const result: SendAdHocRequestResult = {
@@ -88,6 +220,7 @@ export async function sendAdHocRequest(options: SendAdHocRequestOptions): Promis
88
220
  headers: response.headers,
89
221
  body: responseBody,
90
222
  duration_ms: response.duration_ms,
223
+ ...(jsonPathDiagnostic ? { jsonPathDiagnostic } : {}),
91
224
  };
92
225
 
93
226
  return result;
@@ -14,14 +14,26 @@ export interface HttpResponse {
14
14
  body: string;
15
15
  body_parsed?: unknown;
16
16
  duration_ms: number;
17
+ /** TASK-144: number of network-level retries that preceded this response.
18
+ * 0 when the request succeeded on the first attempt. */
19
+ network_retry_count?: number;
17
20
  }
18
21
 
22
+ /**
23
+ * `kind` lets the UI surface the assertion that matches the test's stated
24
+ * intent (primary) ahead of OpenAPI schema noise (schema) and housekeeping
25
+ * checks like duration/header asserts (auxiliary). Optional for backwards
26
+ * compatibility — older runs render as `primary` by default.
27
+ */
28
+ export type AssertionKind = "primary" | "schema" | "auxiliary";
29
+
19
30
  export interface AssertionResult {
20
31
  field: string;
21
32
  rule: string;
22
33
  passed: boolean;
23
34
  actual: unknown;
24
35
  expected: unknown;
36
+ kind?: AssertionKind;
25
37
  }
26
38
 
27
39
  export interface StepResult {
@@ -33,6 +45,32 @@ export interface StepResult {
33
45
  assertions: AssertionResult[];
34
46
  captures: Record<string, unknown>;
35
47
  error?: string;
48
+ provenance?: import("../parser/types.ts").SourceMetadata | null;
49
+ /** TASK-101: classification of why this failure happened — definitely_bug,
50
+ * likely_bug, quirk, env_issue. `null` for passed/skipped/unclassifiable. */
51
+ failure_class?: import("../diagnostics/failure-class.ts").FailureClass | null;
52
+ failure_class_reason?: string | null;
53
+ /** TASK-102: JSON Pointer into the OpenAPI doc + frozen excerpt of the
54
+ * schema at that pointer. Captured at run time so later spec edits don't
55
+ * rewrite history. `null` for manual YAML or when spec isn't available. */
56
+ spec_pointer?: string | null;
57
+ spec_excerpt?: string | null;
58
+ /** TASK-144: how many network-level retries the http-client performed
59
+ * before this step settled. Omitted (or 0) when no retry was needed.
60
+ * Surfaced in the JSON report so flaky-network steps are visible. */
61
+ network_retry?: number;
62
+ /** ARV-157: summary of `--validate-schema` outcome for this step. Present
63
+ * only when the runner had a schema validator attached AND a JSON body
64
+ * was returned (so consumers can distinguish "no drift" from "never
65
+ * validated"). Granular failures still live in `assertions[]` with
66
+ * `kind: "schema"`; this block is the at-a-glance shape skill docs
67
+ * describe and JSON-report consumers grep for. */
68
+ schema_validation?: {
69
+ result: "PASS" | "FAIL" | "no-endpoint" | "no-schema";
70
+ matched_endpoint: { method: string; path: string } | null;
71
+ matched_response_status: string | null;
72
+ error_count: number;
73
+ };
36
74
  }
37
75
 
38
76
  export interface TestRunResult {