@kirrosh/zond 0.26.0 → 0.27.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 (261) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/README.md +22 -21
  3. package/bin/zond.mjs +23 -0
  4. package/package.json +13 -16
  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 -1142
  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,652 +0,0 @@
1
- import { resolve, dirname, basename } from "node:path";
2
- import type { TestSuite, TestStep, Environment, SourceMetadata, AssertionRule } from "../parser/types.ts";
3
- import { substituteString, substituteStep, substituteDeep, extractVariableReferences } from "../parser/variables.ts";
4
- import type { TestRunResult, StepResult, HttpRequest, AssertionResult } from "./types.ts";
5
- import { executeRequest, type FetchOptions } from "./http-client.ts";
6
- import type { RateLimiter } from "./rate-limiter.ts";
7
- import { checkAssertions, extractCaptures, findMissedCaptures } from "./assertions.ts";
8
- import { evaluateExpr } from "./expr-eval.ts";
9
- import { applyTransform } from "./transforms.ts";
10
- import type { SchemaValidator } from "./schema-validator.ts";
11
- import { classifyFailure } from "../diagnostics/failure-class.ts";
12
- import { buildUrl } from "../util/url.ts";
13
-
14
- /** Shallow-merge suite-level и step-level provenance. Step перекрывает suite. */
15
- function mergeProvenance(
16
- suiteSrc?: SourceMetadata,
17
- stepSrc?: SourceMetadata,
18
- ): SourceMetadata | null {
19
- if (!suiteSrc && !stepSrc) return null;
20
- return { ...(suiteSrc ?? {}), ...(stepSrc ?? {}) };
21
- }
22
-
23
- /** ARV-157: build the top-level `schema_validation` summary for a step that
24
- * was run with `--validate-schema`. Mirrors the shape `zond request
25
- * --validate-schema` already produces (see src/cli/commands/request.ts);
26
- * consumers can `jq '.steps[] | .schema_validation'` instead of digging
27
- * into `assertions[] | select(.kind=="schema")`.
28
- *
29
- * Returns undefined when the validator wasn't attached or the response had
30
- * no parseable JSON body — same precondition as `assertions.push(...)` at
31
- * the call site, so the summary is present iff schema actually ran. */
32
- function buildSchemaValidationSummary(
33
- validator: SchemaValidator,
34
- method: string,
35
- path: string,
36
- status: number,
37
- schemaAssertions: AssertionResult[],
38
- ): StepResult["schema_validation"] {
39
- const ins = validator.inspect(method, path, status);
40
- if (!ins.matchedEndpoint) {
41
- return { result: "no-endpoint", matched_endpoint: null, matched_response_status: null, error_count: 0 };
42
- }
43
- if (!ins.hasJsonSchema) {
44
- return {
45
- result: "no-schema",
46
- matched_endpoint: ins.matchedEndpoint,
47
- matched_response_status: ins.matchedResponseStatus,
48
- error_count: 0,
49
- };
50
- }
51
- const failed = schemaAssertions.filter((a) => !a.passed).length;
52
- return {
53
- result: failed === 0 ? "PASS" : "FAIL",
54
- matched_endpoint: ins.matchedEndpoint,
55
- matched_response_status: ins.matchedResponseStatus,
56
- error_count: failed,
57
- };
58
- }
59
-
60
- /** TASK-256: turn each missed-capture (path didn't resolve in response)
61
- * into an auxiliary failed assertion. The step then fails loudly with
62
- * "capture <var>: path '<path>' not found in body" instead of producing
63
- * silent `captures: {}` that the user only notices when the next step in a
64
- * CRUD chain skips with `Depends on missing capture`. */
65
- function buildMissedCaptureAssertions(
66
- misses: ReturnType<typeof findMissedCaptures>,
67
- ): AssertionResult[] {
68
- return misses.map((m) => ({
69
- field: `capture ${m.var}`,
70
- rule: m.source === "body" ? "body-path-exists" : "header-exists",
71
- passed: false,
72
- actual: undefined,
73
- expected: `${m.source} '${m.path}' present`,
74
- kind: "auxiliary",
75
- }));
76
- }
77
-
78
- function collectChainCaptures(tests: TestStep[]): Set<string> {
79
- const out = new Set<string>();
80
- const visit = (rules: Record<string, AssertionRule> | undefined) => {
81
- if (!rules) return;
82
- for (const r of Object.values(rules)) {
83
- if (r.capture) out.add(r.capture);
84
- if (r.each) visit(r.each);
85
- if (r.contains_item) visit(r.contains_item);
86
- }
87
- };
88
- for (const step of tests) visit(step.expect?.body);
89
- return out;
90
- }
91
-
92
- function emptyVarSkipReason(varName: string, chainCaptures: Set<string>): string {
93
- return chainCaptures.has(varName)
94
- ? `chain capture {{${varName}}} unbound (upstream step did not run or did not capture it)`
95
- : `required fixture {{${varName}}} is empty`;
96
- }
97
-
98
- function makeSkippedResult(
99
- stepName: string,
100
- reason: string,
101
- opts?: { cascade?: { missingCapture: string } },
102
- ): StepResult {
103
- const result: StepResult = {
104
- name: stepName,
105
- status: "skip",
106
- duration_ms: 0,
107
- request: { method: "", url: "", headers: {} },
108
- assertions: [],
109
- captures: {},
110
- error: reason,
111
- };
112
- if (opts?.cascade) {
113
- result.failure_class = "cascade";
114
- result.failure_class_reason = `Upstream capture not produced: ${opts.cascade.missingCapture}`;
115
- }
116
- return result;
117
- }
118
-
119
- /** Interpolate {{var}} placeholders inside a test/step name. Falls back to
120
- * the raw name string if substitution returns a non-string value. */
121
- function interpolateName(name: string, vars: Record<string, unknown>): string {
122
- try {
123
- const out = substituteString(name, vars);
124
- return typeof out === "string" ? out : String(out);
125
- } catch {
126
- return name;
127
- }
128
- }
129
-
130
- /**
131
- * Expand a `parameterize: { key: [val, ...] }` map into the cross-product of
132
- * iteration variable bindings. No `parameterize` (or an empty map) yields a
133
- * single empty iteration so the existing single-pass behaviour is preserved.
134
- *
135
- * Exported for tests.
136
- */
137
- export function expandParameterize(params?: Record<string, unknown[]>): Record<string, unknown>[] {
138
- if (!params) return [{}];
139
- const keys = Object.keys(params).filter(k => Array.isArray(params[k]) && (params[k] as unknown[]).length > 0);
140
- if (keys.length === 0) return [{}];
141
- let combos: Record<string, unknown>[] = [{}];
142
- for (const k of keys) {
143
- const values = params[k] as unknown[];
144
- const next: Record<string, unknown>[] = [];
145
- for (const combo of combos) {
146
- for (const v of values) {
147
- next.push({ ...combo, [k]: v });
148
- }
149
- }
150
- combos = next;
151
- }
152
- return combos;
153
- }
154
-
155
- export interface RunSuiteOptions {
156
- rateLimiter?: RateLimiter;
157
- /** Optional OpenAPI response-schema validator. When provided, every step's
158
- * parsed JSON body is validated against the matching schema; failures are
159
- * appended to the step's `assertions`. */
160
- schemaValidator?: SchemaValidator;
161
- /** TASK-144: per-step network-retry budget used by http-client for
162
- * ECONNRESET / EPIPE / `socket hang up` / `fetch failed` / abort cases.
163
- * Set by `zond run --retry-on-network <N>`. HTTP statuses are not retried
164
- * by this path. */
165
- networkRetries?: number;
166
- /** ARV-249: shared HTTP-request budget across all parallel suites. When
167
- * `used >= limit`, remaining steps short-circuit to `skip` with reason
168
- * `max-requests-cap-reached`. Each `retry_until` attempt counts as one
169
- * request; dry-run and set-only steps do not consume the budget. */
170
- requestBudget?: RequestBudget;
171
- /** ARV-249: invoked after every step completes (pass/fail/skip/error) so
172
- * the CLI can render a periodic progress line without each suite
173
- * knowing how many siblings are running in parallel. */
174
- onStepDone?: (step: StepResult) => void;
175
- }
176
-
177
- /** ARV-249: shared `--max-requests` budget. Mutated in-place by every
178
- * parallel `runSuite` call — single-threaded JS makes the
179
- * check-then-increment race-free. `limit === Infinity` means "uncapped"
180
- * (the default). */
181
- export interface RequestBudget {
182
- limit: number;
183
- used: number;
184
- }
185
-
186
- /** Try to reserve one HTTP slot from the shared budget. Returns true if
187
- * the caller may proceed, false if the cap has been reached. */
188
- export function reserveRequest(budget: RequestBudget | undefined): boolean {
189
- if (!budget) return true;
190
- if (budget.used >= budget.limit) return false;
191
- budget.used += 1;
192
- return true;
193
- }
194
-
195
- export const MAX_REQUESTS_SKIP_REASON = "max-requests-cap-reached";
196
-
197
- export async function runSuite(
198
- suite: TestSuite,
199
- env: Environment = {},
200
- dryRun = false,
201
- options: RunSuiteOptions = {},
202
- ): Promise<TestRunResult> {
203
- const startedAt = new Date().toISOString();
204
- const steps: StepResult[] = [];
205
-
206
- /** Push a step result, attaching provenance + failure classification. */
207
- const pushStep = (result: StepResult, currentStep?: TestStep): void => {
208
- const merged = mergeProvenance(suite.source, currentStep?.source);
209
- if (merged !== null) result.provenance = merged;
210
- const classification = classifyFailure(result);
211
- if (classification) {
212
- result.failure_class = classification.failure_class;
213
- result.failure_class_reason = classification.failure_class_reason;
214
- }
215
- steps.push(result);
216
- if (options.onStepDone) options.onStepDone(result);
217
- };
218
-
219
- const fetchOptions: Partial<FetchOptions> = {
220
- timeout: suite.config.timeout,
221
- retries: suite.config.retries,
222
- retry_delay: suite.config.retry_delay,
223
- follow_redirects: suite.config.follow_redirects,
224
- rate_limiter: options.rateLimiter,
225
- ...(options.networkRetries !== undefined ? { network_retries: options.networkRetries } : {}),
226
- };
227
-
228
- // Names of every variable a step in this suite tries to capture from a
229
- // response (expect.body.<field>.capture: <name>). When a later step
230
- // references one of these and the value is empty — under --dry-run, or
231
- // because the capturing step was skipped — the missing var is a chain
232
- // capture, NOT a fixture in .env.yaml. Distinguishing them in the skip
233
- // message stops users from chasing fixture seeding for vars that
234
- // shouldn't live in .env.yaml at all.
235
- const chainCaptures = collectChainCaptures(suite.tests);
236
-
237
- // parameterize cross-product → N iterations of the suite body.
238
- // Captures and tainted/missing sets are reset per iteration so that
239
- // values from one binding never leak into the next.
240
- const iterations = expandParameterize(suite.parameterize);
241
-
242
- for (const iterVars of iterations) {
243
- const variables: Record<string, unknown> = { ...env, ...iterVars };
244
- // Captures whose source step's assertions partially failed, but the value
245
- // itself was extracted. Cleanup/always steps may still consume them.
246
- const taintedCaptures = new Set<string>();
247
- // Captures that were never extracted (response missing the field). Even
248
- // always-steps can't run if their referenced capture is missing.
249
- const missingCaptures = new Set<string>();
250
-
251
- // Expand steps lazily (for_each needs current variables)
252
- let stepIndex = 0;
253
- const rawSteps = [...suite.tests];
254
-
255
- while (stepIndex < rawSteps.length) {
256
- const step = rawSteps[stepIndex]!;
257
- stepIndex++;
258
-
259
- // Expand for_each: insert expanded steps and skip current
260
- if (step.for_each) {
261
- const resolvedIn = substituteDeep(step.for_each.in, variables);
262
- const items = Array.isArray(resolvedIn) ? resolvedIn : [];
263
- const expanded: TestStep[] = [];
264
- for (const item of items) {
265
- const { for_each: _, ...rest } = step;
266
- expanded.push({ ...rest, name: `${step.name} [${step.for_each.var}=${JSON.stringify(item)}]` } as TestStep);
267
- // We'll inject the variable right before executing each expanded step
268
- // Store the var assignment via a set field
269
- }
270
- // Insert expanded steps at current position
271
- rawSteps.splice(stepIndex, 0, ...expanded);
272
- // Set the for_each variable for each expanded step
273
- for (let i = 0; i < items.length; i++) {
274
- const expandedStep = rawSteps[stepIndex + i]!;
275
- // Temporarily inject into variables when we reach this step
276
- // We need a way to pass the variable — use a hidden _for_each_vars
277
- (expandedStep as unknown as Record<string, unknown>).__for_each_var = { key: step.for_each.var, value: items[i] };
278
- }
279
- continue;
280
- }
281
-
282
- // Inject for_each variable if present
283
- const forEachData = (step as unknown as Record<string, unknown>).__for_each_var as { key: string; value: unknown } | undefined;
284
- if (forEachData) {
285
- variables[forEachData.key] = forEachData.value;
286
- delete (step as unknown as Record<string, unknown>).__for_each_var;
287
- }
288
-
289
- // Handle set-only steps (no HTTP request)
290
- if (step.set && step.path === "") {
291
- for (const [key, rawDirective] of Object.entries(step.set)) {
292
- const substituted = substituteDeep(rawDirective, variables);
293
- variables[key] = applyTransform(substituted);
294
- }
295
- pushStep({
296
- name: interpolateName(step.name, variables),
297
- status: "pass",
298
- duration_ms: 0,
299
- request: { method: "", url: "", headers: {} },
300
- assertions: [],
301
- captures: {},
302
- }, step);
303
- continue;
304
- }
305
-
306
- // Skip check: if step references a failed capture, skip — unless
307
- // step is `always: true` AND the capture is just tainted (still extracted).
308
- const referencedVars = extractVariableReferences(step);
309
- const missing = referencedVars.find((v) => missingCaptures.has(v));
310
- if (missing) {
311
- pushStep(
312
- makeSkippedResult(
313
- interpolateName(step.name, variables),
314
- `Depends on missing capture: ${missing}`,
315
- { cascade: { missingCapture: missing } },
316
- ),
317
- step,
318
- );
319
- continue;
320
- }
321
- if (!step.always) {
322
- const tainted = referencedVars.find((v) => taintedCaptures.has(v));
323
- if (tainted) {
324
- pushStep(
325
- makeSkippedResult(
326
- interpolateName(step.name, variables),
327
- `Depends on tainted capture: ${tainted} (use always: true on cleanup steps)`,
328
- { cascade: { missingCapture: tainted } },
329
- ),
330
- step,
331
- );
332
- continue;
333
- }
334
- }
335
-
336
- // skip_if evaluation
337
- if (step.skip_if) {
338
- const exprAfterSubst = String(substituteString(step.skip_if, variables));
339
- if (evaluateExpr(exprAfterSubst)) {
340
- const varMatch = step.skip_if.match(/\{\{([^{}]+)\}\}/);
341
- const skipMsg = varMatch
342
- ? emptyVarSkipReason(varMatch[1]!.trim(), chainCaptures)
343
- : step.skip_if;
344
- pushStep(makeSkippedResult(interpolateName(step.name, variables), skipMsg), step);
345
- continue;
346
- }
347
- }
348
-
349
- // Process set: on HTTP steps — evaluate generators once before building request.
350
- // Substitution can throw on unknown {{$generator}} — fail this step, not the suite.
351
- let resolved: TestStep;
352
- let resolvedBaseUrl: string | undefined;
353
- let resolvedSuiteHeaders: Record<string, string> | undefined;
354
- try {
355
- if (step.set) {
356
- for (const [key, rawDirective] of Object.entries(step.set)) {
357
- const substituted = substituteDeep(rawDirective, variables);
358
- variables[key] = applyTransform(substituted);
359
- }
360
- }
361
- resolved = substituteStep(step, variables);
362
- resolvedBaseUrl = suite.base_url ? substituteString(suite.base_url, variables) as string : undefined;
363
- resolvedSuiteHeaders = suite.headers ? substituteDeep(suite.headers, variables) : undefined;
364
- } catch (err) {
365
- const errorMsg = err instanceof Error ? err.message : String(err);
366
- pushStep({
367
- name: interpolateName(step.name, variables),
368
- status: "error",
369
- duration_ms: 0,
370
- request: { method: step.method, url: step.path, headers: {} },
371
- assertions: [],
372
- captures: {},
373
- error: errorMsg,
374
- }, step);
375
- // Substitution never produced a request → capture truly missing.
376
- if (step.expect.body) {
377
- for (const rule of Object.values(step.expect.body)) {
378
- if (rule.capture) missingCaptures.add(rule.capture);
379
- }
380
- }
381
- continue;
382
- }
383
- // Skip if any path-variable in the template resolved to empty — an empty
384
- // path segment produces URLs like /repos//commits/ which always 404/500.
385
- // The explicit skip_if guard only covers the first param (TASK-237);
386
- // this catches all others.
387
- {
388
- let emptyVar: string | null = null;
389
- for (const m of step.path.matchAll(/\{\{([^{}]+)\}\}/g)) {
390
- const varName = m[1]!.trim();
391
- const val = variables[varName];
392
- if (val === "" || val === null || val === undefined) { emptyVar = varName; break; }
393
- }
394
- if (emptyVar) {
395
- pushStep(makeSkippedResult(
396
- interpolateName(step.name, variables),
397
- emptyVarSkipReason(emptyVar, chainCaptures),
398
- ), step);
399
- continue;
400
- }
401
- }
402
- const url = buildUrl(resolvedBaseUrl, resolved.path, resolved.query);
403
- const headers: Record<string, string> = { ...resolvedSuiteHeaders, ...resolved.headers };
404
- let body: string | undefined;
405
- let formData: FormData | undefined;
406
-
407
- if (resolved.json !== undefined) {
408
- body = JSON.stringify(resolved.json);
409
- if (!headers["Content-Type"] && !headers["content-type"]) {
410
- headers["Content-Type"] = "application/json";
411
- }
412
- } else if (resolved.form) {
413
- body = new URLSearchParams(resolved.form).toString();
414
- if (!headers["Content-Type"] && !headers["content-type"]) {
415
- headers["Content-Type"] = "application/x-www-form-urlencoded";
416
- }
417
- } else if (resolved.multipart) {
418
- const basedir = suite.filePath ? dirname(suite.filePath) : process.cwd();
419
- formData = new FormData();
420
- for (const [key, field] of Object.entries(resolved.multipart)) {
421
- if (typeof field === "string") {
422
- formData.append(key, field);
423
- } else {
424
- const absPath = resolve(basedir, field.file);
425
- const buf = await Bun.file(absPath).arrayBuffer();
426
- const mime = field.content_type ?? "application/octet-stream";
427
- const filename = field.filename ?? basename(absPath);
428
- formData.append(key, new Blob([buf], { type: mime }), filename);
429
- }
430
- }
431
- }
432
-
433
- const request: HttpRequest = { method: resolved.method, url, headers, body, formData };
434
-
435
- // Validate absolute URL before attempting fetch
436
- if (!url.startsWith("http://") && !url.startsWith("https://")) {
437
- pushStep({
438
- name: interpolateName(step.name, variables),
439
- status: "error",
440
- duration_ms: 0,
441
- request,
442
- assertions: [],
443
- captures: {},
444
- error: `base_url is not configured — URL resolved to a relative path: "${url}". Set base_url in .env.yaml`,
445
- }, step);
446
- if (step.expect.body) {
447
- for (const rule of Object.values(step.expect.body)) {
448
- if (rule.capture) missingCaptures.add(rule.capture);
449
- }
450
- }
451
- continue;
452
- }
453
-
454
- if (dryRun) {
455
- const bodyPreview = formData
456
- ? ` [multipart: ${[...formData.keys()].length} field(s)]`
457
- : body ? ` ${body.slice(0, 200)}` : "";
458
- pushStep({
459
- name: interpolateName(step.name, variables),
460
- status: "pass",
461
- duration_ms: 0,
462
- request,
463
- assertions: [],
464
- captures: {},
465
- error: `[DRY RUN] ${resolved.method} ${url}${bodyPreview}`,
466
- }, step);
467
- continue;
468
- }
469
-
470
- // retry_until wrapper
471
- if (step.retry_until) {
472
- const rt = step.retry_until;
473
- let lastStepResult: StepResult | undefined;
474
- for (let attempt = 0; attempt < rt.max_attempts; attempt++) {
475
- if (!reserveRequest(options.requestBudget)) {
476
- lastStepResult = makeSkippedResult(
477
- interpolateName(step.name, variables),
478
- MAX_REQUESTS_SKIP_REASON,
479
- );
480
- break;
481
- }
482
- try {
483
- const response = await executeRequest(request, fetchOptions);
484
- const captures = extractCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
485
- const missedCaps = findMissedCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
486
- const assertions = checkAssertions(resolved.expect, response);
487
- assertions.push(...buildMissedCaptureAssertions(missedCaps));
488
- let schemaValidationSummary: StepResult["schema_validation"] | undefined;
489
- if (options.schemaValidator && response.body_parsed !== undefined) {
490
- const schemaAssertions = options.schemaValidator.validate(resolved.method, resolved.path, response.status, response.body_parsed);
491
- assertions.push(...schemaAssertions);
492
- schemaValidationSummary = buildSchemaValidationSummary(
493
- options.schemaValidator,
494
- resolved.method,
495
- resolved.path,
496
- response.status,
497
- schemaAssertions,
498
- );
499
- }
500
- const allPassed = assertions.every((a) => a.passed);
501
-
502
- lastStepResult = {
503
- name: interpolateName(step.name, variables),
504
- status: allPassed ? "pass" : "fail",
505
- duration_ms: response.duration_ms,
506
- request,
507
- response,
508
- assertions,
509
- captures,
510
- ...(response.network_retry_count && response.network_retry_count > 0
511
- ? { network_retry: response.network_retry_count }
512
- : {}),
513
- ...(schemaValidationSummary ? { schema_validation: schemaValidationSummary } : {}),
514
- };
515
-
516
- // Evaluate condition with response context
517
- const condVars: Record<string, unknown> = { ...variables, ...captures, status: response.status };
518
- if (response.body_parsed && typeof response.body_parsed === "object") {
519
- for (const [k, v] of Object.entries(response.body_parsed as Record<string, unknown>)) {
520
- condVars[k] = v;
521
- }
522
- }
523
- const condStr = String(substituteString(rt.condition, condVars));
524
- if (evaluateExpr(condStr)) {
525
- Object.assign(variables, captures);
526
- break;
527
- }
528
-
529
- if (attempt < rt.max_attempts - 1) {
530
- await new Promise((resolve) => setTimeout(resolve, rt.delay_ms));
531
- }
532
- } catch (err) {
533
- lastStepResult = {
534
- name: interpolateName(step.name, variables),
535
- status: "error",
536
- duration_ms: 0,
537
- request,
538
- assertions: [],
539
- captures: {},
540
- error: err instanceof Error ? err.message : String(err),
541
- };
542
- }
543
- }
544
- if (lastStepResult) pushStep(lastStepResult, step);
545
- continue;
546
- }
547
-
548
- if (!reserveRequest(options.requestBudget)) {
549
- pushStep(makeSkippedResult(
550
- interpolateName(step.name, variables),
551
- MAX_REQUESTS_SKIP_REASON,
552
- ), step);
553
- continue;
554
- }
555
-
556
- try {
557
- const response = await executeRequest(request, fetchOptions);
558
-
559
- // Extract captures (body + header)
560
- const captures = extractCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
561
- Object.assign(variables, captures);
562
-
563
- // Track expected captures that weren't obtained — these are missing.
564
- if (resolved.expect.body) {
565
- for (const rule of Object.values(resolved.expect.body)) {
566
- if (rule.capture && !(rule.capture in captures)) {
567
- missingCaptures.add(rule.capture);
568
- }
569
- }
570
- }
571
-
572
- // Run assertions
573
- const missedCaps = findMissedCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
574
- const assertions = checkAssertions(resolved.expect, response);
575
- assertions.push(...buildMissedCaptureAssertions(missedCaps));
576
- let schemaValidationSummary: StepResult["schema_validation"] | undefined;
577
- if (options.schemaValidator && response.body_parsed !== undefined) {
578
- const schemaAssertions = options.schemaValidator.validate(resolved.method, resolved.path, response.status, response.body_parsed);
579
- assertions.push(...schemaAssertions);
580
- schemaValidationSummary = buildSchemaValidationSummary(
581
- options.schemaValidator,
582
- resolved.method,
583
- resolved.path,
584
- response.status,
585
- schemaAssertions,
586
- );
587
- }
588
- const allPassed = assertions.every((a) => a.passed);
589
-
590
- pushStep({
591
- name: interpolateName(step.name, variables),
592
- status: allPassed ? "pass" : "fail",
593
- duration_ms: response.duration_ms,
594
- request,
595
- response,
596
- assertions,
597
- captures,
598
- ...(response.network_retry_count && response.network_retry_count > 0
599
- ? { network_retry: response.network_retry_count }
600
- : {}),
601
- ...(schemaValidationSummary ? { schema_validation: schemaValidationSummary } : {}),
602
- }, step);
603
-
604
- // If step failed, captures that did extract are tainted (value is real
605
- // but came from a step whose other assertions failed). Always-steps may
606
- // still consume them; non-always steps cascade-skip.
607
- if (!allPassed && resolved.expect.body) {
608
- for (const rule of Object.values(resolved.expect.body)) {
609
- if (rule.capture && rule.capture in captures) {
610
- taintedCaptures.add(rule.capture);
611
- }
612
- }
613
- }
614
- } catch (err) {
615
- const errorMsg = err instanceof Error ? err.message : String(err);
616
- pushStep({
617
- name: interpolateName(step.name, variables),
618
- status: "error",
619
- duration_ms: 0,
620
- request,
621
- assertions: [],
622
- captures: {},
623
- error: errorMsg,
624
- }, step);
625
-
626
- // Network/runtime error → no response → capture truly missing.
627
- if (step.expect.body) {
628
- for (const rule of Object.values(step.expect.body)) {
629
- if (rule.capture) missingCaptures.add(rule.capture);
630
- }
631
- }
632
- }
633
- }
634
- } // end of parameterize iteration loop
635
-
636
- const finishedAt = new Date().toISOString();
637
- return {
638
- suite_name: suite.name,
639
- suite_tags: suite.tags,
640
- suite_description: suite.description,
641
- suite_file: suite.filePath,
642
- started_at: startedAt,
643
- finished_at: finishedAt,
644
- total: steps.length,
645
- passed: steps.filter((s) => s.status === "pass").length,
646
- failed: steps.filter((s) => s.status === "fail").length,
647
- skipped: steps.filter((s) => s.status === "skip").length,
648
- // ARV-318: surface error steps so total = passed+failed+skipped+errored.
649
- errored: steps.filter((s) => s.status === "error").length,
650
- steps,
651
- };
652
- }
@@ -1,41 +0,0 @@
1
- const OPERATORS = ["!=", "==", ">=", "<=", ">", "<"] as const;
2
-
3
- export function evaluateExpr(expr: string): boolean {
4
- const trimmed = expr.trim();
5
- if (trimmed === "") return false;
6
-
7
- for (const op of OPERATORS) {
8
- const idx = trimmed.indexOf(op);
9
- if (idx !== -1) {
10
- const left = trimmed.slice(0, idx).trim();
11
- const right = trimmed.slice(idx + op.length).trim();
12
- return compareValues(left, right, op);
13
- }
14
- }
15
-
16
- // No operator — truthiness
17
- return isTruthy(trimmed);
18
- }
19
-
20
- function compareValues(left: string, right: string, op: string): boolean {
21
- const lNum = Number(left);
22
- const rNum = Number(right);
23
- const numeric = !isNaN(lNum) && !isNaN(rNum) && left !== "" && right !== "";
24
-
25
- switch (op) {
26
- case "==": return numeric ? lNum === rNum : left === right;
27
- case "!=": return numeric ? lNum !== rNum : left !== right;
28
- case ">": return numeric ? lNum > rNum : left > right;
29
- case "<": return numeric ? lNum < rNum : left < right;
30
- case ">=": return numeric ? lNum >= rNum : left >= right;
31
- case "<=": return numeric ? lNum <= rNum : left <= right;
32
- default: return false;
33
- }
34
- }
35
-
36
- function isTruthy(value: string): boolean {
37
- if (value === "" || value === "0" || value === "false" || value === "null" || value === "undefined") {
38
- return false;
39
- }
40
- return true;
41
- }