@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,359 +0,0 @@
1
- import { z } from "zod";
2
- import type { TestSuite, TestStep, AssertionRule, TestStepExpect, SuiteConfig, RetryUntil, ForEach, MultipartField, SourceMetadata } from "./types.ts";
3
-
4
- // ARV-223 (R16/F28): include OPTIONS / HEAD / TRACE so probe-method generated
5
- // suites (which emit one step per missing-method per path) parse and run.
6
- // Without this, `zond probe static --emit-tests → zond run` breaks end-to-end
7
- // on every API where these methods aren't already declared (= almost always).
8
- const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE"] as const;
9
-
10
- function extractMethodAndPath(raw: unknown): unknown {
11
- if (typeof raw !== "object" || raw === null) return raw;
12
- const obj = raw as Record<string, unknown>;
13
-
14
- let foundMethod: string | undefined;
15
- for (const method of HTTP_METHODS) {
16
- if (method in obj) {
17
- if (foundMethod) {
18
- throw new Error(`Ambiguous step: found both ${foundMethod} and ${method} keys`);
19
- }
20
- foundMethod = method;
21
- }
22
- }
23
-
24
- if (foundMethod) {
25
- const path = obj[foundMethod];
26
- if (typeof path !== "string") {
27
- throw new Error(`${foundMethod} value must be a string path, got ${typeof path}`);
28
- }
29
- const { [foundMethod]: _, ...rest } = obj;
30
- return { ...rest, method: foundMethod, path };
31
- }
32
-
33
- // set-only step: no HTTP method required
34
- if (obj.set && !obj.method) {
35
- return { ...obj, method: "GET", path: "" };
36
- }
37
-
38
- return raw;
39
- }
40
-
41
- const ASSERTION_KEYS = new Set([
42
- "capture", "type", "equals", "not_equals", "contains", "not_contains",
43
- "matches", "gt", "lt", "gte", "lte", "exists",
44
- "length", "length_gt", "length_gte", "length_lt", "length_lte",
45
- "each", "contains_item", "set_equals",
46
- ]);
47
-
48
- /**
49
- * Recursively flattens nested body assertion objects into dot-notation keys.
50
- * e.g. { category: { name: { equals: "Dogs" } } } → { "category.name": { equals: "Dogs" } }
51
- * Leaves assertion-level objects untouched (objects where all keys are ASSERTION_KEYS).
52
- * Also skips the special `_body` key prefix.
53
- */
54
- export function flattenBodyAssertions(body: Record<string, unknown>): Record<string, unknown> {
55
- const result: Record<string, unknown> = {};
56
-
57
- function walk(obj: Record<string, unknown>, prefix: string) {
58
- for (const [key, value] of Object.entries(obj)) {
59
- const fullKey = prefix ? `${prefix}.${key}` : key;
60
-
61
- if (
62
- typeof value === "object" && value !== null && !Array.isArray(value) &&
63
- !fullKey.startsWith("_body")
64
- ) {
65
- const objKeys = Object.keys(value as Record<string, unknown>);
66
- const isAssertionRule = objKeys.length > 0 && objKeys.every(k => ASSERTION_KEYS.has(k));
67
-
68
- if (isAssertionRule) {
69
- result[fullKey] = value;
70
- } else {
71
- walk(value as Record<string, unknown>, fullKey);
72
- }
73
- } else {
74
- result[fullKey] = value;
75
- }
76
- }
77
- }
78
-
79
- walk(body, "");
80
- return result;
81
- }
82
-
83
- const AssertionRuleSchemaInner: z.ZodType<AssertionRule> = z.preprocess(
84
- (val) => {
85
- if (typeof val === "string") return { type: val };
86
- if (val === null || val === undefined) return { exists: true };
87
- if (typeof val === "object" && val !== null) {
88
- const obj = val as Record<string, unknown>;
89
- // Coerce exists: "true"/"false" → boolean
90
- if (typeof obj.exists === "string") {
91
- obj.exists = obj.exists === "true";
92
- }
93
- return obj;
94
- }
95
- return val;
96
- },
97
- z.object({
98
- capture: z.string().optional(),
99
- type: z.enum(["string", "integer", "number", "boolean", "array", "object", "null"]).optional(),
100
- equals: z.unknown().optional(),
101
- not_equals: z.unknown().optional(),
102
- contains: z.string().optional(),
103
- not_contains: z.string().optional(),
104
- matches: z.string().optional(),
105
- gt: z.number().optional(),
106
- lt: z.number().optional(),
107
- gte: z.number().optional(),
108
- lte: z.number().optional(),
109
- exists: z.boolean().optional(),
110
- length: z.number().int().optional(),
111
- length_gt: z.number().int().optional(),
112
- length_gte: z.number().int().optional(),
113
- length_lt: z.number().int().optional(),
114
- length_lte: z.number().int().optional(),
115
- each: z.record(z.string(), z.lazy(() => AssertionRuleSchemaInner)).optional(),
116
- contains_item: z.record(z.string(), z.lazy(() => AssertionRuleSchemaInner)).optional(),
117
- set_equals: z.unknown().optional(),
118
- }),
119
- ) as z.ZodType<AssertionRule>;
120
-
121
- const AssertionRuleSchema = AssertionRuleSchemaInner;
122
-
123
- const TestStepExpectSchema: z.ZodType<TestStepExpect> = z.preprocess(
124
- (val) => {
125
- if (typeof val !== "object" || val === null) return val;
126
- const obj = val as Record<string, unknown>;
127
- // Reject `expect.capture: {...}` — non-canonical syntax some users
128
- // reach for. zond captures live INSIDE body-rules
129
- // (`body: { "path.to.field": { capture: var_name } }`); a top-level
130
- // `capture:` block inside `expect:` is silently dropped, leaving the
131
- // test green with no captured values. Throw with a clear pointer.
132
- // (TASK-247)
133
- if ("capture" in obj && typeof obj.capture === "object" && obj.capture !== null && !Array.isArray(obj.capture)) {
134
- throw new Error(
135
- `'expect.capture: {...}' is not a valid step shape. Captures are defined per-field: ` +
136
- `\`expect.body: { "<path>": { capture: <var_name> } }\`. ` +
137
- `Top-level 'capture' inside 'expect' is silently ignored — the test would pass with no captured values.`,
138
- );
139
- }
140
- // expect.status: spot-message for common wrong shapes.
141
- // Schema accepts `number | number[]`. Users reaching from other tools often
142
- // write `oneOf: [...]`, `any: [...]`, a string `"200"`, or an array
143
- // containing strings. The raw zod-issue path (`tests.N.expect.status.0`)
144
- // is hard to read — surface a single-line hint here. (TASK-249, feedback-13#F1)
145
- if ("status" in obj && obj.status !== undefined && obj.status !== null) {
146
- const s = obj.status;
147
- const STATUS_HINT =
148
- "expect.status: use a number (200), an array of numbers ([200, 404]), or omit. " +
149
- "oneOf/any/anyOf are not supported.";
150
- if (typeof s === "object" && !Array.isArray(s)) {
151
- const keys = Object.keys(s as Record<string, unknown>);
152
- const wrong = keys.find((k) => ["oneOf", "anyOf", "any", "in", "one_of"].includes(k));
153
- if (wrong) {
154
- throw new Error(`'expect.status' got '${wrong}: [...]' — ${STATUS_HINT}`);
155
- }
156
- throw new Error(`'expect.status' got an object — ${STATUS_HINT}`);
157
- }
158
- if (typeof s === "string") {
159
- throw new Error(`'expect.status' got string "${s}" — ${STATUS_HINT}`);
160
- }
161
- if (Array.isArray(s) && s.some((v) => typeof v !== "number")) {
162
- throw new Error(`'expect.status' array must contain only numbers — ${STATUS_HINT}`);
163
- }
164
- }
165
- // body: null → remove it
166
- if (obj.body === null) {
167
- const { body: _, ...rest } = obj;
168
- return rest;
169
- }
170
- // Flatten nested body assertions into dot-notation
171
- if (obj.body && typeof obj.body === "object" && !Array.isArray(obj.body)) {
172
- obj.body = flattenBodyAssertions(obj.body as Record<string, unknown>);
173
- }
174
- return obj;
175
- },
176
- z.object({
177
- status: z.union([z.number().int(), z.array(z.number().int())]).optional(),
178
- body: z.record(z.string(), AssertionRuleSchema).optional(),
179
- headers: z.record(z.string(), z.union([z.string(), AssertionRuleSchema])).optional(),
180
- duration: z.number().optional(),
181
- }),
182
- ) as z.ZodType<TestStepExpect>;
183
-
184
- const RetryUntilSchema: z.ZodType<RetryUntil> = z.object({
185
- condition: z.string(),
186
- max_attempts: z.number().int().positive(),
187
- delay_ms: z.number().int().nonnegative(),
188
- });
189
-
190
- const ForEachSchema: z.ZodType<ForEach> = z.object({
191
- var: z.string(),
192
- in: z.unknown(),
193
- });
194
-
195
- const MultipartFileFieldSchema = z.object({
196
- file: z.string(),
197
- filename: z.string().optional(),
198
- content_type: z.string().optional(),
199
- });
200
-
201
- const MultipartFieldSchema: z.ZodType<MultipartField> = z.union([z.string(), MultipartFileFieldSchema]);
202
-
203
- // Provenance metadata: passthrough — все поля optional, неизвестные пропускаем без warning
204
- const SourceMetadataSchema: z.ZodType<SourceMetadata> = z.object({
205
- type: z.enum(["openapi-generated", "manual", "probe-suite"]).optional(),
206
- spec: z.string().optional(),
207
- generator: z.string().optional(),
208
- generated_at: z.string().optional(),
209
- endpoint: z.string().optional(),
210
- response_branch: z.string().optional(),
211
- schema_pointer: z.string().optional(),
212
- }).passthrough() as z.ZodType<SourceMetadata>;
213
-
214
- const KNOWN_STEP_KEYS = new Set([
215
- "name", "source", "method", "path", "headers",
216
- "json", "form", "multipart", "query", "expect",
217
- "skip_if", "retry_until", "for_each", "set", "always",
218
- // raw HTTP method keys are folded into method/path by extractMethodAndPath
219
- ...HTTP_METHODS,
220
- ]);
221
-
222
- // Common typo / wrong-name body keys we detect explicitly to emit an
223
- // actionable error instead of silently dropping. Real APIs reject the empty
224
- // POST that follows, but the user spends 10+ minutes debugging — this hint
225
- // turns it into a one-line fix. (TASK-244)
226
- const BODY_KEY_HINTS: Record<string, string> = {
227
- body: "json (for application/json), form (urlencoded), or multipart (file upload)",
228
- data: "json (for application/json) or form (urlencoded)",
229
- payload: "json",
230
- // TASK-257: previous hint pointed only at `form:` which is x-www-form-urlencoded
231
- // and useless for file uploads. Surface `multipart:` explicitly so users with
232
- // file-upload endpoints (file-upload endpoints, etc.) find it.
233
- raw: "json for raw JSON, multipart: { field: { file: <path> } } for file upload, or form for urlencoded — raw bodies are not parsed",
234
- };
235
-
236
- const TestStepSchema: z.ZodType<TestStep> = z.preprocess(
237
- (raw) => {
238
- const obj = extractMethodAndPath(raw);
239
- if (typeof obj === "object" && obj !== null) {
240
- const o = obj as Record<string, unknown>;
241
-
242
- // Reject silently-dropped body-shaped keys with a clear suggestion.
243
- for (const [bad, hint] of Object.entries(BODY_KEY_HINTS)) {
244
- if (bad in o) {
245
- const stepName = typeof o.name === "string" ? ` in step "${o.name}"` : "";
246
- throw new Error(
247
- `Unknown step key '${bad}'${stepName}. Did you mean '${hint}'? ` +
248
- `(zond does not recognize '${bad}:' and would silently drop the body)`,
249
- );
250
- }
251
- }
252
-
253
- // Make expect optional for set-only steps
254
- if (o.set && !o.expect) {
255
- o.expect = {};
256
- }
257
- }
258
- return obj;
259
- },
260
- z.object({
261
- name: z.string(),
262
- source: SourceMetadataSchema.optional(),
263
- method: z.enum(HTTP_METHODS),
264
- path: z.string(),
265
- headers: z.record(z.string(), z.string()).optional(),
266
- json: z.unknown().optional(),
267
- form: z.record(z.string(), z.string()).optional(),
268
- multipart: z.record(z.string(), MultipartFieldSchema).optional(),
269
- query: z.record(z.string(), z.string()).optional(),
270
- expect: TestStepExpectSchema,
271
- skip_if: z.string().optional(),
272
- retry_until: RetryUntilSchema.optional(),
273
- for_each: ForEachSchema.optional(),
274
- set: z.record(z.string(), z.unknown()).optional(),
275
- always: z.boolean().optional(),
276
- }),
277
- ) as z.ZodType<TestStep>;
278
-
279
- export const DEFAULT_CONFIG: SuiteConfig = {
280
- timeout: 30000,
281
- retries: 0,
282
- retry_delay: 1000,
283
- follow_redirects: true,
284
- verify_ssl: true,
285
- };
286
-
287
- const SuiteConfigSchema = z.preprocess(
288
- (val) => ({ ...DEFAULT_CONFIG, ...(typeof val === "object" && val !== null ? val : {}) }),
289
- z.object({
290
- timeout: z.number(),
291
- retries: z.number(),
292
- retry_delay: z.number(),
293
- follow_redirects: z.boolean(),
294
- verify_ssl: z.boolean(),
295
- }),
296
- ) as z.ZodType<SuiteConfig>;
297
-
298
- const TestSuiteSchema = z.preprocess(
299
- (val) => {
300
- if (typeof val === "object" && val !== null && !("config" in val)) {
301
- return { ...val, config: DEFAULT_CONFIG };
302
- }
303
- return val;
304
- },
305
- z.object({
306
- name: z.string(),
307
- description: z.string().optional(),
308
- setup: z.boolean().optional(),
309
- tags: z.array(z.string()).optional(),
310
- source: SourceMetadataSchema.optional(),
311
- base_url: z.string().optional(),
312
- headers: z.record(z.string(), z.string()).optional(),
313
- parameterize: z.record(z.string(), z.array(z.unknown()).min(1)).optional(),
314
- config: SuiteConfigSchema,
315
- tests: z.array(TestStepSchema).min(1),
316
- }),
317
- );
318
-
319
- export function validateSuite(raw: unknown): TestSuite {
320
- return TestSuiteSchema.parse(raw) as TestSuite;
321
- }
322
-
323
- /** Render a zod path array (`["tests", 0, "expect", "status"]`) as
324
- * `tests[0].expect.status`. Numeric segments become bracket-indices, string
325
- * segments dot-join. */
326
- function pathToHuman(path: ReadonlyArray<string | number>): string {
327
- let out = "";
328
- for (const seg of path) {
329
- if (typeof seg === "number") out += `[${seg}]`;
330
- else out += out ? `.${seg}` : seg;
331
- }
332
- return out || "(root)";
333
- }
334
-
335
- /**
336
- * Format a {@link z.ZodError} as a compact, multi-line, human-readable list:
337
- *
338
- * N validation issue(s):
339
- * <path>: <message>
340
- * ...
341
- *
342
- * The default `ZodError.message` is a JSON dump of the full issue list with
343
- * internal field names (`_def`, deeply numeric paths, "Invalid input" prefix).
344
- * The wrapper that callers used to surface ("Validation error in <file>:
345
- * [{...}]") was unreadable for tester users — they had to mentally parse the
346
- * stack to find the real path. (TASK-249)
347
- */
348
- export function formatZodError(err: z.ZodError): string {
349
- const lines = err.issues.map((i) => {
350
- const path = pathToHuman(i.path as ReadonlyArray<string | number>);
351
- // zod v4 messages are already readable; strip the redundant "Invalid input: "
352
- // prefix that adds noise without info.
353
- const msg = i.message.replace(/^Invalid input:\s*/, "");
354
- return ` ${path}: ${msg}`;
355
- });
356
- const header = `${err.issues.length} validation issue${err.issues.length === 1 ? "" : "s"}:`;
357
- return `${header}\n${lines.join("\n")}`;
358
- }
359
-
@@ -1,117 +0,0 @@
1
- export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD" | "TRACE";
2
-
3
- export interface AssertionRule {
4
- capture?: string;
5
- type?: "string" | "integer" | "number" | "boolean" | "array" | "object" | "null";
6
- equals?: unknown;
7
- not_equals?: unknown;
8
- contains?: string;
9
- not_contains?: string;
10
- matches?: string;
11
- gt?: number;
12
- lt?: number;
13
- gte?: number;
14
- lte?: number;
15
- exists?: boolean;
16
- length?: number;
17
- length_gt?: number;
18
- length_gte?: number;
19
- length_lt?: number;
20
- length_lte?: number;
21
- each?: Record<string, AssertionRule>;
22
- contains_item?: Record<string, AssertionRule>;
23
- set_equals?: unknown;
24
- }
25
-
26
- export interface TestStepExpect {
27
- status?: number | number[];
28
- body?: Record<string, AssertionRule>;
29
- headers?: Record<string, string | AssertionRule>;
30
- duration?: number;
31
- }
32
-
33
- export interface RetryUntil {
34
- condition: string;
35
- max_attempts: number;
36
- delay_ms: number;
37
- }
38
-
39
- export interface ForEach {
40
- var: string;
41
- in: unknown;
42
- }
43
-
44
- /**
45
- * Provenance metadata: «откуда этот test/suite». Optional, не участвует в
46
- * matching/dedup/validation. Suite-level задаёт общие поля; step-level
47
- * наследует через shallow merge `{ ...suite.source, ...step.source }`.
48
- */
49
- export interface SourceMetadata {
50
- type?: "openapi-generated" | "manual" | "probe-suite";
51
- spec?: string;
52
- generator?: string;
53
- generated_at?: string;
54
- endpoint?: string;
55
- response_branch?: string;
56
- schema_pointer?: string;
57
- [key: string]: unknown;
58
- }
59
-
60
- export interface MultipartFileField {
61
- file: string;
62
- filename?: string;
63
- content_type?: string;
64
- }
65
-
66
- export type MultipartField = string | MultipartFileField;
67
-
68
- export interface TestStep {
69
- name: string;
70
- source?: SourceMetadata;
71
- method: HttpMethod;
72
- path: string;
73
- headers?: Record<string, string>;
74
- json?: unknown;
75
- form?: Record<string, string>;
76
- multipart?: Record<string, MultipartField>;
77
- query?: Record<string, string>;
78
- expect: TestStepExpect;
79
- skip_if?: string;
80
- retry_until?: RetryUntil;
81
- for_each?: ForEach;
82
- set?: Record<string, unknown>;
83
- /**
84
- * Run this step even when prior steps in the suite have failed assertions
85
- * (so their captures are "tainted"). Designed for cleanup steps. Still
86
- * skips if a referenced capture is genuinely missing from a response.
87
- */
88
- always?: boolean;
89
- }
90
-
91
- export interface SuiteConfig {
92
- timeout: number;
93
- retries: number;
94
- retry_delay: number;
95
- follow_redirects: boolean;
96
- verify_ssl: boolean;
97
- }
98
-
99
- export interface TestSuite {
100
- name: string;
101
- description?: string;
102
- /** If true, this suite runs before all regular suites and its captures are shared into their env */
103
- setup?: boolean;
104
- tags?: string[];
105
- source?: SourceMetadata;
106
- base_url?: string;
107
- headers?: Record<string, string>;
108
- /** Cross-product parameterisation: each key contributes one variable
109
- * binding per array entry. Suite body runs once per combination. */
110
- parameterize?: Record<string, unknown[]>;
111
- config: SuiteConfig;
112
- tests: TestStep[];
113
- /** Absolute path to the source file, set by yaml-parser */
114
- filePath?: string;
115
- }
116
-
117
- export type Environment = Record<string, string>;
Binary file
@@ -1,181 +0,0 @@
1
- import { Glob } from "bun";
2
- import { resolve } from "node:path";
3
- import YAML from "yaml";
4
- import { z } from "zod";
5
- import { validateSuite, formatZodError } from "./schema.ts";
6
- import type { TestSuite } from "./types.ts";
7
-
8
- export interface ParseOptions {
9
- /** Surface raw `ZodError.message` (the JSON-formatted issue stack) instead
10
- * of the human-friendly summary. Useful for filing zod bugs / debugging the
11
- * schema itself; default output is human-readable. (TASK-249) */
12
- verbose?: boolean;
13
- }
14
-
15
- /** Convert a 0-based byte offset into a 1-based (line, col) position. */
16
- function offsetToLineCol(text: string, offset: number): { line: number; col: number } {
17
- let line = 1;
18
- let col = 1;
19
- for (let i = 0; i < offset && i < text.length; i++) {
20
- if (text.charCodeAt(i) === 0x0a) {
21
- line++;
22
- col = 1;
23
- } else {
24
- col++;
25
- }
26
- }
27
- return { line, col };
28
- }
29
-
30
- /**
31
- * Format a YAML parse error as `file:line:col: <reason>` plus a snippet with
32
- * a column pointer. Bun.YAML's SyntaxError exposes JS stack coordinates, not
33
- * YAML positions, so on parse failure we re-parse with eemeli/yaml (which
34
- * provides accurate `linePos`) just for diagnostics.
35
- *
36
- * Exported for tests.
37
- */
38
- export function formatYamlParseError(filePath: string, text: string, primary: Error): Error {
39
- const doc = YAML.parseDocument(text);
40
- const e = doc.errors[0];
41
- if (e?.linePos?.[0]) {
42
- const { line, col } = e.linePos[0];
43
- // eemeli's message reads "<reason> at line X, column Y:\n\n<snippet>".
44
- // Strip the "at line ..." part since we surface line:col in the prefix.
45
- const cleaned = e.message.replace(/\s+at line \d+, column \d+:/, ":");
46
- return new Error(`Invalid YAML in ${filePath}:${line}:${col}: ${cleaned}`);
47
- }
48
- // eemeli accepted but Bun rejected — fall back to original message.
49
- return new Error(`Invalid YAML in ${filePath}: ${primary.message}`);
50
- }
51
-
52
- export async function parseFile(filePath: string, opts: ParseOptions = {}): Promise<TestSuite> {
53
- let text: string;
54
- try {
55
- text = await Bun.file(filePath).text();
56
- } catch (err) {
57
- throw new Error(`Failed to read file ${filePath}: ${(err as Error).message}`);
58
- }
59
-
60
- // Both Bun.YAML and eemeli/yaml accept NUL bytes silently, but they corrupt
61
- // downstream consumers (sqlite TEXT, JSON, terminals). Surface explicitly.
62
- const nulIdx = text.indexOf("\x00");
63
- if (nulIdx >= 0) {
64
- const { line, col } = offsetToLineCol(text, nulIdx);
65
- throw new Error(
66
- `Invalid YAML in ${filePath}:${line}:${col}: NUL byte (\\x00) in source — ` +
67
- `if you need a NUL in a request body, use the {{$nullByte}} generator instead of inlining the byte`
68
- );
69
- }
70
-
71
- let raw: unknown;
72
- try {
73
- raw = Bun.YAML.parse(text);
74
- } catch (err) {
75
- throw formatYamlParseError(filePath, text, err as Error);
76
- }
77
-
78
- try {
79
- const suite = validateSuite(raw);
80
- suite.filePath = resolve(filePath);
81
- return suite;
82
- } catch (err) {
83
- if (err instanceof z.ZodError && !opts.verbose) {
84
- throw new Error(`Validation error in ${filePath}:\n${formatZodError(err)}`);
85
- }
86
- throw new Error(`Validation error in ${filePath}: ${(err as Error).message}`);
87
- }
88
- }
89
-
90
- /**
91
- * Files that live alongside test suites but aren't suites themselves. The
92
- * yaml-parser scans recursively from the workspace root, so picking these up
93
- * would surface spurious "Validation error: missing field name" noise.
94
- */
95
- function isNonSuiteYaml(file: string): boolean {
96
- if (file.match(/\.env(\..+)?\.ya?ml$/)) return true;
97
- // Workspace marker — present at the root of every zond workspace.
98
- if (file === "zond.config.yml" || file === "zond.config.yaml") return true;
99
- // Per-API artifact files written by `zond add api` / `zond refresh-api`.
100
- // Match the basename so it works for files at any depth (apis/<name>/...).
101
- const basename = file.split("/").pop() ?? file;
102
- if (/^\.api-[a-z0-9-]+\.ya?ml$/i.test(basename)) return true;
103
- return false;
104
- }
105
-
106
- export async function parseDirectory(dirPath: string, opts: ParseOptions = {}): Promise<TestSuite[]> {
107
- const glob = new Glob("**/*.{yaml,yml}");
108
- const suites: TestSuite[] = [];
109
-
110
- for await (const file of glob.scan({ cwd: dirPath, absolute: false })) {
111
- if (isNonSuiteYaml(file)) {
112
- continue;
113
- }
114
- const fullPath = `${dirPath}/${file}`;
115
- try {
116
- suites.push(await parseFile(fullPath, opts));
117
- } catch {
118
- // Skip files that fail to parse (e.g. invalid AI-generated YAML)
119
- // so one bad file doesn't block the entire directory
120
- }
121
- }
122
-
123
- return suites;
124
- }
125
-
126
- export interface ParseDirectoryResult {
127
- suites: TestSuite[];
128
- errors: { file: string; error: string }[];
129
- }
130
-
131
- export async function parseDirectorySafe(dirPath: string, opts: ParseOptions = {}): Promise<ParseDirectoryResult> {
132
- const glob = new Glob("**/*.{yaml,yml}");
133
- const suites: TestSuite[] = [];
134
- const errors: { file: string; error: string }[] = [];
135
-
136
- for await (const file of glob.scan({ cwd: dirPath, absolute: false })) {
137
- if (isNonSuiteYaml(file)) {
138
- continue;
139
- }
140
- const fullPath = `${dirPath}/${file}`;
141
- try {
142
- suites.push(await parseFile(fullPath, opts));
143
- } catch (err) {
144
- errors.push({ file, error: (err as Error).message });
145
- }
146
- }
147
-
148
- return { suites, errors };
149
- }
150
-
151
- export async function parse(path: string, opts: ParseOptions = {}): Promise<TestSuite[]> {
152
- const file = Bun.file(path);
153
- const exists = await file.exists();
154
-
155
- if (exists) {
156
- return [await parseFile(path, opts)];
157
- }
158
-
159
- // Not a file, try as directory
160
- return parseDirectory(path, opts);
161
- }
162
-
163
- /**
164
- * Like {@link parse}, but never silently drops files. Returns both successfully
165
- * parsed suites and per-file parse errors so callers (run, validate, tag-filter)
166
- * can surface failures instead of pretending the file did not exist.
167
- */
168
- export async function parseSafe(path: string, opts: ParseOptions = {}): Promise<ParseDirectoryResult> {
169
- const file = Bun.file(path);
170
- const exists = await file.exists();
171
-
172
- if (exists) {
173
- try {
174
- return { suites: [await parseFile(path, opts)], errors: [] };
175
- } catch (err) {
176
- return { suites: [], errors: [{ file: path, error: (err as Error).message }] };
177
- }
178
- }
179
-
180
- return parseDirectorySafe(path, opts);
181
- }
@@ -1,34 +0,0 @@
1
- /**
2
- * Probe registry bootstrap (m-17 / ARV-49).
3
- *
4
- * Called once from the CLI program init. Imports each Probe class and
5
- * runs `registerProbe`, which validates the contract from `types.ts`
6
- * and throws if a slot is missing. Boot-time failure is louder than
7
- * runtime — adding a new probe class without --dry-run / --report
8
- * support won't ship; that's the whole point of the m-17 contract.
9
- *
10
- * Idempotent: repeated calls are no-ops (matters for unit tests that
11
- * run the bootstrap multiple times).
12
- */
13
- import { listProbes, registerProbe } from "./registry.ts";
14
- import { SecurityProbe } from "./security-probe-class.ts";
15
- import { MassAssignmentProbe } from "./mass-assignment-probe-class.ts";
16
- import { StaticProbe } from "./static-probe-class.ts";
17
-
18
- let bootstrapped = false;
19
-
20
- export function bootstrapProbes(): void {
21
- if (bootstrapped) return;
22
- if (listProbes().length === 0) {
23
- registerProbe(new StaticProbe());
24
- registerProbe(new MassAssignmentProbe());
25
- registerProbe(new SecurityProbe());
26
- }
27
- bootstrapped = true;
28
- }
29
-
30
- /** Test helper — resets the singleton so the next `bootstrapProbes()`
31
- * re-registers from scratch. Pair with `clearProbes()` from registry. */
32
- export function resetBootstrap(): void {
33
- bootstrapped = false;
34
- }