@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,759 +0,0 @@
1
- import type { OpenAPIV3 } from "openapi-types";
2
-
3
- /**
4
- * Recursively generates test data from an OpenAPI schema.
5
- * Uses heuristic placeholders ({{$...}} generators) where possible.
6
- *
7
- * `forRequest` (default true) toggles request-body filters that strip
8
- * server-assigned fields the client must not send: properties marked
9
- * `readOnly: true`, and the literal field name `id` at any object level
10
- * (universally server-assigned in REST). Pass `forRequest: false` to
11
- * preserve full schema shape for response-side fixtures.
12
- */
13
- export function generateFromSchema(
14
- schema: OpenAPIV3.SchemaObject,
15
- propertyName?: string,
16
- opts: { _depth?: number; forRequest?: boolean } = {},
17
- ): unknown {
18
- const _depth = opts._depth ?? 0;
19
- const forRequest = opts.forRequest ?? true;
20
- const recurse = (s: OpenAPIV3.SchemaObject, name?: string) =>
21
- generateFromSchema(s, name, { _depth: _depth + 1, forRequest });
22
-
23
- if (_depth > 7) {
24
- return depthLimitDefault(schema, propertyName);
25
- }
26
-
27
- // Highest-priority signal: explicit example from spec.
28
- // Beats enum, format, heuristics — the spec author told us what to send.
29
- // Two exceptions:
30
- // 1. `null` examples are noise (often nullable: true with no real example) —
31
- // skip so we fall through to type/format defaults instead of emitting null.
32
- // 2. UUID-shaped examples on FK-context fields (name ends with `_id` or
33
- // schema.format === "uuid") are usually copy-pasted from another tenant's
34
- // spec. Honoring them leaks foreign IDs and guarantees 422 on real APIs;
35
- // `{{$uuid}}` is at least an honest test placeholder.
36
- //
37
- // OpenAPI 3.1 / JSON Schema also allows `examples: [...]` (plural array). When
38
- // both are present `example` wins; otherwise pick the first non-null entry
39
- // from `examples` and apply the same FK-UUID guard. `example` (singular) is
40
- // still the OpenAPI 3.0 form and remains supported.
41
- const exampleValue = pickExampleValue(schema);
42
- if (exampleValue !== undefined) {
43
- if (!isLikelyForeignFKExample(schema, propertyName, exampleValue)) {
44
- return exampleValue;
45
- }
46
- }
47
-
48
- // allOf: merge all schemas
49
- if (schema.allOf) {
50
- const merged: OpenAPIV3.SchemaObject = { type: "object", properties: {} };
51
- for (const sub of schema.allOf) {
52
- const s = sub as OpenAPIV3.SchemaObject;
53
- if (s.properties) {
54
- merged.properties = { ...merged.properties, ...s.properties };
55
- }
56
- }
57
- return recurse(merged, propertyName);
58
- }
59
-
60
- // oneOf / anyOf: pick the most informative variant. Prefer objects with
61
- // properties over loose primitives — APIs that accept `Array<{id}>|Array<string>`
62
- // need the object variant, not a string that 422s. Falls back to first
63
- // non-null entry.
64
- //
65
- // ARV-78 (feedback round-04 / F25): when the parent schema declares a
66
- // `discriminator: { propertyName, mapping? }` (typical OpenAPI 3 polymorphism —
67
- // /automations.steps with type=trigger|action), pick the variant whose
68
- // discriminator property carries a const/enum-single value and stamp that
69
- // value into the result. Without this, generator emits a random variant and
70
- // the API 422s with "Missing <required-by-other-variant>".
71
- if (schema.oneOf) {
72
- const variants = schema.oneOf as OpenAPIV3.SchemaObject[];
73
- const picked = pickBestVariant(variants, schema.discriminator);
74
- const result = recurse(picked, propertyName);
75
- return stampDiscriminator(result, picked, schema.discriminator);
76
- }
77
- if (schema.anyOf) {
78
- const variants = schema.anyOf as OpenAPIV3.SchemaObject[];
79
- const picked = pickBestVariant(variants, schema.discriminator);
80
- const result = recurse(picked, propertyName);
81
- return stampDiscriminator(result, picked, schema.discriminator);
82
- }
83
-
84
- // enum: first value (always valid for the API contract)
85
- if (schema.enum && schema.enum.length > 0) {
86
- return schema.enum[0];
87
- }
88
-
89
- // Format-based placeholders override type resolution. Schemas in the wild
90
- // commonly carry `format` without an explicit `type` (loosely-defined specs)
91
- // or with `type: ["string", "null"]` (OpenAPI 3.1 nullable). Falling through
92
- // to the type switch in those cases dropped us into the default branch and
93
- // produced `{{$randomString}}` for `format: email` — TASK-86 regression.
94
- const formatPlaceholder = formatToPlaceholder(schema.format);
95
- if (formatPlaceholder !== undefined) return formatPlaceholder;
96
-
97
- // OpenAPI 3.1: type can be `["string", "null"]`. Collapse to the first
98
- // non-null entry so the switch below routes correctly.
99
- let effectiveType = Array.isArray(schema.type)
100
- ? (schema.type as string[]).find(t => t !== "null") as OpenAPIV3.SchemaObject["type"] | undefined
101
- : schema.type;
102
-
103
- // ARV-67 (feedback round-01 / F7): schemas in the wild routinely omit
104
- // `type` on nested-object fields and rely on `properties` / `required`
105
- // / `items` to convey shape. Without the salvage below, the default
106
- // branch returns "{{$randomString}}" for a missing-type field — which
107
- // is what made `prepare-fixtures --seed` send a string for nested
108
- // objects like `automations.config` / `automations.steps` and earn
109
- // "Expected object, received string" 422s. Infer the type
110
- // from structural hints when nothing else gives one.
111
- if (effectiveType === undefined) {
112
- if ((schema as { items?: unknown }).items !== undefined) effectiveType = "array";
113
- else if (schema.properties || Array.isArray(schema.required)) effectiveType = "object";
114
- }
115
-
116
- switch (effectiveType) {
117
- case "string":
118
- return guessStringPlaceholder(schema, propertyName);
119
-
120
- case "integer":
121
- return guessIntPlaceholder(propertyName, schema);
122
-
123
- case "number":
124
- return 29.99;
125
-
126
- case "boolean":
127
- return true;
128
-
129
- case "array": {
130
- const arr = schema as OpenAPIV3.ArraySchemaObject;
131
- if (arr.items) {
132
- const item = recurse(arr.items as OpenAPIV3.SchemaObject, undefined);
133
- return [item];
134
- }
135
- return [];
136
- }
137
-
138
- case "object":
139
- default: {
140
- // Treat unknown type with properties as object
141
- if (schema.properties) {
142
- const obj: Record<string, unknown> = {};
143
- for (const [key, propSchema] of Object.entries(schema.properties)) {
144
- const ps = propSchema as OpenAPIV3.SchemaObject;
145
- if (forRequest && shouldSkipForRequest(key, ps)) continue;
146
- obj[key] = recurse(ps, key);
147
- }
148
- return obj;
149
- }
150
- // Record type (additionalProperties only). The historical behavior was
151
- // to materialize fake `key1`/`key2` entries to make the shape visible.
152
- // Real APIs reject those — the keys are always domain-specific
153
- // (filter names, label keys). Emit an empty `{}` instead: it preserves
154
- // the object type (so type-validators pass) without injecting payloads
155
- // the server didn't ask for. Callers who need realistic record content
156
- // should override via fixture-pack/.env.yaml.
157
- if (
158
- (schema.additionalProperties && typeof schema.additionalProperties === "object") ||
159
- schema.additionalProperties === true
160
- ) {
161
- return {};
162
- }
163
- // Bare object with no properties
164
- if (effectiveType === "object") {
165
- return {};
166
- }
167
- return "{{$randomString}}";
168
- }
169
- }
170
- }
171
-
172
- /** Fields the client must not send in a request body: explicit `readOnly: true`,
173
- * or the literal name `id`. The latter is a heuristic for under-specified specs
174
- * (common in in-house APIs) that don't mark the server-assigned id readOnly
175
- * but still 4xx on it being present.
176
- *
177
- * Also strips Stripe-style `expand` meta-param when it's declared as a string
178
- * array — Stripe doesn't mark it `readOnly` but rejects synthetic random
179
- * values with 400 "This property cannot be expanded (<random>)", which kills
180
- * 50+ baseline POSTs in mass-assignment / stateful checks on Stripe specs.
181
- * The shape check (array of strings on a request body) keeps false-positives
182
- * low: APIs using `expand` for real payload fields would normally use an
183
- * object/enum, not a free-string array. */
184
- function shouldSkipForRequest(name: string, schema: OpenAPIV3.SchemaObject): boolean {
185
- if (schema.readOnly === true) return true;
186
- if (name === "id") return true;
187
- if (name === "expand" && isStringArray(schema)) return true;
188
- return false;
189
- }
190
-
191
- function isStringArray(schema: OpenAPIV3.SchemaObject): boolean {
192
- if (schema.type !== "array") return false;
193
- const items = schema.items as OpenAPIV3.SchemaObject | undefined;
194
- if (!items) return false;
195
- return items.type === "string";
196
- }
197
-
198
- /** When recursion hits the depth cap, return a type-appropriate placeholder
199
- * rather than `{}` — `{}` for `array<string>` produces `[{}]` which 422s on
200
- * every realistic API. */
201
- function depthLimitDefault(schema: OpenAPIV3.SchemaObject, name?: string): unknown {
202
- const t = Array.isArray(schema.type)
203
- ? (schema.type as string[]).find(x => x !== "null")
204
- : schema.type;
205
- switch (t) {
206
- case "string": return formatToPlaceholder(schema.format) ?? guessStringPlaceholder(schema, name);
207
- case "integer": return 1;
208
- case "number": return 1;
209
- case "boolean": return true;
210
- case "array": return [];
211
- case "object":
212
- default: return {};
213
- }
214
- }
215
-
216
- /** ARV-135 (m-21): score-based oneOf/anyOf variant selection.
217
- *
218
- * Picks the variant most likely to produce a body the server accepts:
219
- *
220
- * 1. Drop `type: "null"` shorthands unless they're the only choice
221
- * (OpenAPI 3.1 nullable spelling).
222
- * 2. Prefer the variant with the fewest UNRESOLVABLE required fields
223
- * — i.e. required keys that have no entry in `properties`, which
224
- * the generator can't synthesise. The historical bug was picking
225
- * the FIRST variant matching the discriminator filter even when a
226
- * sibling variant was demonstrably more complete; this leaves
227
- * `required: [config, event_name]` partially-filled and the API
228
- * 422s with "Missing config, event_name" (F24/Resend automations).
229
- * 3. Among ties, prefer object-typed variants over primitives
230
- * (TASK-222: `Array<{id}>|Array<string>` should pick the object).
231
- * 4. Prefer the variant with more declared properties (richer surface
232
- * is closer to what real callers send).
233
- * 5. When a `discriminator.propertyName` is present, treat variants
234
- * that carry a single-value enum/const for that property as more
235
- * authoritative — they tie-break ahead of variants without one.
236
- *
237
- * The discriminator's `mapping` is used to derive the stamped value
238
- * even when the picked variant lacks an inline enum/const: typical
239
- * specs (Stripe, Linear) declare mapping centrally and omit the value
240
- * on each variant.
241
- */
242
- function pickBestVariant(
243
- variants: OpenAPIV3.SchemaObject[],
244
- discriminator: OpenAPIV3.DiscriminatorObject | undefined,
245
- ): OpenAPIV3.SchemaObject {
246
- const isNull = (s: OpenAPIV3.SchemaObject) => (s as { type?: unknown }).type === "null";
247
- const nonNull = variants.filter(v => !isNull(v));
248
- const pool = nonNull.length > 0 ? nonNull : variants;
249
-
250
- const discriminatorKey = discriminator?.propertyName;
251
- const hasDiscriminatorEnum = (v: OpenAPIV3.SchemaObject): boolean => {
252
- if (!discriminatorKey) return false;
253
- const prop = v.properties?.[discriminatorKey] as OpenAPIV3.SchemaObject | undefined;
254
- if (!prop) return false;
255
- const en = (prop as { enum?: unknown[] }).enum;
256
- const cn = (prop as { const?: unknown }).const;
257
- return (Array.isArray(en) && en.length === 1) || (cn !== undefined && cn !== null);
258
- };
259
-
260
- const score = (v: OpenAPIV3.SchemaObject) => {
261
- const req = (v.required ?? []) as string[];
262
- const props = v.properties ?? {};
263
- const unresolvable = req.filter(r => !(r in props)).length;
264
- const propCount = Object.keys(props).length;
265
- const isObjectWithProps = v.type === "object" && propCount > 0;
266
- return {
267
- unresolvable,
268
- hasDiscriminator: hasDiscriminatorEnum(v) ? 1 : 0,
269
- isObjectWithProps: isObjectWithProps ? 1 : 0,
270
- propCount,
271
- };
272
- };
273
-
274
- // Sort: fewer unresolvable → has discriminator → object-with-props → more props.
275
- // Sort is stable, so original spec order breaks remaining ties.
276
- const ranked = [...pool].sort((a, b) => {
277
- const sa = score(a);
278
- const sb = score(b);
279
- if (sa.unresolvable !== sb.unresolvable) return sa.unresolvable - sb.unresolvable;
280
- if (sa.hasDiscriminator !== sb.hasDiscriminator) return sb.hasDiscriminator - sa.hasDiscriminator;
281
- if (sa.isObjectWithProps !== sb.isObjectWithProps) return sb.isObjectWithProps - sa.isObjectWithProps;
282
- return sb.propCount - sa.propCount;
283
- });
284
-
285
- return ranked[0]!;
286
- }
287
-
288
- /** Stamp the discriminator key onto a generated object. Without this the
289
- * variant choice is "anonymous" from the body's point of view — APIs that
290
- * switch on `type` reject the request even when every other field is
291
- * perfect.
292
- *
293
- * ARV-135: now also honours `discriminator.mapping` — when the picked
294
- * variant has no inline enum/const for the discriminator property, fall
295
- * back to the first mapping key. Specs that declare polymorphism via
296
- * central mapping (rather than inline `enum: ["x"]` on each variant)
297
- * previously left the body un-stamped. */
298
- function stampDiscriminator(
299
- result: unknown,
300
- variant: OpenAPIV3.SchemaObject,
301
- discriminator: OpenAPIV3.DiscriminatorObject | undefined,
302
- ): unknown {
303
- const propertyName = discriminator?.propertyName;
304
- if (!propertyName) return result;
305
- if (!result || typeof result !== "object" || Array.isArray(result)) return result;
306
- let stamp: unknown;
307
- const prop = variant.properties?.[propertyName] as OpenAPIV3.SchemaObject | undefined;
308
- if (prop) {
309
- const en = (prop as { enum?: unknown[] }).enum;
310
- const cn = (prop as { const?: unknown }).const;
311
- if (Array.isArray(en) && en.length === 1) stamp = en[0];
312
- else if (cn !== undefined && cn !== null) stamp = cn;
313
- }
314
- if (stamp === undefined && discriminator?.mapping) {
315
- const keys = Object.keys(discriminator.mapping);
316
- if (keys.length > 0) stamp = keys[0];
317
- }
318
- if (stamp === undefined) return result;
319
- (result as Record<string, unknown>)[propertyName] = stamp;
320
- return result;
321
- }
322
-
323
- const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
324
-
325
- /** Names that strongly imply an email field. Kept in sync with the email
326
- * branch of `guessStringPlaceholder`/`classifyFieldSource`. Used to gate the
327
- * description-based domain heuristic so phrases like "verified sending
328
- * domain" in the description of a `from`/`to` field don't override the
329
- * email mapping. */
330
- function isEmailContextName(name?: string): boolean {
331
- if (!name) return false;
332
- // Canonical snake_case so camelCase (`replyTo`, `userEmail`) matches the
333
- // same rules as `reply_to` / `user_email`.
334
- const lower = canonicalVarName(name);
335
- return (
336
- lower === "email" ||
337
- lower === "from" ||
338
- lower === "to" ||
339
- lower === "cc" ||
340
- lower === "bcc" ||
341
- lower === "sender" ||
342
- lower === "recipient" ||
343
- lower === "reply_to" ||
344
- lower.endsWith("_email") ||
345
- lower.endsWith("_reply_to") ||
346
- lower.endsWith("_from") ||
347
- lower.endsWith("_to") ||
348
- lower.endsWith("_cc") ||
349
- lower.endsWith("_bcc")
350
- );
351
- }
352
-
353
- /** A schema `pattern` that explicitly allows lowercase but not uppercase
354
- * letters (typical slug regex like `^[a-z0-9_\-]+$`). Used to switch from
355
- * mixed-case `{{$randomString}}` to a slug-shaped generator. */
356
- function isLowercaseOnlyPattern(pattern: string | undefined): boolean {
357
- if (!pattern) return false;
358
- return pattern.includes("a-z") && !pattern.includes("A-Z");
359
- }
360
-
361
- /** A string example shaped like a UUID, on a field that looks like a foreign
362
- * key (name ends with `_id` or schema declares `format: uuid`), is almost
363
- * always a tenant-specific value the spec author left in `example:`. Sending
364
- * it verbatim guarantees 422 on a fresh account and leaks foreign IDs. */
365
- function isLikelyForeignFKExample(
366
- schema: OpenAPIV3.SchemaObject,
367
- name?: string,
368
- value?: unknown,
369
- ): boolean {
370
- const ex = value !== undefined ? value : schema.example;
371
- if (typeof ex !== "string") return false;
372
- if (!UUID_RE.test(ex)) return false;
373
- const fkByName = !!name && name.toLowerCase().endsWith("_id");
374
- const fkByFormat = schema.format === "uuid";
375
- return fkByName || fkByFormat;
376
- }
377
-
378
- /** Resolve the effective example value from a schema, supporting both
379
- * OpenAPI 3.0 `example` (singular) and OpenAPI 3.1 / JSON Schema `examples`
380
- * (plural array). `example` wins when both are set — it's the more
381
- * intentional, single-source signal. `null` is treated as "no example"
382
- * (see TASK-221). For `examples`, we pick the first non-null entry. */
383
- function pickExampleValue(schema: OpenAPIV3.SchemaObject): unknown {
384
- if (schema.example !== undefined && schema.example !== null) {
385
- return schema.example;
386
- }
387
- const examples = (schema as { examples?: unknown }).examples;
388
- if (Array.isArray(examples)) {
389
- for (const ex of examples) {
390
- if (ex !== null && ex !== undefined) return ex;
391
- }
392
- }
393
- return undefined;
394
- }
395
-
396
- /**
397
- * TASK-269 — per-field provenance for `zond generate --explain`.
398
- *
399
- * Returns a label describing *why* `generateFromSchema` would emit the
400
- * value it does for a given (schema, propertyName). Mirrors the dispatch
401
- * priority in `generateFromSchema` without producing the value, so
402
- * `--explain` can show "name → {{$randomName}} [heuristic:name]" without
403
- * re-executing generation.
404
- *
405
- * Kept as a parallel function instead of refactoring `generateFromSchema`
406
- * to record sources — the recursion path-tracking complexity would
407
- * outweigh the value for what is currently a debug-only surface. The
408
- * heuristic order here MUST stay in lockstep with the function above; a
409
- * unit test (data-factory.test.ts) pins the labels for each branch.
410
- */
411
- export type FieldSource =
412
- | "example"
413
- | "examples"
414
- | "enum"
415
- | "format"
416
- | "pattern"
417
- | "min"
418
- | "max"
419
- | "random"
420
- | "default"
421
- | `heuristic:${string}`;
422
-
423
- export function classifyFieldSource(
424
- schema: OpenAPIV3.SchemaObject,
425
- propertyName?: string,
426
- ): FieldSource {
427
- // example > examples (3.1) — same FK-UUID guard as generateFromSchema.
428
- if (schema.example !== undefined && schema.example !== null) {
429
- if (!isLikelyForeignFKExample(schema, propertyName, schema.example)) {
430
- return "example";
431
- }
432
- }
433
- const examples = (schema as { examples?: unknown }).examples;
434
- if (Array.isArray(examples)) {
435
- for (const ex of examples) {
436
- if (ex === null || ex === undefined) continue;
437
- if (!isLikelyForeignFKExample(schema, propertyName, ex)) return "examples";
438
- break;
439
- }
440
- }
441
- if (schema.enum && schema.enum.length > 0) return "enum";
442
- if (formatToPlaceholder(schema.format) !== undefined) return "format";
443
-
444
- const t = Array.isArray(schema.type)
445
- ? (schema.type as string[]).find(x => x !== "null")
446
- : schema.type;
447
-
448
- if (t === "string") {
449
- // ARV-38: keep --explain in sync with guessStringPlaceholder — when a
450
- // default is consumed, label the source as "default", not "random".
451
- if (typeof schema.default === "string" && schema.default.length > 0) return "default";
452
- if (isLowercaseOnlyPattern(schema.pattern)) return "pattern";
453
- if (
454
- schema.description &&
455
- /\b(domain|hostname|fqdn)\b/i.test(schema.description) &&
456
- !isEmailContextName(propertyName)
457
- ) {
458
- return "heuristic:domain-from-description";
459
- }
460
- if (propertyName) {
461
- const lower = canonicalVarName(propertyName);
462
- if (lower === "slug" || lower.endsWith("_slug")) return "heuristic:slug";
463
- if (lower === "domain" || lower === "hostname" || lower === "fqdn" || lower.endsWith("_domain")) return "heuristic:domain";
464
- if (lower === "platform") return "heuristic:platform";
465
- if (lower === "language" || lower === "lang" || lower === "locale") return "heuristic:locale";
466
- if (lower === "country" || lower === "country_code" || lower.endsWith("_country") || lower.endsWith("_country_code")) return "heuristic:country";
467
- if (lower === "timezone" || lower === "time_zone" || lower === "tz") return "heuristic:timezone";
468
- if (lower === "currency" || lower === "currency_code" || lower.endsWith("_currency") || lower.endsWith("_currency_code")) return "heuristic:currency";
469
- if (lower === "mcc" || lower.endsWith("_mcc") || lower === "merchant_category_code") return "heuristic:mcc";
470
- if (lower === "color" || lower.endsWith("_color") || lower === "background_color" || lower === "hex" || lower.endsWith("_hex_color")) return "heuristic:color";
471
- if (lower === "ip" || lower === "ip_address" || lower.endsWith("_ip") || lower.endsWith("_ip_address")) return "heuristic:ip";
472
- if (
473
- lower === "email" || lower === "from" || lower === "to" || lower === "cc" ||
474
- lower === "bcc" || lower === "sender" || lower === "recipient" ||
475
- lower === "reply_to" ||
476
- lower.endsWith("_email") ||
477
- lower.endsWith("_reply_to") || lower.endsWith("_from") ||
478
- lower.endsWith("_to") || lower.endsWith("_cc") || lower.endsWith("_bcc")
479
- ) return "heuristic:email";
480
- if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) return "heuristic:id";
481
- if (lower === "name" || lower.endsWith("_name")) return "heuristic:name";
482
- if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") return "heuristic:url";
483
- if (lower === "password" || lower.endsWith("_password")) return "heuristic:password";
484
- if (lower === "phone" || lower === "telephone" || lower.endsWith("_phone")) return "heuristic:phone";
485
- }
486
- return "random";
487
- }
488
-
489
- if (t === "integer") {
490
- if (schema.maximum !== undefined) return "max";
491
- if (schema.minimum !== undefined && schema.minimum > 0) return "min";
492
- return "random";
493
- }
494
-
495
- if (t === "number" || t === "boolean") return "default";
496
- return "default";
497
- }
498
-
499
- /**
500
- * ARV-138: canonicalise a body field name to a manifest/fixture var name.
501
- * Converts camelCase → snake_case + lowercase so spec body fields
502
- * (`issueId`, `sequenceTypeCode`) collapse onto the same var as the
503
- * path-param spelling. Idempotent on already-snake_case input. The HTTP
504
- * request still sends the raw field name — only the {{var}} namespace is
505
- * normalised. Lives here (leaf module) so both fixtures-builder and
506
- * suite-generator can share it without an import cycle.
507
- */
508
- export function canonicalVarName(name: string): string {
509
- return name
510
- .replace(/([a-z0-9])([A-Z])/g, "$1_$2")
511
- .replace(/[^a-zA-Z0-9]+/g, "_")
512
- .toLowerCase();
513
- }
514
-
515
- const FK_ID_SUFFIX = /(?:_id|Id|_uuid)$/;
516
- const CODE_REF_SUFFIX = /(?:_code|Code)$/;
517
-
518
- /**
519
- * ARV-45: a required request-body field that is a foreign-key / closed-
520
- * vocabulary reference the generator can't synthesise a valid value for —
521
- * so the generated test should reference a `{{fixture}}` the user fills in
522
- * `.env.yaml`, not random junk that 400s and kills the CRUD chain at step 1.
523
- *
524
- * - `*_id` / `*Id` / `*_uuid`: always an FK. `generateFromSchema` emits
525
- * `{{$uuid}}` here — a random id that 404/422s on a live API.
526
- * - `*_code` / `*Code`: a reference code ONLY when no heuristic resolves
527
- * it. `countryCode`/`currencyCode`/`mcc`… already map to real literals
528
- * via `guessStringPlaceholder`, so they're excluded; the domain codes
529
- * that motivated this (`sequenceTypeCode`, `templateGroupCode`) fall
530
- * through to `{{$randomString}}` → 400.
531
- *
532
- * Enum/format/example-backed fields are never FK fixtures — the spec
533
- * already told us a valid value.
534
- */
535
- export function isFkFixtureField(name: string, schema: OpenAPIV3.SchemaObject): boolean {
536
- if (schema.enum && schema.enum.length > 0) return false;
537
- if (pickExampleValue(schema) !== undefined) return false;
538
- if (FK_ID_SUFFIX.test(name)) return true;
539
- if (CODE_REF_SUFFIX.test(name)) {
540
- return generateFromSchema(schema, name) === "{{$randomString}}";
541
- }
542
- return false;
543
- }
544
-
545
- /**
546
- * Flatten a request-body schema to its effective `{properties, required}`,
547
- * merging `allOf` branches. .NET / Swagger-gen specs wrap almost every model
548
- * in `allOf: [{...}]` (inheritance), so a direct `schema.properties` read sees
549
- * nothing — `generateFromSchema` already merges allOf when producing the body,
550
- * and FK wiring / the manifest must resolve the same shape or they silently
551
- * miss every FK field on these specs. Recurses through nested allOf; ignores
552
- * oneOf/anyOf (the generator picks one variant there, out of scope for FK
553
- * wiring until a real spec needs it).
554
- */
555
- export function effectiveObjectShape(
556
- schema: OpenAPIV3.SchemaObject,
557
- ): { properties: Record<string, OpenAPIV3.SchemaObject>; required: Set<string> } {
558
- const properties: Record<string, OpenAPIV3.SchemaObject> = {};
559
- const required = new Set<string>();
560
- const visit = (s: OpenAPIV3.SchemaObject | undefined) => {
561
- if (!s || typeof s !== "object") return;
562
- if (Array.isArray(s.allOf)) {
563
- for (const sub of s.allOf) visit(sub as OpenAPIV3.SchemaObject);
564
- }
565
- if (s.properties) {
566
- for (const [k, v] of Object.entries(s.properties)) {
567
- properties[k] = v as OpenAPIV3.SchemaObject;
568
- }
569
- }
570
- if (Array.isArray(s.required)) for (const r of s.required) required.add(r);
571
- };
572
- visit(schema);
573
- return { properties, required };
574
- }
575
-
576
- /**
577
- * Map an OpenAPI `format` value to a zond generator placeholder. Returns
578
- * undefined when the format is unknown or absent so callers can fall back
579
- * to type / property-name heuristics. Exported for tests.
580
- */
581
- export function formatToPlaceholder(format: string | undefined): string | undefined {
582
- switch (format) {
583
- case "email": return "{{$randomEmail}}";
584
- case "uuid": return "{{$uuid}}";
585
- case "date-time": return "{{$randomIsoDate}}";
586
- case "date": return "{{$randomDate}}";
587
- case "uri":
588
- case "url": return "{{$randomUrl}}";
589
- case "hostname": return "{{$randomFqdn}}";
590
- case "ipv4": return "{{$randomIpv4}}";
591
- case "ipv6": return "::1";
592
- case "password": return "TestPass123!";
593
- // ARV-165: format-aware helpers. None of these are standard OpenAPI 3.x
594
- // formats, but Stripe/GitHub/Shopify/Twilio specs frequently carry them
595
- // as ad-hoc `format:` tags. Falling through to {{$randomString}} guarantees
596
- // 400 from format-validated APIs (R09 finding: 199 hit-but-fail Stripe steps).
597
- case "iso-country-code":
598
- case "country-code":
599
- case "country": return "{{$randomCountryCode}}";
600
- case "iso-currency-code":
601
- case "currency-code":
602
- case "currency": return "{{$randomCurrencyCode}}";
603
- case "mcc": return "{{$randomMCC}}";
604
- case "color":
605
- case "hex-color":
606
- case "rgb-hex": return "{{$randomColorHex}}";
607
- default: return undefined;
608
- }
609
- }
610
-
611
- /**
612
- * Generate a multipart body object from an OpenAPI multipart/form-data schema.
613
- * Binary fields (format: binary/byte) become file upload objects; all others become strings.
614
- */
615
- export function generateMultipartFromSchema(
616
- schema: OpenAPIV3.SchemaObject,
617
- ): Record<string, unknown> {
618
- const result: Record<string, unknown> = {};
619
-
620
- if (!schema.properties) return result;
621
-
622
- for (const [key, propSchema] of Object.entries(schema.properties)) {
623
- const s = propSchema as OpenAPIV3.SchemaObject;
624
- if (shouldSkipForRequest(key, s)) continue;
625
- if (s.format === "binary" || s.format === "byte") {
626
- result[key] = { file: `./fixtures/${key}.bin`, content_type: "application/octet-stream" };
627
- } else {
628
- const val = generateFromSchema(s, key);
629
- result[key] = val;
630
- }
631
- }
632
-
633
- return result;
634
- }
635
-
636
- function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string): string {
637
- // Format-based dispatch already happened earlier in generateFromSchema;
638
- // this branch only sees strings whose format is empty or unrecognised.
639
-
640
- // ARV-38: when the spec declares a JSON-Schema `default` for a string-typed
641
- // field with no enum, prefer it over heuristics. PATCH endpoints in
642
- // particular rely on this — e.g. a `PATCH /domains/{id}` with
643
- // `tls: { type: string, default: "opportunistic" }` would otherwise get
644
- // a random fallback and a guaranteed 422 every run.
645
- if (typeof schema.default === "string" && schema.default.length > 0) {
646
- return schema.default;
647
- }
648
-
649
- // Pattern-aware: many specs constrain slugs via regex like
650
- // `^(?![0-9]+$)[a-z0-9_\-]+$` without setting `format`. Default
651
- // `{{$randomString}}` mixes upper+lower → 400 from the validator.
652
- // Heuristic: pattern allows `a-z` but forbids `A-Z` → emit a slug.
653
- if (isLowercaseOnlyPattern(schema.pattern)) {
654
- return "{{$randomSlug}}";
655
- }
656
-
657
- // Description-aware: when the schema describes a domain/hostname (e.g.
658
- // a `POST /domains/`-style endpoint or DNS-zone create route) but the
659
- // field is generically named `name`, the default `{{$randomName}}`
660
- // returns "Bob Wilson" and the server rejects it. TASK-224.
661
- // Skip when the field name is clearly in email vocabulary — email-API
662
- // specs often describe `from`/`to`/etc. with phrases like "verified
663
- // sending domain" or "Name <user@domain>", which trips the regex but
664
- // the field is an email, not a domain. Email vocab > domain-from-description.
665
- if (
666
- schema.description &&
667
- /\b(domain|hostname|fqdn)\b/i.test(schema.description) &&
668
- !isEmailContextName(name)
669
- ) {
670
- return "{{$randomDomain}}";
671
- }
672
-
673
- // Name-based heuristics. Match on the canonical snake_case form so
674
- // camelCase fields (`countryCode`, `firstName`, `userEmail`) hit the same
675
- // rules as their snake_case spelling — otherwise they fall through to
676
- // {{$randomString}} and 400 on closed-vocab/format-strict APIs.
677
- if (name) {
678
- const lower = canonicalVarName(name);
679
- if (lower === "slug" || lower.endsWith("_slug")) {
680
- return "{{$randomSlug}}";
681
- }
682
- if (lower === "domain" || lower === "hostname" || lower === "fqdn" || lower.endsWith("_domain")) {
683
- return "{{$randomDomain}}";
684
- }
685
- // Closed-vocabulary fields where servers validate against an internal
686
- // dictionary even when the spec lacks `enum:`. Random strings → 400.
687
- // Pick the most universally-accepted value per dictionary.
688
- if (lower === "platform") return "python";
689
- if (lower === "language" || lower === "lang" || lower === "locale") return "en";
690
- // ARV-165: country/currency literals (US/USD) were universally accepted
691
- // but offered zero variety — added endsWith() patterns so nested fields
692
- // like `bank_account.country`, `payout.currency_code`, `from_country`
693
- // also resolve. Still emit a literal — picking from the random helper
694
- // would weaken the "always-valid" property for downstream assertions
695
- // that pin on the first value.
696
- if (lower === "country" || lower === "country_code" || lower.endsWith("_country") || lower.endsWith("_country_code")) return "US";
697
- if (lower === "timezone" || lower === "time_zone" || lower === "tz") return "UTC";
698
- if (lower === "currency" || lower === "currency_code" || lower.endsWith("_currency") || lower.endsWith("_currency_code")) return "USD";
699
- // ARV-165: MCC (merchant category code) — Stripe/Square/issuing APIs.
700
- // Random {{$randomString}} → 400 because it's not a 4-digit code.
701
- if (lower === "mcc" || lower.endsWith("_mcc") || lower === "merchant_category_code") return "{{$randomMCC}}";
702
- // ARV-165: hex color — Stripe brand settings, Slack themes, GitHub labels.
703
- if (lower === "color" || lower.endsWith("_color") || lower === "background_color" || lower === "hex" || lower.endsWith("_hex_color")) return "{{$randomColorHex}}";
704
- // ARV-165: IP addresses — Stripe tos_acceptance.ip, audit logs, fraud APIs.
705
- if (lower === "ip" || lower === "ip_address" || lower.endsWith("_ip") || lower.endsWith("_ip_address")) return "{{$randomIpv4}}";
706
- // Email-context fields. Email-API specs often
707
- // omit `format: email` on `from`/`to`/`reply_to`/`cc`/`bcc` — the field
708
- // name is the only clue, and `{{$randomString}}` guarantees a 422.
709
- if (
710
- lower === "email" ||
711
- lower === "from" ||
712
- lower === "to" ||
713
- lower === "cc" ||
714
- lower === "bcc" ||
715
- lower === "sender" ||
716
- lower === "recipient" ||
717
- lower === "reply_to" ||
718
- lower.endsWith("_email") ||
719
- lower.endsWith("_reply_to") ||
720
- lower.endsWith("_from") ||
721
- lower.endsWith("_to") ||
722
- lower.endsWith("_cc") ||
723
- lower.endsWith("_bcc")
724
- ) {
725
- return "{{$randomEmail}}";
726
- }
727
- if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) {
728
- return "{{$uuid}}";
729
- }
730
- if (lower === "name" || lower.endsWith("_name")) {
731
- return "{{$randomName}}";
732
- }
733
- if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") {
734
- return "{{$randomUrl}}";
735
- }
736
- if (lower === "password" || lower.endsWith("_password")) {
737
- return "TestPass123!";
738
- }
739
- if (lower === "phone" || lower === "telephone" || lower.endsWith("_phone")) {
740
- return "+1234567890";
741
- }
742
- }
743
-
744
- return "{{$randomString}}";
745
- }
746
-
747
- function guessIntPlaceholder(name?: string, schema?: OpenAPIV3.SchemaObject): number | string {
748
- const min = schema?.minimum;
749
- const max = schema?.maximum;
750
- if (max !== undefined) {
751
- // Use a safe concrete value within the declared range
752
- const lo = min !== undefined && min > 0 ? min : 1;
753
- return Math.min(lo, max);
754
- }
755
- if (min !== undefined && min > 0) {
756
- return min;
757
- }
758
- return "{{$randomInt}}";
759
- }