@kirrosh/zond 0.26.1 → 0.27.1

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 (261) hide show
  1. package/CHANGELOG.md +65 -0
  2. package/README.md +39 -22
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +36 -23
  5. package/scripts/npm/postinstall.mjs +76 -0
  6. package/src/CLAUDE.md +0 -112
  7. package/src/bun-types.d.ts +0 -5
  8. package/src/cli/argv.ts +0 -122
  9. package/src/cli/commands/add-api.ts +0 -146
  10. package/src/cli/commands/api/annotate/idempotency.ts +0 -59
  11. package/src/cli/commands/api/annotate/index.ts +0 -880
  12. package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
  13. package/src/cli/commands/api/annotate/overlay.ts +0 -206
  14. package/src/cli/commands/api/annotate/pagination.ts +0 -64
  15. package/src/cli/commands/api/annotate/prompts.ts +0 -220
  16. package/src/cli/commands/api/annotate/readback.ts +0 -58
  17. package/src/cli/commands/api/annotate/resources.ts +0 -91
  18. package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
  19. package/src/cli/commands/audit.ts +0 -786
  20. package/src/cli/commands/catalog.ts +0 -97
  21. package/src/cli/commands/check.ts +0 -361
  22. package/src/cli/commands/checks.ts +0 -1072
  23. package/src/cli/commands/ci-init.ts +0 -223
  24. package/src/cli/commands/clean.ts +0 -212
  25. package/src/cli/commands/cleanup.ts +0 -236
  26. package/src/cli/commands/completions.ts +0 -192
  27. package/src/cli/commands/coverage.ts +0 -872
  28. package/src/cli/commands/db.ts +0 -611
  29. package/src/cli/commands/describe.ts +0 -120
  30. package/src/cli/commands/discover.ts +0 -1356
  31. package/src/cli/commands/doctor.ts +0 -661
  32. package/src/cli/commands/fixtures.ts +0 -402
  33. package/src/cli/commands/generate.ts +0 -577
  34. package/src/cli/commands/init/agents-md.ts +0 -61
  35. package/src/cli/commands/init/bootstrap.ts +0 -111
  36. package/src/cli/commands/init/index.ts +0 -244
  37. package/src/cli/commands/init/skills.ts +0 -141
  38. package/src/cli/commands/init/templates/agents.md +0 -86
  39. package/src/cli/commands/init/templates/markdown.d.ts +0 -4
  40. package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
  41. package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
  42. package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
  43. package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
  44. package/src/cli/commands/init/templates/skills/zond.md +0 -861
  45. package/src/cli/commands/init/templates/zond-config.yml +0 -14
  46. package/src/cli/commands/prepare-fixtures.ts +0 -97
  47. package/src/cli/commands/probe/_seed-bodies.ts +0 -52
  48. package/src/cli/commands/probe/mass-assignment.ts +0 -594
  49. package/src/cli/commands/probe/security.ts +0 -537
  50. package/src/cli/commands/probe/static.ts +0 -255
  51. package/src/cli/commands/probe/webhooks.ts +0 -163
  52. package/src/cli/commands/probe.ts +0 -535
  53. package/src/cli/commands/reference.ts +0 -87
  54. package/src/cli/commands/refresh-api.ts +0 -227
  55. package/src/cli/commands/remove-api.ts +0 -150
  56. package/src/cli/commands/report-bundle.ts +0 -310
  57. package/src/cli/commands/report.ts +0 -241
  58. package/src/cli/commands/request.ts +0 -548
  59. package/src/cli/commands/run.ts +0 -1162
  60. package/src/cli/commands/schema-from-runs.ts +0 -128
  61. package/src/cli/commands/secrets.ts +0 -133
  62. package/src/cli/commands/session.ts +0 -244
  63. package/src/cli/commands/use.ts +0 -74
  64. package/src/cli/index.ts +0 -55
  65. package/src/cli/json-envelope.ts +0 -108
  66. package/src/cli/json-schemas.ts +0 -314
  67. package/src/cli/output.ts +0 -40
  68. package/src/cli/program.ts +0 -219
  69. package/src/cli/resolve.ts +0 -105
  70. package/src/cli/runtime.ts +0 -7
  71. package/src/cli/safe-live.ts +0 -24
  72. package/src/cli/status-filter.ts +0 -114
  73. package/src/cli/util/api-context.ts +0 -85
  74. package/src/cli/version.ts +0 -8
  75. package/src/core/audit/persist.ts +0 -183
  76. package/src/core/checks/budget.ts +0 -59
  77. package/src/core/checks/checks/_crud-helpers.ts +0 -133
  78. package/src/core/checks/checks/_negative_mutator.ts +0 -133
  79. package/src/core/checks/checks/_readback-helpers.ts +0 -133
  80. package/src/core/checks/checks/content_type_conformance.ts +0 -39
  81. package/src/core/checks/checks/cross_call_references.ts +0 -147
  82. package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
  83. package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
  84. package/src/core/checks/checks/idempotency_replay.ts +0 -242
  85. package/src/core/checks/checks/ignored_auth.ts +0 -254
  86. package/src/core/checks/checks/index.ts +0 -68
  87. package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
  88. package/src/core/checks/checks/missing_required_header.ts +0 -40
  89. package/src/core/checks/checks/negative_data_rejection.ts +0 -148
  90. package/src/core/checks/checks/not_a_server_error.ts +0 -35
  91. package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
  92. package/src/core/checks/checks/pagination_invariants.ts +0 -419
  93. package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
  94. package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
  95. package/src/core/checks/checks/response_headers_conformance.ts +0 -74
  96. package/src/core/checks/checks/response_schema_conformance.ts +0 -30
  97. package/src/core/checks/checks/status_code_conformance.ts +0 -132
  98. package/src/core/checks/checks/unsupported_method.ts +0 -63
  99. package/src/core/checks/checks/use_after_free.ts +0 -78
  100. package/src/core/checks/index.ts +0 -30
  101. package/src/core/checks/mode.ts +0 -82
  102. package/src/core/checks/recommended-action.ts +0 -68
  103. package/src/core/checks/registry.ts +0 -78
  104. package/src/core/checks/runner.ts +0 -1461
  105. package/src/core/checks/sarif.ts +0 -230
  106. package/src/core/checks/spec-findings.ts +0 -308
  107. package/src/core/checks/stateful.ts +0 -121
  108. package/src/core/checks/types.ts +0 -305
  109. package/src/core/checks/zond-extensions.ts +0 -73
  110. package/src/core/classifier/recommended-action.ts +0 -251
  111. package/src/core/context/current.ts +0 -51
  112. package/src/core/context/session.ts +0 -78
  113. package/src/core/coverage/loader.ts +0 -216
  114. package/src/core/coverage/reasons.ts +0 -300
  115. package/src/core/diagnostics/db-analysis.ts +0 -666
  116. package/src/core/diagnostics/failure-class.ts +0 -140
  117. package/src/core/diagnostics/failure-hints.ts +0 -112
  118. package/src/core/diagnostics/spec-pointer.ts +0 -99
  119. package/src/core/diagnostics/suggested-fixes.ts +0 -155
  120. package/src/core/exporter/case-study/index.ts +0 -270
  121. package/src/core/exporter/curl.ts +0 -40
  122. package/src/core/exporter/exporter.ts +0 -48
  123. package/src/core/exporter/html-report/escape.ts +0 -24
  124. package/src/core/exporter/html-report/index.ts +0 -479
  125. package/src/core/exporter/html-report/script.ts +0 -100
  126. package/src/core/exporter/html-report/styles.ts +0 -408
  127. package/src/core/generator/catalog-builder.ts +0 -179
  128. package/src/core/generator/chunker.ts +0 -78
  129. package/src/core/generator/coverage-phase.ts +0 -0
  130. package/src/core/generator/coverage-scanner.ts +0 -87
  131. package/src/core/generator/data-factory.ts +0 -759
  132. package/src/core/generator/describe.ts +0 -302
  133. package/src/core/generator/endpoint-warnings.ts +0 -52
  134. package/src/core/generator/fixtures-builder.ts +0 -332
  135. package/src/core/generator/index.ts +0 -14
  136. package/src/core/generator/openapi-reader.ts +0 -297
  137. package/src/core/generator/path-param-disambig.ts +0 -140
  138. package/src/core/generator/resources-builder.ts +0 -898
  139. package/src/core/generator/schema-utils.ts +0 -112
  140. package/src/core/generator/serializer.ts +0 -343
  141. package/src/core/generator/suite-generator.ts +0 -1301
  142. package/src/core/generator/types.ts +0 -63
  143. package/src/core/identity/identity-file.ts +0 -0
  144. package/src/core/lint/affects.ts +0 -28
  145. package/src/core/lint/config.ts +0 -96
  146. package/src/core/lint/format.ts +0 -42
  147. package/src/core/lint/index.ts +0 -94
  148. package/src/core/lint/reporter.ts +0 -128
  149. package/src/core/lint/rules/consistency.ts +0 -158
  150. package/src/core/lint/rules/heuristics.ts +0 -97
  151. package/src/core/lint/rules/strictness.ts +0 -109
  152. package/src/core/lint/types.ts +0 -96
  153. package/src/core/lint/walker.ts +0 -248
  154. package/src/core/meta/meta-store.ts +0 -11
  155. package/src/core/output/README.md +0 -73
  156. package/src/core/output/index.ts +0 -13
  157. package/src/core/output/run.ts +0 -91
  158. package/src/core/output/types.ts +0 -122
  159. package/src/core/parser/dynamic-values.ts +0 -160
  160. package/src/core/parser/env-interpolation.ts +0 -104
  161. package/src/core/parser/filter.ts +0 -97
  162. package/src/core/parser/schema.ts +0 -359
  163. package/src/core/parser/types.ts +0 -117
  164. package/src/core/parser/variables.ts +0 -0
  165. package/src/core/parser/yaml-parser.ts +0 -181
  166. package/src/core/probe/bootstrap.ts +0 -34
  167. package/src/core/probe/dry-run-envelope.ts +0 -61
  168. package/src/core/probe/mass-assignment/classify.ts +0 -175
  169. package/src/core/probe/mass-assignment/cleanup.ts +0 -52
  170. package/src/core/probe/mass-assignment/digest.ts +0 -114
  171. package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
  172. package/src/core/probe/mass-assignment/regression.ts +0 -141
  173. package/src/core/probe/mass-assignment/suspects.ts +0 -92
  174. package/src/core/probe/mass-assignment/types.ts +0 -135
  175. package/src/core/probe/mass-assignment-probe-class.ts +0 -198
  176. package/src/core/probe/mass-assignment-probe.ts +0 -27
  177. package/src/core/probe/mass-assignment-template.ts +0 -240
  178. package/src/core/probe/method-probe.ts +0 -164
  179. package/src/core/probe/method-shared.ts +0 -69
  180. package/src/core/probe/negative-probe.ts +0 -691
  181. package/src/core/probe/orphan-tracker.ts +0 -188
  182. package/src/core/probe/path-discovery.ts +0 -439
  183. package/src/core/probe/probe-harness.ts +0 -119
  184. package/src/core/probe/registry.ts +0 -89
  185. package/src/core/probe/runner.ts +0 -136
  186. package/src/core/probe/security/baseline.ts +0 -174
  187. package/src/core/probe/security/classify.ts +0 -341
  188. package/src/core/probe/security/cleanup.ts +0 -125
  189. package/src/core/probe/security/detectors.ts +0 -71
  190. package/src/core/probe/security/digest.ts +0 -104
  191. package/src/core/probe/security/orchestrator.ts +0 -398
  192. package/src/core/probe/security/regression.ts +0 -103
  193. package/src/core/probe/security/types.ts +0 -151
  194. package/src/core/probe/security-probe-class.ts +0 -207
  195. package/src/core/probe/security-probe.ts +0 -32
  196. package/src/core/probe/shared.ts +0 -531
  197. package/src/core/probe/static-probe-class.ts +0 -125
  198. package/src/core/probe/types.ts +0 -165
  199. package/src/core/probe/verdict-aggregator.ts +0 -33
  200. package/src/core/probe/webhooks-probe.ts +0 -282
  201. package/src/core/reporter/console.ts +0 -240
  202. package/src/core/reporter/index.ts +0 -22
  203. package/src/core/reporter/json.ts +0 -22
  204. package/src/core/reporter/junit.ts +0 -93
  205. package/src/core/reporter/ndjson.ts +0 -37
  206. package/src/core/reporter/types.ts +0 -15
  207. package/src/core/runner/assertions.ts +0 -383
  208. package/src/core/runner/async-pool.ts +0 -108
  209. package/src/core/runner/auth-path.ts +0 -8
  210. package/src/core/runner/ci-context.ts +0 -72
  211. package/src/core/runner/executor.ts +0 -652
  212. package/src/core/runner/expr-eval.ts +0 -41
  213. package/src/core/runner/form-encode.ts +0 -41
  214. package/src/core/runner/http-client.ts +0 -224
  215. package/src/core/runner/learn-drift.ts +0 -293
  216. package/src/core/runner/preflight-vars.ts +0 -153
  217. package/src/core/runner/progress-tracker.ts +0 -73
  218. package/src/core/runner/rate-limiter.ts +0 -185
  219. package/src/core/runner/run-kind.ts +0 -45
  220. package/src/core/runner/schema-validator.ts +0 -308
  221. package/src/core/runner/send-request.ts +0 -232
  222. package/src/core/runner/transforms.ts +0 -65
  223. package/src/core/runner/types.ts +0 -94
  224. package/src/core/secrets/registry.ts +0 -164
  225. package/src/core/secrets/secrets-file.ts +0 -115
  226. package/src/core/selectors/operation-filter.ts +0 -144
  227. package/src/core/setup-api.ts +0 -590
  228. package/src/core/severity/category.ts +0 -94
  229. package/src/core/severity/index.ts +0 -58
  230. package/src/core/spec/infer-schema.ts +0 -102
  231. package/src/core/spec/layers.ts +0 -154
  232. package/src/core/spec/merge-specs.ts +0 -156
  233. package/src/core/spec/schema-from-runs.ts +0 -117
  234. package/src/core/spec/schema-overlay.ts +0 -130
  235. package/src/core/util/ajv.ts +0 -13
  236. package/src/core/util/format-eta.ts +0 -21
  237. package/src/core/util/headers.ts +0 -9
  238. package/src/core/util/url.ts +0 -24
  239. package/src/core/utils.ts +0 -13
  240. package/src/core/workspace/config.ts +0 -129
  241. package/src/core/workspace/fixture-gap-report.ts +0 -84
  242. package/src/core/workspace/fixture-gaps.ts +0 -71
  243. package/src/core/workspace/manifest.ts +0 -283
  244. package/src/core/workspace/output-rotation.ts +0 -62
  245. package/src/core/workspace/root.ts +0 -96
  246. package/src/core/workspace/triage-path.ts +0 -87
  247. package/src/db/lint-runs.ts +0 -47
  248. package/src/db/migrate.ts +0 -128
  249. package/src/db/migrations/0001_run_kind.sql +0 -25
  250. package/src/db/migrations/0002_run_kind_request.sql +0 -59
  251. package/src/db/migrations/sql.d.ts +0 -4
  252. package/src/db/queries/collections.ts +0 -133
  253. package/src/db/queries/coverage.ts +0 -9
  254. package/src/db/queries/dashboard.ts +0 -59
  255. package/src/db/queries/results.ts +0 -216
  256. package/src/db/queries/runs.ts +0 -289
  257. package/src/db/queries/sessions.ts +0 -42
  258. package/src/db/queries/settings.ts +0 -28
  259. package/src/db/queries/types.ts +0 -172
  260. package/src/db/queries.ts +0 -75
  261. package/src/db/schema.ts +0 -298
@@ -1,1162 +0,0 @@
1
- import { dirname } from "path";
2
- import { stat } from "node:fs/promises";
3
- import { parseSafe } from "../../core/parser/yaml-parser.ts";
4
- import { loadEnvironment, loadEnvMeta, loadEnvFile } from "../../core/parser/variables.ts";
5
- import { filterSuitesByTags, excludeSuitesByTags, filterSuitesByMethod, filterSuitesByOperationFilter } from "../../core/parser/filter.ts";
6
- import { preflightCheckVars, formatMissingVarLine, summarizeMissingVars } from "../../core/runner/preflight-vars.ts";
7
- import { runSuite, expandParameterize } from "../../core/runner/executor.ts";
8
- import {
9
- ProgressTracker,
10
- formatProgressLine,
11
- PROGRESS_INTERVAL_MS,
12
- PROGRESS_QUIET_MS,
13
- } from "../../core/runner/progress-tracker.ts";
14
- import { createSchemaValidator } from "../../core/runner/schema-validator.ts";
15
- import { readOpenApiSpec } from "../../core/generator/openapi-reader.ts";
16
- import { createRateLimiter, createAdaptiveRateLimiter } from "../../core/runner/rate-limiter.ts";
17
- import { getReporter, generateJsonReport, generateJunitXml } from "../../core/reporter/index.ts";
18
- import type { ReporterName } from "../../core/reporter/types.ts";
19
- import { resolveOutput, OutputSpecError, type OutputSpec } from "../../core/output/index.ts";
20
- import { writeFile, mkdir } from "node:fs/promises";
21
- import { dirname as pathDirname, resolve as pathResolve } from "node:path";
22
- import type { TestSuite } from "../../core/parser/types.ts";
23
- import type { TestRunResult } from "../../core/runner/types.ts";
24
- import { printError, printWarning } from "../output.ts";
25
- import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
26
- import { getDb } from "../../db/schema.ts";
27
- import { createRun, finalizeRun, saveResults, findCollectionByTestPath } from "../../db/queries.ts";
28
- import { AUTH_PATH_RE } from "../../core/runner/auth-path.ts";
29
- import { resolveCollectionSpec } from "../../core/setup-api.ts";
30
- import { findWorkspaceRoot } from "../../core/workspace/root.ts";
31
- import { existsSync } from "node:fs";
32
- import { buildSpecPointer } from "../../core/diagnostics/spec-pointer.ts";
33
- import { detectStatusDrifts, formatDriftPlan, applyDriftsToTests, appendToleratedDrifts } from "../../core/runner/learn-drift.ts";
34
- import { detectCiContext } from "../../core/runner/ci-context.ts";
35
- import { detectRunKind } from "../../core/runner/run-kind.ts";
36
- import { resolveRateLimit } from "../../core/workspace/config.ts";
37
-
38
- /**
39
- * ARV-117 (m-19): OutputSpec for `zond run`. Three formats; console is
40
- * the default (stdout, rich per-suite stream); `json` / `junit` default
41
- * to stdout but accept `--output <path>` to redirect. None of the
42
- * formats are envelope-wrapped — `--report json` is a test-run report
43
- * (per-suite breakdown), NOT the `{ok, data, errors}` envelope that
44
- * other commands emit for `--json`. That distinction is intentional
45
- * (TASK-134) and remains enforced by the explicit absence of `--json`
46
- * on this command.
47
- */
48
- export const RUN_OUTPUT_SPEC: OutputSpec<unknown> = {
49
- command: "run",
50
- defaultFormat: "console",
51
- formats: {
52
- console: { defaultChannel: "stdout", description: "Rich per-suite stream (default)" },
53
- json: { defaultChannel: "stdout", description: "Per-test JSON breakdown (`generateJsonReport`)" },
54
- junit: { defaultChannel: "stdout", description: "JUnit XML — CI consumption" },
55
- },
56
- };
57
-
58
- export interface RunOptions {
59
- /**
60
- * One or more paths to a YAML file or a directory of YAML files.
61
- * Multi-path: shell-glob expansion (`zond run tests/*.yaml`) — suites from
62
- * every path are merged into a single run. The first path is used as the
63
- * anchor for env-file resolution and DB collection lookup.
64
- */
65
- paths: string[];
66
- env?: string;
67
- report: ReporterName;
68
- timeout?: number;
69
- rateLimit?: number | "auto";
70
- bail: boolean;
71
- /** Run regular suites sequentially (one after another) instead of in parallel. */
72
- sequential?: boolean;
73
- noDb?: boolean;
74
- dbPath?: string;
75
- authToken?: string;
76
- safe?: boolean;
77
- tag?: string[];
78
- excludeTag?: string[];
79
- method?: string;
80
- /** ARV-25: parity with `zond generate`/`zond checks run` — selector
81
- * grammar `<path|method|tag|operation-id>:<value>`, repeatable, OR. */
82
- include?: string[];
83
- /** ARV-25: same grammar as `include`; evaluated after includes. */
84
- exclude?: string[];
85
- envVars?: string[];
86
- /** Hard-fail (exit 2) on undefined {{var}} references instead of warning. */
87
- strictVars?: boolean;
88
- dryRun?: boolean;
89
- json?: boolean;
90
- /** ARV-117: write the report to a file instead of stdout. Replaces
91
- * the legacy `--report-out` flag (no alias — see m-19 lesson §E).
92
- * Resolved through `core/output`'s OutputSpec policy. */
93
- output?: string;
94
- /** Validate every JSON response against the OpenAPI response schema. */
95
- validateSchema?: boolean;
96
- /** Explicit OpenAPI spec path/URL (overrides collection.openapi_spec). */
97
- specPath?: string;
98
- /** ARV-44: --api <name> passed alongside paths — used as the lookup-by-name
99
- * fallback when test-path lookup in DB doesn't yield a spec. Parity with
100
- * ARV-33 (probe mass-assignment / probe security). */
101
- apiName?: string;
102
- /** Group this run under a session id (multi-run campaigns). */
103
- sessionId?: string;
104
- /** TASK-144: per-step retry budget for transient network errors
105
- * (ECONNRESET / EPIPE / socket hang up / fetch failed / abort).
106
- * HTTP statuses are not retried by this path. Default 1, 0 disables. */
107
- retryOnNetwork?: number;
108
- /** TASK-282: detect "passing-test-but-wrong-status" drift and print a plan.
109
- * Implies --validate-schema (requires a spec) so we only flag drift when
110
- * the body matches the OpenAPI schema — the case where retrying with the
111
- * observed status would produce a green test. */
112
- learn?: boolean;
113
- /** TASK-282: actually mutate files instead of printing the plan. Requires
114
- * `learn: true` and a `learnTarget`. */
115
- learnApply?: boolean;
116
- /** TASK-282: where to record the drift — rewrite YAML (`test`) or append to
117
- * apis/<name>/tolerated-drifts.yaml (`drifts`). */
118
- learnTarget?: "test" | "drifts";
119
- /** TASK-265: console reporter emits only the grand-total summary line. */
120
- quiet?: boolean;
121
- /** ARV-72 (feedback round-02 / F14): default true. Set to false (via
122
- * --no-fail-on-failures) to keep exit code 0 even when steps failed —
123
- * useful for advisory runs (audit pre-pass, surface discovery) where
124
- * CI shouldn't break on a single test red. */
125
- failOnFailures?: boolean;
126
- /** ARV-249: hard cap on outgoing HTTP requests across the whole run.
127
- * Once reached, remaining steps short-circuit to `skip` with reason
128
- * `max-requests-cap-reached`. Useful for sampling huge probe-suite
129
- * runs and for CI time-boxing. Each `retry_until` attempt counts as
130
- * one request. */
131
- maxRequests?: number;
132
- }
133
-
134
- /** ARV-249: rough up-front estimate of the total step count for the
135
- * progress reporter. Walks the parameterize cross-product so a 3×4 grid
136
- * on a 50-step suite reports as 600. for_each is dynamic (depends on
137
- * upstream captures) and not counted — the percentage will overshoot a
138
- * little on for_each-heavy suites; acceptable for an ETA. */
139
- function estimateTotalSteps(suites: TestSuite[]): number {
140
- let total = 0;
141
- for (const suite of suites) {
142
- const iters = expandParameterize(suite.parameterize).length || 1;
143
- total += suite.tests.length * iters;
144
- }
145
- return total;
146
- }
147
-
148
- export async function runCommand(options: RunOptions): Promise<number> {
149
- if (options.paths.length === 0) {
150
- printError("No path given");
151
- return 2;
152
- }
153
- const emptyPaths = options.paths.filter((p) => typeof p !== "string" || p.trim().length === 0);
154
- if (emptyPaths.length > 0) {
155
- printError(`Empty path argument (got ${emptyPaths.length} blank entr${emptyPaths.length === 1 ? "y" : "ies"}) — pass a non-empty file or directory path`);
156
- return 2;
157
- }
158
- // ARV-39: strip leading/trailing whitespace from path args so a stray space
159
- // from copy-paste / shell history doesn't produce an ENOENT that quotes
160
- // the rogue character. Mutating options.paths so downstream lookups
161
- // (collection by test_path, env-file search) see the cleaned form.
162
- options.paths = options.paths.map((p) => p.trim());
163
-
164
- // ARV-383: a path that does not exist on disk is a hard error, not an
165
- // empty (exit-0) run. Without this, `parseDirectorySafe` globs a missing
166
- // cwd -> 0 suites and run falls through to the ARV-357 empty-report path,
167
- // reporting a green 0-test "pass". Real-world trigger: a probe that matched
168
- // 0 fields never created its output dir, then `zond run <that-dir>` went
169
- // silently green. An existing-but-empty dir still flows to ARV-357.
170
- const missingPaths: string[] = [];
171
- for (const p of options.paths) {
172
- try {
173
- await stat(p);
174
- } catch {
175
- missingPaths.push(p);
176
- }
177
- }
178
- if (missingPaths.length > 0) {
179
- printError(`No such file or directory: ${missingPaths.join(", ")} — nothing to run`);
180
- return 2;
181
- }
182
-
183
- const primaryPath = options.paths[0]!;
184
-
185
- // 1. Parse test files from every input path (collect parse errors instead
186
- // of silently skipping). Suites from all paths are merged into one run.
187
- let suites: TestSuite[] = [];
188
- const parseErrors: { file: string; error: string }[] = [];
189
- for (const p of options.paths) {
190
- try {
191
- const parsed = await parseSafe(p);
192
- suites.push(...parsed.suites);
193
- parseErrors.push(...parsed.errors);
194
- } catch (err) {
195
- const raw = err instanceof Error ? err.message : String(err);
196
- printError(formatPathError(p, raw));
197
- return 2;
198
- }
199
- }
200
-
201
- for (const pe of parseErrors) {
202
- printWarning(`Skipped ${pe.file}: ${pe.error}`);
203
- }
204
-
205
- if (suites.length === 0) {
206
- const pathList = options.paths.join(", ");
207
- if (parseErrors.length > 0) {
208
- printError(`All ${parseErrors.length} test file(s) in ${pathList} failed to parse`);
209
- return 2;
210
- }
211
- printWarning(`No test files found in ${pathList}`);
212
- // ARV-357: an empty dir used to exit 0 and write NO --output file, so a
213
- // scripted pipeline saw a missing file with no error to key on. Still
214
- // write the requested report (empty JSON `[]` / junit tests=0 envelope)
215
- // so downstream stages parse "zero tests" instead of silently skipping.
216
- if (options.output) {
217
- try {
218
- const spec = resolveOutput(RUN_OUTPUT_SPEC, { report: options.report, output: options.output });
219
- if (spec.channel === "file") {
220
- const content = spec.format === "junit" ? generateJunitXml([]) : generateJsonReport([]);
221
- await mkdir(pathDirname(spec.path!), { recursive: true });
222
- await writeFile(spec.path!, content, "utf-8");
223
- process.stderr.write(`zond: empty report written to ${spec.path!} (0 tests)\n`);
224
- }
225
- } catch (err) {
226
- printError(`Failed to write --output file ${options.output}: ${(err as Error).message}`);
227
- return 2;
228
- }
229
- }
230
- return 0;
231
- }
232
-
233
- // ARV-37: when a selector matches zero suites (typo'd --tag, dead --include
234
- // pattern, etc.), exit non-zero. Previous fail-open let CI builds go green
235
- // for `--tag smok` instead of `smoke`. For --tag we also surface the tags
236
- // actually available so the user can correct without re-reading help.
237
- // 1b0. ARV-25: unified --include/--exclude filter (parity with generate/checks).
238
- // Applied before tag/method filters so it can narrow the scope first.
239
- if ((options.include && options.include.length > 0) || (options.exclude && options.exclude.length > 0)) {
240
- const result = filterSuitesByOperationFilter(suites, options.include ?? [], options.exclude ?? []);
241
- if (result.errors.length > 0) {
242
- for (const err of result.errors) printError(err);
243
- return 2;
244
- }
245
- suites = result.suites;
246
- if (suites.length === 0) {
247
- const parts: string[] = [];
248
- if (options.include?.length) parts.push(`--include [${options.include.join(", ")}]`);
249
- if (options.exclude?.length) parts.push(`--exclude [${options.exclude.join(", ")}]`);
250
- printError(`No tests match ${parts.join(" / ")}`);
251
- return 1;
252
- }
253
- }
254
-
255
- // 1b. Tag filter
256
- if (options.tag && options.tag.length > 0) {
257
- const availableTags = collectAvailableTags(suites);
258
- suites = filterSuitesByTags(suites, options.tag);
259
- if (suites.length === 0) {
260
- const tagHint = availableTags.length > 0
261
- ? ` Available tags: ${availableTags.join(", ")}.`
262
- : " (loaded suites declare no tags.)";
263
- if (parseErrors.length > 0) {
264
- printError(
265
- `No suites match tags [${options.tag.join(", ")}] — but ${parseErrors.length} file(s) failed to parse (see warnings above). Fix parse errors and retry.${tagHint}`
266
- );
267
- return 1;
268
- }
269
- printError(`No suites match tags [${options.tag.join(", ")}].${tagHint}`);
270
- return 1;
271
- }
272
- }
273
-
274
- // 1b2. Exclude-tag filter
275
- if (options.excludeTag && options.excludeTag.length > 0) {
276
- suites = excludeSuitesByTags(suites, options.excludeTag);
277
- if (suites.length === 0) {
278
- printError(`All suites excluded by --exclude-tag [${options.excludeTag.join(", ")}]`);
279
- return 1;
280
- }
281
- }
282
-
283
- // 1b3. Method filter
284
- if (options.method) {
285
- suites = filterSuitesByMethod(suites, options.method);
286
- if (suites.length === 0) {
287
- printError(`No tests found with method ${options.method.toUpperCase()}`);
288
- return 1;
289
- }
290
- }
291
-
292
- // 1c. Safe mode: keep GET, set-only steps, and auth-related requests
293
- if (options.safe) {
294
- for (const suite of suites) {
295
- suite.tests = suite.tests.filter(t => {
296
- if (t.method === "GET" || !t.method) return true;
297
- if (AUTH_PATH_RE.test(t.path)) return true;
298
- return false;
299
- });
300
- }
301
- suites = suites.filter(s => s.tests.length > 0);
302
- if (suites.length === 0) {
303
- printWarning("No safe tests found. Nothing to run in safe mode.");
304
- return 0;
305
- }
306
- }
307
-
308
- // 2. Load environment (resolve collection for scoped envs)
309
- // Use path itself as searchDir if it's a directory; dirname() on a dir path gives the parent
310
- const pathStat = await stat(primaryPath).catch(() => null);
311
- const searchDir = pathStat?.isDirectory() ? primaryPath : dirname(primaryPath);
312
- let collectionForEnv: { id: number } | null = null;
313
- if (!options.noDb) {
314
- try {
315
- getDb(options.dbPath);
316
- collectionForEnv = findCollectionByTestPath(primaryPath);
317
- } catch { /* DB not available — OK */ }
318
- }
319
-
320
- let env: Record<string, string> = {};
321
- try {
322
- env = await loadEnvironment(options.env, searchDir);
323
- } catch (err) {
324
- printError(`Failed to load environment: ${(err as Error).message}`);
325
- return 2;
326
- }
327
-
328
- // Auto-load ./.env.yaml from cwd when --env not given and the searchDir env
329
- // file produced nothing. Useful when running with absolute test paths from
330
- // a collection cwd (e.g. APPLY agents in the auto-loop).
331
- if (!options.env && Object.keys(env).length === 0) {
332
- const cwd = process.cwd();
333
- const cwdEnvPath = `${cwd}/.env.yaml`;
334
- // Avoid double-load if cwd is already covered by searchDir or its parent
335
- const alreadyCovered = cwd === searchDir || cwd === dirname(searchDir);
336
- if (!alreadyCovered) {
337
- try {
338
- const cwdVars = await loadEnvFile(cwdEnvPath);
339
- if (cwdVars && Object.keys(cwdVars).length > 0) {
340
- env = { ...cwdVars };
341
- if (!options.json) {
342
- process.stderr.write(`zond: using ./.env.yaml (cwd fallback)\n`);
343
- }
344
- }
345
- } catch (err) {
346
- printError(`Failed to load environment: ${(err as Error).message}`);
347
- return 2;
348
- }
349
- }
350
- }
351
-
352
- if (options.sessionId && !options.json) {
353
- process.stderr.write(`zond: session ${options.sessionId} (run will be grouped)\n`);
354
- }
355
-
356
- // Inject CLI auth token — overrides env file value
357
- if (options.authToken) {
358
- env.auth_token = options.authToken;
359
- }
360
-
361
- // Inject --env-var KEY=VALUE overrides (highest priority)
362
- if (options.envVars && options.envVars.length > 0) {
363
- for (const pair of options.envVars) {
364
- const eqIdx = pair.indexOf("=");
365
- if (eqIdx > 0) {
366
- env[pair.slice(0, eqIdx)] = pair.slice(eqIdx + 1);
367
- }
368
- }
369
- }
370
-
371
- // Warn if --env was explicitly set but file was not found (empty env)
372
- if (options.env && Object.keys(env).length === 0) {
373
- printWarning(`Environment file .env.${options.env}.yaml not found in ${searchDir}`);
374
- }
375
-
376
- // 3. Apply timeout override
377
- if (options.timeout !== undefined) {
378
- for (const suite of suites) {
379
- suite.config.timeout = options.timeout;
380
- }
381
- }
382
-
383
- // 3b. Resolve rate limit: CLI flag > .env.yaml `rateLimit:` > workspace
384
- // `defaults.rate_limit` (TASK-301) > undefined.
385
- let envRateLimit: number | "auto" | undefined;
386
- try {
387
- envRateLimit = (await loadEnvMeta(options.env, searchDir)).rateLimit;
388
- } catch { /* meta load failure is non-fatal */ }
389
- const rateLimit = resolveRateLimit(options.rateLimit, envRateLimit);
390
- // ARV-64 (feedback round-01 / F4): when no rate-limit was configured
391
- // explicitly, default to an adaptive limiter. Adaptive is a no-op until
392
- // a response carries RateLimit-* headers (RFC 9568) — in which case it
393
- // learns the policy and throttles subsequent requests so a burst can't
394
- // blow through small windows like small windows (e.g. 5 req/s). Without this default
395
- // `zond run` ignored server-published rate-limit headers entirely and
396
- // 22% of a typical sweep landed in 429.
397
- let rateLimiter: ReturnType<typeof createAdaptiveRateLimiter> | undefined;
398
- if (rateLimit === "auto") {
399
- rateLimiter = createAdaptiveRateLimiter();
400
- } else if (rateLimit !== undefined) {
401
- rateLimiter = createRateLimiter(rateLimit);
402
- } else {
403
- rateLimiter = createAdaptiveRateLimiter();
404
- }
405
-
406
- // 3c. Resolve OpenAPI spec. Explicit --spec wins; otherwise fall back to the
407
- // collection record. The doc is reused for --validate-schema (TASK-50) and
408
- // for spec_pointer/spec_excerpt frozen evidence (TASK-102).
409
- let schemaValidator: ReturnType<typeof createSchemaValidator> | undefined;
410
- let openApiDoc: unknown | undefined;
411
- {
412
- let specPath = options.specPath;
413
- if (!specPath) {
414
- try {
415
- const collection = findCollectionByTestPath(primaryPath);
416
- if (collection?.openapi_spec) specPath = resolveCollectionSpec(collection.openapi_spec);
417
- } catch { /* DB not available — fall through */ }
418
- }
419
- // ARV-44: parity with ARV-33 (probe mass-assignment / security). When the
420
- // user passed --api <name> together with a path, the test-path lookup
421
- // above may miss (path normalisation, alias, --all merge). Fall back to
422
- // lookup-by-name, then to apis/<name>/spec.json on disk.
423
- if (!specPath && options.apiName) {
424
- try {
425
- const byName = resolveApiCollection(options.apiName, options.dbPath);
426
- if (!("error" in byName) && byName.spec) specPath = byName.spec;
427
- } catch { /* fall through to disk probe */ }
428
- if (!specPath) {
429
- try {
430
- const ws = findWorkspaceRoot();
431
- const onDisk = pathResolve(ws.root, "apis", options.apiName, "spec.json");
432
- if (existsSync(onDisk)) specPath = onDisk;
433
- } catch { /* workspace not initialised — give up silently */ }
434
- }
435
- }
436
- // ARV-209 (R12/F11): when --validate-schema is requested but no --api was
437
- // passed (explicit suite path suppresses current-api fallback at the
438
- // argv-parse level), still try to derive the spec from the test path
439
- // pattern `apis/<name>/tests/...`. SKILL.md DEPTH-PASS examples use that
440
- // exact shape and expect the spec to resolve automatically.
441
- if (!specPath && primaryPath) {
442
- const m = primaryPath.replace(/\\/g, "/").match(/(?:^|\/)apis\/([^\/]+)\/tests(?:\/|$)/);
443
- if (m) {
444
- const apiName = m[1]!;
445
- try {
446
- const byName = resolveApiCollection(apiName, options.dbPath);
447
- if (!("error" in byName) && byName.spec) specPath = byName.spec;
448
- } catch { /* fall through to disk probe */ }
449
- if (!specPath) {
450
- try {
451
- const ws = findWorkspaceRoot();
452
- const onDisk = pathResolve(ws.root, "apis", apiName, "spec.json");
453
- if (existsSync(onDisk)) specPath = onDisk;
454
- } catch { /* give up silently */ }
455
- }
456
- }
457
- }
458
- if (specPath) {
459
- try {
460
- openApiDoc = await readOpenApiSpec(specPath);
461
- } catch (err) {
462
- if (options.validateSchema) {
463
- printError(`Failed to load OpenAPI spec '${specPath}': ${(err as Error).message}`);
464
- return 2;
465
- }
466
- // spec_pointer is best-effort — non-fatal when spec can't be loaded.
467
- printWarning(`Failed to load OpenAPI spec '${specPath}' for spec_pointer evidence: ${(err as Error).message}`);
468
- }
469
- }
470
- // TASK-282: --learn requires schema validation evidence (status mismatch
471
- // is only a "drift" if the body actually matches the OpenAPI contract —
472
- // otherwise we'd silently encourage masking a real schema bug).
473
- const needsSchema = options.validateSchema || options.learn;
474
- if (needsSchema) {
475
- if (!openApiDoc) {
476
- const flag = options.learn ? "--learn" : "--validate-schema";
477
- printError(
478
- `${flag} requires --spec <path|url> or a collection with openapi_spec set. ` +
479
- `Pass \`--api <name>\` (resolves apis/<name>/spec.json) or add \`--spec apis/<name>/spec.json\` explicitly.`,
480
- );
481
- return 2;
482
- }
483
- schemaValidator = createSchemaValidator(openApiDoc as Parameters<typeof createSchemaValidator>[0]);
484
- }
485
- }
486
-
487
- // TASK-282: validate --learn flag combinations early (before run).
488
- // ARV-199: --learn-apply implies --learn — cf. `git stash apply` (no
489
- // explicit `git stash` toggle required). The earlier hard error here
490
- // tripped up scripted callers that only thought to pass --learn-apply.
491
- if (options.learnApply && !options.learn) {
492
- options.learn = true;
493
- }
494
- if (options.learnApply && !options.learnTarget) {
495
- printError("--learn-apply requires --learn-target=test or --learn-target=drifts");
496
- return 2;
497
- }
498
-
499
- // ARV-249: shared HTTP-request budget. `Infinity` means uncapped.
500
- const requestBudget = options.maxRequests !== undefined && options.maxRequests > 0
501
- ? { limit: options.maxRequests, used: 0 }
502
- : undefined;
503
-
504
- // ARV-249: progress reporter. Enabled when stderr is a TTY and the user
505
- // hasn't opted out via --quiet. Suppressed for non-interactive output
506
- // (CI logs already track timestamps; an extra line every 5s is noise).
507
- const progressEnabled = !options.quiet
508
- && !options.dryRun
509
- && Boolean((process.stderr as { isTTY?: boolean }).isTTY);
510
- const totalStepsForProgress = progressEnabled
511
- ? estimateTotalSteps(suites)
512
- : 0;
513
- const tracker = progressEnabled
514
- ? new ProgressTracker(totalStepsForProgress)
515
- : undefined;
516
- let lastProgressLineLen = 0;
517
- const writeProgressLine = (final: boolean): void => {
518
- if (!tracker) return;
519
- const snap = tracker.snapshot();
520
- // Hold off on the first emit until the run has actually been running
521
- // a while — short test suites should stay silent.
522
- if (!final && snap.elapsedMs < PROGRESS_QUIET_MS) return;
523
- const line = formatProgressLine(snap);
524
- // Overwrite previous line in place (single TTY row, no scroll).
525
- const pad = " ".repeat(Math.max(0, lastProgressLineLen - line.length));
526
- process.stderr.write(`\r${line}${pad}${final ? "\n" : ""}`);
527
- lastProgressLineLen = line.length;
528
- };
529
- let progressInterval: ReturnType<typeof setInterval> | undefined;
530
- if (tracker) {
531
- progressInterval = setInterval(() => writeProgressLine(false), PROGRESS_INTERVAL_MS);
532
- // Don't let the timer pin the event loop alive past run completion.
533
- (progressInterval as { unref?: () => void }).unref?.();
534
- }
535
-
536
- const runOpts = {
537
- rateLimiter,
538
- schemaValidator,
539
- networkRetries: options.retryOnNetwork,
540
- requestBudget,
541
- onStepDone: tracker
542
- ? (step: import("../../core/runner/types.ts").StepResult) => tracker.recordStep(step)
543
- : undefined,
544
- };
545
-
546
- // 4. Run suites — setup suites run first (sequentially), their captures flow into regular suites
547
- const results: TestRunResult[] = [];
548
- const dryRun = options.dryRun === true;
549
-
550
- const setupSuites = suites.filter(s => s.setup);
551
- const regularSuites = suites.filter(s => !s.setup);
552
- const setupCaptures: Record<string, string> = {};
553
-
554
- // 3d. Pre-flight variable check on setup suites — only `env` is known
555
- // (their captures don't exist yet).
556
- {
557
- const setupHits = preflightCheckVars(setupSuites, env);
558
- emitMissingVarWarnings(setupHits);
559
- if (options.strictVars && setupHits.length > 0) {
560
- printError(`--strict-vars: ${setupHits.length} undefined variable reference(s) in setup suites`);
561
- return 2;
562
- }
563
- }
564
-
565
- for (const suite of setupSuites) {
566
- const result = await runSuite(suite, env, dryRun, runOpts);
567
- results.push(result);
568
- for (const step of result.steps) {
569
- for (const [k, v] of Object.entries(step.captures)) {
570
- setupCaptures[k] = String(v);
571
- }
572
- }
573
- }
574
-
575
- const enrichedEnv = { ...env, ...setupCaptures };
576
-
577
- // 3e. Pre-flight variable check on regular suites — env + setup captures
578
- // are known producers; per-suite captures/sets/parameterize handled inside.
579
- {
580
- const hits = preflightCheckVars(regularSuites, enrichedEnv);
581
- emitMissingVarWarnings(hits);
582
- if (options.strictVars && hits.length > 0) {
583
- printError(`--strict-vars: ${hits.length} undefined variable reference(s)`);
584
- return 2;
585
- }
586
- }
587
-
588
- if (options.bail) {
589
- // Sequential with bail at suite level
590
- for (const suite of regularSuites) {
591
- const result = await runSuite(suite, enrichedEnv, dryRun, runOpts);
592
- results.push(result);
593
- if (!dryRun && (result.failed > 0 || result.steps.some((s) => s.status === "error"))) {
594
- break;
595
- }
596
- }
597
- } else if (options.sequential) {
598
- // Sequential without bail — run suites one by one
599
- for (const suite of regularSuites) {
600
- const result = await runSuite(suite, enrichedEnv, dryRun, runOpts);
601
- results.push(result);
602
- }
603
- } else {
604
- // Parallel
605
- const all = await Promise.all(regularSuites.map((suite) => runSuite(suite, enrichedEnv, dryRun, runOpts)));
606
- results.push(...all);
607
- }
608
-
609
- // ARV-249: stop the progress tracker before any reporter output starts
610
- // — overlapping carriage returns and report lines garble the terminal.
611
- if (progressInterval !== undefined) {
612
- clearInterval(progressInterval);
613
- // Clear the in-place progress line so it doesn't linger above the report.
614
- if (lastProgressLineLen > 0) {
615
- process.stderr.write(`\r${" ".repeat(lastProgressLineLen)}\r`);
616
- lastProgressLineLen = 0;
617
- }
618
- }
619
-
620
- // 5. Collect warnings
621
- const warnings: string[] = [];
622
- const rateLimited = results.flatMap(r => r.steps)
623
- .filter(s => s.response?.status === 429);
624
- if (rateLimited.length > 0) {
625
- warnings.push(`${rateLimited.length} request(s) hit rate limit (429). Consider: consolidating login steps, adding --bail, or using retry_until with delay.`);
626
- }
627
- // ARV-249: surface --max-requests cap when it actually fired.
628
- if (requestBudget && requestBudget.used >= requestBudget.limit) {
629
- const cappedSteps = results.flatMap(r => r.steps)
630
- .filter(s => s.status === "skip" && s.error === "max-requests-cap-reached").length;
631
- if (cappedSteps > 0) {
632
- warnings.push(`--max-requests ${requestBudget.limit} cap reached; ${cappedSteps} subsequent step(s) skipped. Raise the cap or narrow the suite to cover them.`);
633
- }
634
- }
635
-
636
- // 5b. Report — ARV-117: route through core/output's OutputSpec so
637
- // `--report <format>` + `--output <path>` follow the same policy as
638
- // every other command. `--output` (was `--report-out`) is honoured for
639
- // any format; with `console` format it falls back to JSON in the file
640
- // (most useful), matching prior behaviour.
641
- let resolvedOutput;
642
- try {
643
- resolvedOutput = resolveOutput(RUN_OUTPUT_SPEC, {
644
- report: options.report,
645
- output: options.output,
646
- });
647
- } catch (err) {
648
- if (err instanceof OutputSpecError) {
649
- printError(err.message);
650
- return 2;
651
- }
652
- throw err;
653
- }
654
- if (!options.json) {
655
- if (resolvedOutput.channel === "file") {
656
- const outPath = resolvedOutput.path!;
657
- let content: string;
658
- let label: string;
659
- switch (resolvedOutput.format) {
660
- case "junit":
661
- content = generateJunitXml(results);
662
- label = "JUnit XML";
663
- break;
664
- case "json":
665
- default: // "console" — fall back to JSON in the file (most useful)
666
- content = generateJsonReport(results);
667
- label = "JSON";
668
- break;
669
- }
670
- try {
671
- await mkdir(pathDirname(outPath), { recursive: true });
672
- await writeFile(outPath, content, "utf-8");
673
- process.stderr.write(`zond: ${label} report written to ${outPath}\n`);
674
- } catch (err) {
675
- printError(`Failed to write --output file ${outPath}: ${(err as Error).message}`);
676
- return 2;
677
- }
678
- for (const w of warnings) {
679
- printWarning(w);
680
- }
681
- } else {
682
- const reporter = getReporter(options.report);
683
- reporter.report(results, options.quiet ? { quiet: true } : undefined);
684
- // TASK-265: --quiet drops the warnings tail too — they are non-essential
685
- // run hints (deprecation, timing). Errors still reach stderr via the
686
- // dedicated path; --strict-vars still aborts on undefined refs before
687
- // we get here.
688
- if (!options.quiet) {
689
- for (const w of warnings) {
690
- printWarning(w);
691
- }
692
- }
693
- }
694
- }
695
-
696
- // 5b. Resolve spec_pointer + spec_excerpt for steps with provenance.
697
- // Frozen evidence: pointer and excerpt are computed once, against the spec
698
- // doc loaded at run time, and saved into the DB so later spec edits don't
699
- // rewrite history.
700
- if (openApiDoc) {
701
- for (const r of results) {
702
- for (const s of r.steps) {
703
- if (!s.provenance) continue;
704
- const ptr = buildSpecPointer(s.provenance, openApiDoc);
705
- if (ptr) {
706
- s.spec_pointer = ptr.pointer;
707
- s.spec_excerpt = ptr.excerpt;
708
- }
709
- }
710
- }
711
- }
712
-
713
- // 5c. TASK-282: --learn — surface or apply status-code drift.
714
- if (options.learn) {
715
- const drifts = detectStatusDrifts(results, { schemaValidatorAttached: schemaValidator !== undefined });
716
- if (!options.learnApply) {
717
- process.stderr.write(formatDriftPlan(drifts));
718
- } else if (drifts.length === 0) {
719
- process.stderr.write("zond: --learn-apply: no drift to apply\n");
720
- } else if (options.learnTarget === "test") {
721
- const applied = await applyDriftsToTests(drifts);
722
- process.stderr.write(`zond: --learn-apply --learn-target=test: rewrote ${applied.updated} step(s)\n`);
723
- for (const e of applied.errors) {
724
- printWarning(`learn-apply: ${e.suite_file} step "${e.step_name}": ${e.reason}`);
725
- }
726
- } else if (options.learnTarget === "drifts") {
727
- // Resolve apis/<name>/ from the primary path's collection record.
728
- let apiDir: string | undefined;
729
- try {
730
- const collection = findCollectionByTestPath(primaryPath);
731
- if (collection?.base_dir) apiDir = collection.base_dir;
732
- else if (collection?.test_path) apiDir = dirname(collection.test_path);
733
- } catch { /* DB unavailable */ }
734
- if (!apiDir) {
735
- printError("--learn-target=drifts: cannot resolve apis/<name>/ — collection not registered (run `zond add api <name>`)");
736
- return 2;
737
- }
738
- const written = await appendToleratedDrifts(apiDir, drifts);
739
- process.stderr.write(`zond: --learn-apply --learn-target=drifts: wrote ${written.written} entry(ies) to ${written.file}\n`);
740
- }
741
- }
742
-
743
- // 6. Save to DB
744
- let savedRunId: number | undefined;
745
- if (!options.noDb) {
746
- try {
747
- getDb(options.dbPath);
748
- const collection = findCollectionByTestPath(primaryPath);
749
- // TASK-274: capture suite-level tags executed in this run (plus any
750
- // explicit --tag filter). Stored on the run row so
751
- // `coverage --union tag:<x>` can later select runs by tag.
752
- const tagSet = new Set<string>();
753
- for (const s of suites) {
754
- for (const t of s.tags ?? []) tagSet.add(t);
755
- }
756
- for (const t of options.tag ?? []) tagSet.add(t);
757
- const tags = [...tagSet].sort();
758
- // TASK-116: stamp CI context on the run row when running under a
759
- // detected CI environment (or when the user passed an explicit
760
- // `--trigger ci`). Manual runs default to trigger=manual with no
761
- // commit/branch — preserving prior behaviour.
762
- const ci = detectCiContext();
763
- // ARV-55: classify the run by what kinds of suite files it executed
764
- // *before* INSERT, so coverage's default query becomes a simple
765
- // run_kind='regular' compare instead of a per-result regex scan.
766
- const runKind = detectRunKind(suites.map((s) => s.filePath ?? null));
767
- savedRunId = createRun({
768
- started_at: results[0]?.started_at ?? new Date().toISOString(),
769
- environment: options.env,
770
- collection_id: collection?.id,
771
- session_id: options.sessionId,
772
- trigger: ci.trigger,
773
- commit_sha: ci.commit_sha ?? undefined,
774
- branch: ci.branch ?? undefined,
775
- ...(tags.length > 0 ? { tags } : {}),
776
- run_kind: runKind,
777
- });
778
- finalizeRun(savedRunId, results);
779
- saveResults(savedRunId, results);
780
- } catch (err) {
781
- printWarning(`Failed to save results to DB: ${(err as Error).message}`);
782
- }
783
- }
784
-
785
- // 7. Exit code (always 0 in dry-run mode)
786
- if (dryRun) {
787
- if (options.json) {
788
- printJson(jsonOk("run", { summary: { total: results.length, passed: 0, failed: 0 }, dryRun: true }));
789
- }
790
- return 0;
791
- }
792
- const hasFailures = results.some((r) => r.failed > 0 || r.steps.some((s) => s.status === "error"));
793
-
794
- // ARV-105 (F10): a suite where every step was skipped (e.g. probe-emitted
795
- // regression suite that needs an unfilled capture-chain var) reports
796
- // total>0, passed=0, failed=0, skipped=total. Without surfacing this,
797
- // CI sees "0 failed" and the run looks green even though nothing was
798
- // actually tested. Compute the list once and expose it in both the
799
- // JSON envelope and the stderr tail.
800
- const allSkippedSuites = results
801
- .filter(r => r.total > 0 && r.passed === 0 && r.failed === 0 && r.skipped === r.total)
802
- .map(r => ({
803
- suite: r.suite_name,
804
- ...(r.suite_file ? { file: r.suite_file } : {}),
805
- total: r.total,
806
- // Sample skip reason from the first step — usually "missing variable
807
- // {{X}}" or similar. Helps the operator route to fixtures vs spec.
808
- first_skip_reason: r.steps[0]?.error ?? null,
809
- }));
810
-
811
- if (options.json) {
812
- const total = results.reduce((s, r) => s + r.total, 0);
813
- const passed = results.reduce((s, r) => s + r.passed, 0);
814
- const failed = results.reduce((s, r) => s + r.failed, 0);
815
- const failures = results.flatMap(r =>
816
- r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
817
- suite: r.suite_name,
818
- test: s.name,
819
- ...(r.suite_file ? { file: r.suite_file } : {}),
820
- status: s.status,
821
- ...(typeof s.response?.status === "number" ? { http_status: s.response.status } : {}),
822
- ...(typeof s.response?.status === "number" && s.response.status >= 500 && s.response.status < 600 ? { is_5xx: true } : {}),
823
- error: s.error,
824
- ...(s.failure_class ? { failure_class: s.failure_class, failure_class_reason: s.failure_class_reason } : {}),
825
- ...(s.provenance ? { provenance: s.provenance } : {}),
826
- ...(s.spec_pointer ? { spec_pointer: s.spec_pointer, spec_excerpt: s.spec_excerpt } : {}),
827
- ...(s.network_retry ? { network_retry: s.network_retry } : {}),
828
- }))
829
- );
830
- const fiveXx = failures.filter(f => f.is_5xx).length;
831
- printJson(jsonOk("run", {
832
- summary: { total, passed, failed, fiveXx, allSkippedSuites: allSkippedSuites.length },
833
- failures,
834
- ...(allSkippedSuites.length > 0 ? { all_skipped_suites: allSkippedSuites } : {}),
835
- warnings,
836
- runId: savedRunId,
837
- }));
838
- }
839
-
840
- // ARV-105 (F10): non-json — name the all-skipped suites on stderr so a
841
- // tail-eyeballing operator notices the visibility-pitfall. Doesn't gate
842
- // exit code (skipping isn't a failure) but is loud enough that a green
843
- // "0 failed" can no longer hide a regression suite that ran zero steps.
844
- if (allSkippedSuites.length > 0 && !options.json) {
845
- process.stderr.write(`zond: ${allSkippedSuites.length} suite(s) ran with every step skipped (no test executed — likely missing fixtures or capture-chain ids).\n`);
846
- for (const s of allSkippedSuites) {
847
- const reason = s.first_skip_reason ? ` — ${s.first_skip_reason}` : "";
848
- process.stderr.write(` - ${s.suite}${s.file ? ` (${s.file})` : ""}: ${s.total} step(s) skipped${reason}\n`);
849
- }
850
- }
851
-
852
- // ARV-162 (round-08 F19): when suites failed parse-time validation
853
- // (`zond check tests` reject — e.g. form value emitted unquoted, parsed
854
- // as int), per-file warnings already went to stderr at the top of the
855
- // run, but they easily get lost in long output. Add a loud trailing
856
- // summary so "47/68 ran" doesn't silently hide 21 invalid files.
857
- if (parseErrors.length > 0 && !options.json) {
858
- process.stderr.write(
859
- `zond: ${parseErrors.length} test file(s) skipped due to validation errors — ` +
860
- `run \`zond check tests <path>\` to see why. Coverage numbers below exclude these files.\n`,
861
- );
862
- }
863
-
864
- // ARV-72 (feedback round-02 / F14): make the exit-code → failures
865
- // mapping visible. Tester reported "545 failed, exit_code=0" which is
866
- // not what this function returns — but the symptom is real: the reader
867
- // can't tell whether the surrounding shell or a wrapper script swallowed
868
- // the non-zero exit. Print a one-line tail to stderr that names the
869
- // exit code so wrapper scripts that hide it become obvious. Skipped in
870
- // --json so the JSON envelope stays alone on stdout (this is stderr
871
- // anyway, but skip avoids confusing parsers that capture both streams).
872
- if (hasFailures && !options.json) {
873
- // ARV-318: error steps (env_issue/network — couldn't execute) also drive
874
- // the non-zero exit (see hasFailures above). The old line counted only
875
- // `failed`, so an all-errored run printed "0 test step(s) failed —
876
- // exiting with code 1", a contradiction. Name both classes.
877
- const failed = results.reduce((s, r) => s + r.failed, 0);
878
- const errored = results.reduce((s, r) => s + r.steps.filter((st) => st.status === "error").length, 0);
879
- const parts: string[] = [];
880
- if (failed > 0) parts.push(`${failed} step(s) failed`);
881
- if (errored > 0) parts.push(`${errored} step(s) errored (couldn't execute — see failure_class)`);
882
- process.stderr.write(`zond: ${parts.join(", ")} — exiting with code 1 (pass --no-fail-on-failures to suppress, e.g. for advisory runs).\n`);
883
- }
884
-
885
- if (hasFailures && options.failOnFailures === false) {
886
- return 0;
887
- }
888
- return hasFailures ? 1 : 0;
889
- }
890
-
891
- import type { Command } from "commander";
892
- import { Option } from "commander";
893
- import { resolveApiCollection } from "../resolve.ts";
894
- import { collect, flatSplit, parseNonNegativeInt, parsePositiveInt, parseRateLimit, parseReporter } from "../argv.ts";
895
- import { resolveSessionId } from "../../core/context/session.ts";
896
- import { getApi } from "../util/api-context.ts";
897
- import { readdirSync, statSync } from "node:fs";
898
- import { join } from "node:path";
899
-
900
- /**
901
- * TASK-248: when 4+ hits collapse them into one summary line per unique
902
- * variable name. Below threshold the per-(suite,step) form still helps
903
- * users locate the missing reference.
904
- */
905
- function emitMissingVarWarnings(hits: import("../../core/runner/preflight-vars.ts").MissingVarHit[]): void {
906
- if (hits.length === 0) return;
907
- if (hits.length < 4) {
908
- for (const hit of hits) printWarning(formatMissingVarLine(hit));
909
- return;
910
- }
911
- for (const line of summarizeMissingVars(hits)) printWarning(line);
912
- }
913
-
914
- /**
915
- * ARV-39: rewrite an ENOENT bubbling up from `parseSafe` so the user sees
916
- * a clean path quote and an actionable hint. Bun's raw glob/open error
917
- * quotes the path with a trailing space ("'foo.yaml '"), which makes users
918
- * think the path itself has a typo. We strip the noise and, when the file's
919
- * parent directory exists, suggest either the dir form (`apis/<api>/tests`)
920
- * or list its YAML siblings so the right invocation is discoverable.
921
- */
922
- function formatPathError(path: string, raw: string): string {
923
- const isEnoent = /ENOENT/i.test(raw) || /no such file or directory/i.test(raw);
924
- if (!isEnoent) return raw;
925
- const cleanPath = path.trim();
926
- const parent = pathDirname(cleanPath);
927
- const lines: string[] = [`Path not found: ${cleanPath}`];
928
- // Parent directory exists → list known YAML suites to make the right
929
- // invocation obvious. Parent missing → just say so; nothing useful to add.
930
- try {
931
- if (parent && existsSync(parent) && statSync(parent).isDirectory()) {
932
- const siblings = readdirSync(parent)
933
- .filter((f) => /\.ya?ml$/i.test(f))
934
- .sort();
935
- if (siblings.length === 0) {
936
- lines.push(`(${parent}/ has no YAML files — did you forget to run \`zond generate\`?)`);
937
- } else {
938
- // Prefer suggesting the directory itself — that's the parity command
939
- // most users actually want ("run all suites under apis/<api>/tests").
940
- lines.push(`Did you mean \`zond run ${parent}\` (runs ${siblings.length} suite${siblings.length === 1 ? "" : "s"})?`);
941
- const preview = siblings.slice(0, 6).map((s) => ` - ${parent}/${s}`).join("\n");
942
- const more = siblings.length > 6 ? `\n … ${siblings.length - 6} more` : "";
943
- lines.push(`Known suites in ${parent}/:\n${preview}${more}`);
944
- }
945
- }
946
- } catch { /* readdir / stat failed — fall through with the basic message */ }
947
- return lines.join("\n");
948
- }
949
-
950
- /**
951
- * ARV-37: list distinct tags across loaded suites so a fail-loud `--tag`
952
- * mismatch can suggest the user-facing values without forcing them into
953
- * `--help`. Order is alphabetical for stable output across runs.
954
- */
955
- function collectAvailableTags(suites: { tags?: string[] }[]): string[] {
956
- const seen = new Set<string>();
957
- for (const s of suites) {
958
- if (!s.tags) continue;
959
- for (const t of s.tags) {
960
- const trimmed = t.trim();
961
- if (trimmed) seen.add(trimmed);
962
- }
963
- }
964
- return [...seen].sort();
965
- }
966
-
967
- /**
968
- * TASK-116: discover every `apis/<name>/tests/` directory in the workspace
969
- * for `zond run --all`. Skips entries without a tests/ subdir (some APIs
970
- * may have probes only). Returns absolute paths so the run command resolves
971
- * env files correctly even when invoked from a subdirectory.
972
- */
973
- function discoverWorkspaceTestPaths(): { paths: string[] } | { error: string } {
974
- let root: string;
975
- try {
976
- const ws = findWorkspaceRoot();
977
- if (ws.fromFallback) {
978
- return { error: "--all requires a workspace marker (zond.config.yml). Run `zond init` first." };
979
- }
980
- root = ws.root;
981
- } catch (err) {
982
- return { error: `Failed to locate workspace root: ${(err as Error).message}` };
983
- }
984
-
985
- const apisDir = join(root, "apis");
986
- if (!existsSync(apisDir)) return { paths: [] };
987
-
988
- const out: string[] = [];
989
- for (const entry of readdirSync(apisDir, { withFileTypes: true })) {
990
- if (!entry.isDirectory()) continue;
991
- const testsDir = join(apisDir, entry.name, "tests");
992
- if (!existsSync(testsDir)) continue;
993
- try {
994
- if (statSync(testsDir).isDirectory()) out.push(testsDir);
995
- } catch { /* skip unreadable */ }
996
- }
997
- return { paths: out.sort() };
998
- }
999
-
1000
- export function registerRun(program: Command): void {
1001
- program
1002
- .command("run [paths...]")
1003
- .description(
1004
- "Run API tests. Accepts one or more file/dir paths (shell-glob friendly: zond run tests/*.yaml). " +
1005
- "TASK-134: this command does NOT accept `--json` (which would collide with `--report json`). " +
1006
- "For JSON-envelope-style output use `--report json` (per-test breakdown) or `--report junit`. " +
1007
- "Other commands (`request`, `coverage`, `db diagnose`, etc.) DO accept `--json` — there it returns " +
1008
- "a small `{ok, data, errors}` envelope. The two flags are intentionally distinct: " +
1009
- "`run --report json` is a test-run report, `<cmd> --json` is a query-result envelope.",
1010
- )
1011
- .option("--env <name>", "Use environment file (.env.<name>.yaml)")
1012
- .option("--api <name>", "Use API collection (resolves test path automatically)")
1013
- .addOption(
1014
- new Option("--report <format>", "Output format")
1015
- .choices(["console", "json", "junit"])
1016
- .default("console")
1017
- .argParser(parseReporter),
1018
- )
1019
- .option("--timeout <ms>", "Override request timeout", parsePositiveInt("--timeout"))
1020
- .option("--rate-limit <N|auto>", "Throttle requests to at most N per second, or `auto` to adapt from ratelimit-* response headers. Default: adaptive (ARV-64) — no-op until the server publishes RateLimit-* headers, then paces requests automatically. Pass a number for hard caps; `off` is not supported (set --workers 1 + no flag for sequential).", parseRateLimit)
1021
- .option("--bail", "Stop on first suite failure")
1022
- .option("--sequential", "Run regular suites one after another instead of in parallel (opt-out of Promise.all)")
1023
- .option("--all", "TASK-116: discover every apis/<name>/tests/ directory in the workspace and merge them into a single run row (one runs.id per CI invocation, even with multiple registered APIs). Implies CI-style aggregation; pairs with auto-detected commit_sha / branch / trigger=ci.")
1024
- .option("--no-db", "Do not save results to .zond/zond.db")
1025
- .option("--db <path>", "Path to SQLite database file (default: .zond/zond.db)")
1026
- .option("--auth-token <token>", "Auth token injected as {{auth_token}} variable")
1027
- .option("--safe", "Run only GET tests (read-only, safe mode)")
1028
- .option("--tag <tag>", "Filter suites by tag (repeatable, comma-separated)", collect, [])
1029
- .option("--exclude-tag <tag>", "Exclude suites by tag (repeatable, comma-separated)", collect, [])
1030
- .option("--method <method>", "Filter tests by HTTP method (e.g. GET, POST)")
1031
- .option(
1032
- "--include <spec...>",
1033
- "ARV-25: keep only steps matching <selector>:<value> (path|method|tag|operation-id). Same grammar as `zond generate` / `zond checks run`. Repeatable, combines with OR.",
1034
- )
1035
- .option(
1036
- "--exclude <spec...>",
1037
- "ARV-25: drop steps matching <selector>:<value>. Same grammar as --include. Excludes evaluated after includes.",
1038
- )
1039
- .option("--env-var <KEY=VALUE>", "Inject env variable (repeatable, overrides env file)", collect, [])
1040
- .option("--strict-vars", "Hard-fail (exit 2) when a {{var}} reference has no producer (default: warn and continue)")
1041
- .option("--dry-run", "Show requests without sending them (exit code always 0)")
1042
- .option(
1043
- "--quiet",
1044
- "TASK-265: emit only the grand-total summary line — drops per-suite/per-test detail and warning footers. Exit code (0/1) still differentiates pass/fail. For CI logs and watcher loops where step-level output is noise.",
1045
- )
1046
- .option("--output <file>", "ARV-117: write the report to a file instead of stdout. Replaces the legacy --report-out flag. With --report console, falls back to JSON in the file (machine-parseable). Path is resolved relative to cwd; parent directories are auto-created.")
1047
- .option("--validate-schema", "Validate JSON responses against the OpenAPI schema (recommended for CRUD runs — catches contract drift like date-format and enum mismatches; requires --spec or a collection with openapi_spec set)")
1048
- .option("--spec <path>", "Path or URL to OpenAPI spec used for --validate-schema (overrides the collection's openapi_spec)")
1049
- .option("--session-id <id>", "Group this run under a session. Resolution order: --session-id flag > ZOND_SESSION_ID env > .zond/current-session file (set by 'zond session start')")
1050
- .option("--learn", "TASK-282: detect status-code drift (passing-test-but-wrong-status). Implies --validate-schema. Without --learn-apply prints the plan; combine with --learn-apply --learn-target=test|drifts to mutate.")
1051
- .option("--learn-apply", "Apply the drift plan instead of printing it. Implies --learn; still requires --learn-target=test|drifts.")
1052
- .addOption(
1053
- new Option("--learn-target <where>", "What to mutate when --learn-apply is set: rewrite expect.status in YAML (test) or append to apis/<name>/tolerated-drifts.yaml (drifts)")
1054
- .choices(["test", "drifts"]),
1055
- )
1056
- .option(
1057
- "--retry-on-network <N>",
1058
- "Auto-retry on transient network errors (ECONNRESET, EPIPE, socket hang up, fetch failed, timeout) — HTTP status codes (incl. 5xx) are NOT retried. Exponential backoff with jitter, base 250ms. Default 1, 0 disables.",
1059
- parseNonNegativeInt("--retry-on-network"),
1060
- 1,
1061
- )
1062
- .option(
1063
- "--no-fail-on-failures",
1064
- "ARV-72: keep exit code 0 even when steps failed (advisory runs). Default: exit 1 on any failure. The stderr tail still names the count for visibility.",
1065
- )
1066
- .option(
1067
- "--max-requests <N>",
1068
- "ARV-249: hard cap on outgoing HTTP requests across the whole run. Once reached, remaining steps short-circuit to `skip` with reason `max-requests-cap-reached`. Each retry_until attempt counts as one request. Useful for sampling huge probe-suite runs and for CI time-boxing.",
1069
- parsePositiveInt("--max-requests"),
1070
- )
1071
- .action(async (pathArgs: string[] | undefined, opts, cmd: Command) => {
1072
- let paths = pathArgs ?? [];
1073
- // ARV-53: explicit paths or --all suppress the current-API fallback —
1074
- // `run path/to/test.yaml` should never silently pick up `.zond/current-api`.
1075
- // Otherwise resolve via cli/util/api-context.ts.
1076
- const apiFlag = (paths.length > 0 || opts.all === true)
1077
- ? (opts.api as string | undefined)
1078
- : getApi(cmd, opts);
1079
- const dbPath = typeof opts.db === "string" ? opts.db : undefined;
1080
-
1081
- // TASK-116: --all expands to every apis/<name>/tests/ directory in the
1082
- // workspace, merging them into a single run row.
1083
- if (opts.all === true) {
1084
- const discovered = discoverWorkspaceTestPaths();
1085
- if ("error" in discovered) {
1086
- printError(discovered.error);
1087
- process.exitCode = 2;
1088
- return;
1089
- }
1090
- if (discovered.paths.length === 0) {
1091
- printError("--all found no apis/<name>/tests/ directories in the workspace. Run `zond add api <name>` and `zond generate` first.");
1092
- process.exitCode = 1;
1093
- return;
1094
- }
1095
- paths = discovered.paths;
1096
- }
1097
-
1098
- if (paths.length === 0 && apiFlag) {
1099
- const resolved = resolveApiCollection(apiFlag, dbPath);
1100
- if ("error" in resolved) {
1101
- printError(resolved.error);
1102
- process.exitCode = resolved.error.startsWith("Failed") ? 2 : 1;
1103
- return;
1104
- }
1105
- if (!resolved.testPath) {
1106
- printError(`API '${apiFlag}' has no test_path`);
1107
- process.exitCode = 1;
1108
- return;
1109
- }
1110
- paths = [resolved.testPath];
1111
- }
1112
- if (paths.length === 0) {
1113
- printError("No path given and no current API set; run `zond use <api>`, set ZOND_API, pass --api <name>, or pass path explicitly");
1114
- process.exitCode = 2;
1115
- return;
1116
- }
1117
-
1118
- const tags = flatSplit(opts.tag);
1119
- const excludeTags = flatSplit(opts.excludeTag);
1120
- const envVars = (opts.envVar as string[] | undefined)?.length ? (opts.envVar as string[]) : undefined;
1121
- const includeSpecs = (opts.include as string[] | undefined)?.length ? (opts.include as string[]) : undefined;
1122
- const excludeSpecs = (opts.exclude as string[] | undefined)?.length ? (opts.exclude as string[]) : undefined;
1123
-
1124
- process.exitCode = await runCommand({
1125
- paths,
1126
- env: opts.env,
1127
- report: opts.report as ReporterName,
1128
- timeout: opts.timeout,
1129
- rateLimit: opts.rateLimit,
1130
- bail: opts.bail === true,
1131
- sequential: opts.sequential === true,
1132
- noDb: opts.db === false,
1133
- dbPath: typeof opts.db === "string" ? opts.db : undefined,
1134
- authToken: opts.authToken,
1135
- safe: opts.safe === true,
1136
- tag: tags,
1137
- excludeTag: excludeTags,
1138
- method: opts.method,
1139
- include: includeSpecs,
1140
- exclude: excludeSpecs,
1141
- envVars,
1142
- strictVars: opts.strictVars === true,
1143
- dryRun: opts.dryRun === true,
1144
- quiet: opts.quiet === true,
1145
- failOnFailures: opts.failOnFailures !== false,
1146
- output: typeof opts.output === "string" ? opts.output : undefined,
1147
- validateSchema: opts.validateSchema === true,
1148
- specPath: typeof opts.spec === "string" ? opts.spec : undefined,
1149
- apiName: apiFlag,
1150
- sessionId: resolveSessionId({
1151
- flag: typeof opts.sessionId === "string" ? opts.sessionId : null,
1152
- env: process.env.ZOND_SESSION_ID ?? null,
1153
- }) ?? undefined,
1154
- json: false,
1155
- retryOnNetwork: typeof opts.retryOnNetwork === "number" ? opts.retryOnNetwork : 1,
1156
- learn: opts.learn === true,
1157
- learnApply: opts.learnApply === true,
1158
- learnTarget: opts.learnTarget as "test" | "drifts" | undefined,
1159
- maxRequests: typeof opts.maxRequests === "number" ? opts.maxRequests : undefined,
1160
- });
1161
- });
1162
- }