@kirrosh/zond 0.23.0 → 0.26.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.
- package/CHANGELOG.md +164 -1
- package/README.md +8 -7
- package/package.json +2 -3
- package/src/CLAUDE.md +112 -0
- package/src/cli/commands/add-api.ts +19 -7
- package/src/cli/commands/api/annotate/index.ts +359 -4
- package/src/cli/commands/api/annotate/lifecycle.ts +1 -1
- package/src/cli/commands/api/annotate/overlay.ts +1 -1
- package/src/cli/commands/api/annotate/pagination.ts +10 -6
- package/src/cli/commands/api/annotate/prompts.ts +39 -2
- package/src/cli/commands/audit.ts +360 -54
- package/src/cli/commands/check.ts +15 -2
- package/src/cli/commands/checks.ts +352 -36
- package/src/cli/commands/cleanup.ts +4 -30
- package/src/cli/commands/coverage.ts +275 -57
- package/src/cli/commands/db.ts +311 -8
- package/src/cli/commands/discover.ts +281 -161
- package/src/cli/commands/doctor.ts +57 -3
- package/src/cli/commands/fixtures.ts +1 -1
- package/src/cli/commands/generate.ts +24 -7
- package/src/cli/commands/init/bootstrap.ts +4 -1
- package/src/cli/commands/init/index.ts +2 -2
- package/src/cli/commands/init/skills.ts +43 -0
- package/src/cli/commands/init/templates/agents.md +12 -3
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +268 -44
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +88 -26
- package/src/cli/commands/init/templates/skills/zond.md +274 -64
- package/src/cli/commands/init/templates/zond-config.yml +1 -1
- package/src/cli/commands/prepare-fixtures.ts +14 -52
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +101 -10
- package/src/cli/commands/probe/security.ts +95 -12
- package/src/cli/commands/probe/webhooks.ts +2 -0
- package/src/cli/commands/probe.ts +87 -11
- package/src/cli/commands/refresh-api.ts +59 -1
- package/src/cli/commands/report-bundle.ts +3 -11
- package/src/cli/commands/request.ts +116 -0
- package/src/cli/commands/run.ts +33 -5
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/json-envelope.ts +0 -20
- package/src/cli/json-schemas.ts +51 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +5 -4
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +0 -10
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/cross_call_references.ts +17 -4
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/idempotency_replay.ts +1 -5
- package/src/core/checks/checks/ignored_auth.ts +44 -1
- package/src/core/checks/checks/index.ts +3 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +169 -26
- package/src/core/checks/checks/negative_data_rejection.ts +119 -16
- package/src/core/checks/checks/not_a_server_error.ts +8 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +47 -18
- package/src/core/checks/checks/pagination_invariants.ts +298 -117
- package/src/core/checks/checks/positive_data_acceptance.ts +1 -4
- package/src/core/checks/checks/status_code_conformance.ts +78 -7
- package/src/core/checks/mode.ts +3 -0
- package/src/core/checks/recommended-action.ts +5 -1
- package/src/core/checks/runner.ts +614 -27
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/types.ts +117 -1
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +35 -6
- package/src/core/coverage/loader.ts +31 -0
- package/src/core/diagnostics/db-analysis.ts +200 -106
- package/src/core/diagnostics/failure-class.ts +21 -1
- package/src/core/diagnostics/failure-hints.ts +4 -208
- package/src/core/diagnostics/suggested-fixes.ts +2 -3
- package/src/core/generator/chunker.ts +1 -8
- package/src/core/generator/data-factory.ts +199 -61
- package/src/core/generator/fixtures-builder.ts +38 -31
- package/src/core/generator/index.ts +0 -2
- package/src/core/generator/openapi-reader.ts +98 -4
- package/src/core/generator/path-param-disambig.ts +30 -4
- package/src/core/generator/resources-builder.ts +276 -26
- package/src/core/generator/schema-utils.ts +22 -0
- package/src/core/generator/suite-generator.ts +168 -15
- package/src/core/generator/types.ts +6 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/output/README.md +11 -29
- package/src/core/output/index.ts +1 -1
- package/src/core/output/run.ts +0 -35
- package/src/core/output/types.ts +0 -7
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/variables.ts +0 -0
- package/src/core/probe/dry-run-envelope.ts +4 -0
- package/src/core/probe/mass-assignment/classify.ts +175 -0
- package/src/core/probe/mass-assignment/cleanup.ts +52 -0
- package/src/core/probe/mass-assignment/digest.ts +114 -0
- package/src/core/probe/mass-assignment/orchestrator.ts +459 -0
- package/src/core/probe/mass-assignment/regression.ts +141 -0
- package/src/core/probe/mass-assignment/suspects.ts +92 -0
- package/src/core/probe/mass-assignment/types.ts +135 -0
- package/src/core/probe/mass-assignment-probe.ts +23 -1118
- package/src/core/probe/mass-assignment-template.ts +32 -4
- package/src/core/probe/path-discovery.ts +3 -4
- package/src/core/probe/probe-harness.ts +21 -22
- package/src/core/probe/security/baseline.ts +174 -0
- package/src/core/probe/security/classify.ts +341 -0
- package/src/core/probe/security/cleanup.ts +125 -0
- package/src/core/probe/security/detectors.ts +71 -0
- package/src/core/probe/security/digest.ts +104 -0
- package/src/core/probe/security/orchestrator.ts +398 -0
- package/src/core/probe/security/regression.ts +103 -0
- package/src/core/probe/security/types.ts +151 -0
- package/src/core/probe/security-probe-class.ts +8 -2
- package/src/core/probe/security-probe.ts +28 -1449
- package/src/core/probe/shared.ts +26 -0
- package/src/core/probe/webhooks-probe.ts +5 -7
- package/src/core/runner/assertions.ts +1 -1
- package/src/core/runner/executor.ts +3 -18
- package/src/core/runner/form-encode.ts +8 -18
- package/src/core/runner/http-client.ts +38 -1
- package/src/core/runner/preflight-vars.ts +19 -15
- package/src/core/runner/rate-limiter.ts +11 -29
- package/src/core/runner/run-kind.ts +7 -1
- package/src/core/runner/schema-validator.ts +2 -6
- package/src/core/runner/send-request.ts +11 -6
- package/src/core/runner/types.ts +6 -0
- package/src/core/setup-api.ts +53 -15
- package/src/core/severity/index.ts +0 -63
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/merge-specs.ts +156 -0
- package/src/core/spec/schema-from-runs.ts +117 -0
- package/src/core/spec/schema-overlay.ts +130 -0
- package/src/core/util/ajv.ts +13 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/db/migrate.ts +2 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/queries/collections.ts +2 -2
- package/src/db/queries/results.ts +88 -0
- package/src/db/queries/runs.ts +56 -2
- package/src/db/queries.ts +3 -0
- package/src/db/schema.ts +7 -7
- package/src/cli/commands/bootstrap.ts +0 -710
- package/src/core/anti-fp/bootstrap.ts +0 -34
- package/src/core/anti-fp/index.ts +0 -33
- package/src/core/anti-fp/registry.ts +0 -44
- package/src/core/anti-fp/rules/baseline-echo.ts +0 -74
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +0 -52
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +0 -38
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +0 -35
- package/src/core/anti-fp/rules/schemathesis/index.ts +0 -24
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +0 -53
- package/src/core/anti-fp/rules/subscription-gated/index.ts +0 -11
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +0 -75
- package/src/core/anti-fp/types.ts +0 -68
- package/src/core/generator/create-body.ts +0 -89
|
@@ -1,44 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Failure classification
|
|
3
|
-
*
|
|
2
|
+
* Failure classification → closed-enum `recommended_action`.
|
|
3
|
+
* ARV-338: prose hints (statusHint/envHint/softDeleteHint/schemaHint,
|
|
4
|
+
* env_issue clustering, auth_hint, agent_directive) removed — the agent
|
|
5
|
+
* triages by enum + raw evidence, not by heuristic guess-text.
|
|
4
6
|
*/
|
|
5
7
|
|
|
6
8
|
import { classify } from "../classifier/recommended-action.ts";
|
|
7
9
|
|
|
8
|
-
export function statusHint(status: number | null | undefined): string | null {
|
|
9
|
-
if (!status) return null;
|
|
10
|
-
if (status >= 500) return "Server-side error — inspect response_body for errorMessage/errorDetail; likely a backend bug";
|
|
11
|
-
if (status === 401 || status === 403) return "Auth failure — check auth_token/api_key in .env.yaml";
|
|
12
|
-
if (status === 404) return "Resource not found — verify the path and ID";
|
|
13
|
-
if (status === 400 || status === 422) return "Validation error — check request body fields match the schema";
|
|
14
|
-
if (status === 429) return "Rate limited — too many requests. Consider consolidating auth/login steps or adding delays between suites";
|
|
15
|
-
return null;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
10
|
export function classifyFailure(status: string, responseStatus: number | null): "api_error" | "assertion_failed" | "network_error" {
|
|
19
11
|
if (status === "error" && (responseStatus === null || responseStatus < 500)) return "network_error";
|
|
20
12
|
if (responseStatus !== null && responseStatus >= 500) return "api_error";
|
|
21
13
|
return "assertion_failed";
|
|
22
14
|
}
|
|
23
15
|
|
|
24
|
-
export function envHint(url: string | null, errorMessage: string | null, envFilePath?: string): string | null {
|
|
25
|
-
const envFile = envFilePath ? envFilePath : ".env.yaml in your API directory";
|
|
26
|
-
|
|
27
|
-
if (url && /\{\{[^}]+\}\}/.test(url)) {
|
|
28
|
-
return `URL contains unresolved variable: "${url}" — variable name may not match the key in ${envFile}`;
|
|
29
|
-
}
|
|
30
|
-
if (url && !url.startsWith("http://") && !url.startsWith("https://")) {
|
|
31
|
-
return `base_url is not set or empty — URL resolved to "${url}". Add base_url to ${envFile}`;
|
|
32
|
-
}
|
|
33
|
-
if (errorMessage?.includes("base_url is not configured")) {
|
|
34
|
-
return `base_url is missing or empty. Add base_url: https://your-api.com to ${envFile}`;
|
|
35
|
-
}
|
|
36
|
-
if (errorMessage?.includes("URL is invalid") || errorMessage?.includes("Failed to parse URL")) {
|
|
37
|
-
return `URL is malformed — likely base_url is empty or invalid. Check base_url in ${envFile}`;
|
|
38
|
-
}
|
|
39
|
-
return null;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
16
|
export type RecommendedAction =
|
|
43
17
|
| "report_backend_bug"
|
|
44
18
|
| "fix_auth_config"
|
|
@@ -136,181 +110,3 @@ export function isGeneratedTest(
|
|
|
136
110
|
if (typeof suite_file === "string" && /(^|\/)apis\/[^/]+\/tests\//.test(suite_file)) return true;
|
|
137
111
|
return false;
|
|
138
112
|
}
|
|
139
|
-
|
|
140
|
-
export function envCategory(hint: string | undefined): string | null {
|
|
141
|
-
if (!hint) return null;
|
|
142
|
-
if (hint.includes("base_url is not set") || hint.includes("base_url is missing") || hint.includes("base_url is not configured")) return "base_url_missing";
|
|
143
|
-
if (hint.includes("unresolved variable")) return "unresolved_variable";
|
|
144
|
-
if (hint.includes("URL is malformed")) return "url_malformed";
|
|
145
|
-
return null;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
export function schemaHint(
|
|
149
|
-
failureType: string,
|
|
150
|
-
responseStatus: number | null | undefined,
|
|
151
|
-
): string | null {
|
|
152
|
-
if (failureType === "assertion_failed" || responseStatus === 400 || responseStatus === 422) {
|
|
153
|
-
return "Use describe_endpoint(specPath, method, path) to verify expected request/response schema";
|
|
154
|
-
}
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
export function softDeleteHint(
|
|
159
|
-
actualStatus: number | null | undefined,
|
|
160
|
-
requestMethod: string | null | undefined,
|
|
161
|
-
responseBody: unknown,
|
|
162
|
-
): string | null {
|
|
163
|
-
if (actualStatus !== 200 || requestMethod?.toUpperCase() !== "GET") return null;
|
|
164
|
-
if (responseBody && typeof responseBody === "object") {
|
|
165
|
-
const hasStatusField =
|
|
166
|
-
"status" in (responseBody as object) ||
|
|
167
|
-
"state" in (responseBody as object) ||
|
|
168
|
-
"deleted" in (responseBody as object) ||
|
|
169
|
-
"is_deleted" in (responseBody as object);
|
|
170
|
-
if (hasStatusField) {
|
|
171
|
-
return 'GET returned 200 with a status/state field after DELETE — likely soft delete. Update the test: remove the "Verify deleted → 404" step and instead assert the status field value (e.g. status: "cancelled")';
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export function computeSharedEnvIssue(
|
|
178
|
-
failures: Array<{ hint?: string }>,
|
|
179
|
-
envFilePath?: string,
|
|
180
|
-
): string | null {
|
|
181
|
-
const categories = new Set(failures.map(f => envCategory(f.hint)).filter(Boolean));
|
|
182
|
-
if (categories.size !== 1) return null;
|
|
183
|
-
|
|
184
|
-
const envFile = envFilePath ?? ".env.yaml";
|
|
185
|
-
if (categories.has("base_url_missing")) {
|
|
186
|
-
return `All failures: base_url is not set — add base_url to ${envFile}`;
|
|
187
|
-
}
|
|
188
|
-
if (categories.has("unresolved_variable")) {
|
|
189
|
-
return `All failures: some variables are not substituted — check variable names in ${envFile}`;
|
|
190
|
-
}
|
|
191
|
-
// url_malformed
|
|
192
|
-
return [...failures.map(f => f.hint).filter(Boolean)][0] ?? null;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// ── TASK-98: per-suite env clustering ──────────────────────────────────────
|
|
196
|
-
//
|
|
197
|
-
// Round-3 review showed that the all-or-nothing run-level detector misses
|
|
198
|
-
// real env_issue scenarios: a single suite needs `{{stripe_key}}`, a webhook
|
|
199
|
-
// host is unreachable for one suite only, an auth token expires part-way
|
|
200
|
-
// through. Cluster classification — group by suite, flag a suite when ≥80%
|
|
201
|
-
// of its non-5xx failures share an env-symptom — closes that gap without
|
|
202
|
-
// laundering 5xx (real backend bugs) into env_issue.
|
|
203
|
-
export type EnvSymptom = "missing_var" | "base_url" | "url_malformed" | "auth_expired";
|
|
204
|
-
|
|
205
|
-
function envSymptomOf(failure: {
|
|
206
|
-
hint?: string;
|
|
207
|
-
failure_type: string;
|
|
208
|
-
response_status: number | null;
|
|
209
|
-
}): EnvSymptom | null {
|
|
210
|
-
if (failure.failure_type === "api_error") return null; // 5xx never counted
|
|
211
|
-
const cat = envCategory(failure.hint);
|
|
212
|
-
if (cat === "unresolved_variable") return "missing_var";
|
|
213
|
-
if (cat === "base_url_missing") return "base_url";
|
|
214
|
-
if (cat === "url_malformed") return "url_malformed";
|
|
215
|
-
if (failure.response_status === 401 || failure.response_status === 403) return "auth_expired";
|
|
216
|
-
return null;
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
export interface EnvIssue {
|
|
220
|
-
/** Human-readable summary; used by reporters and shown to the user. */
|
|
221
|
-
message: string;
|
|
222
|
-
/** "run" when the issue spans most/all suites; "suite:<name>" when localized. */
|
|
223
|
-
scope: "run" | `suite:${string}`;
|
|
224
|
-
/** Suites the env_issue covers — one entry for suite scope, ≥2 for run scope. */
|
|
225
|
-
affected_suites: string[];
|
|
226
|
-
/** Histogram of root-cause symptoms across affected failures. */
|
|
227
|
-
symptoms: Partial<Record<EnvSymptom, number>>;
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
/**
|
|
231
|
-
* Cluster non-5xx failures by suite and return per-suite env clusters that
|
|
232
|
-
* meet the env-symptom threshold (default ≥80% AND ≥2 failures). 5xx are
|
|
233
|
-
* excluded so backend bugs cannot be reclassified as env issues.
|
|
234
|
-
*/
|
|
235
|
-
export function clusterEnvIssues(
|
|
236
|
-
failures: Array<{
|
|
237
|
-
suite_name: string;
|
|
238
|
-
hint?: string;
|
|
239
|
-
failure_type: string;
|
|
240
|
-
response_status: number | null;
|
|
241
|
-
}>,
|
|
242
|
-
threshold = 0.8,
|
|
243
|
-
): Array<{ suite: string; symptoms: Partial<Record<EnvSymptom, number>>; total: number }> {
|
|
244
|
-
const bySuite = new Map<string, typeof failures>();
|
|
245
|
-
for (const f of failures) {
|
|
246
|
-
if (f.failure_type === "api_error") continue;
|
|
247
|
-
const list = bySuite.get(f.suite_name) ?? [];
|
|
248
|
-
list.push(f);
|
|
249
|
-
bySuite.set(f.suite_name, list);
|
|
250
|
-
}
|
|
251
|
-
const clusters: Array<{ suite: string; symptoms: Partial<Record<EnvSymptom, number>>; total: number }> = [];
|
|
252
|
-
for (const [suite, items] of bySuite) {
|
|
253
|
-
if (items.length === 0) continue;
|
|
254
|
-
const symptoms: Partial<Record<EnvSymptom, number>> = {};
|
|
255
|
-
let envCount = 0;
|
|
256
|
-
for (const f of items) {
|
|
257
|
-
const s = envSymptomOf(f);
|
|
258
|
-
if (s) {
|
|
259
|
-
symptoms[s] = (symptoms[s] ?? 0) + 1;
|
|
260
|
-
envCount++;
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
if (envCount / items.length >= threshold && envCount >= 1) {
|
|
264
|
-
clusters.push({ suite, symptoms, total: items.length });
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
return clusters;
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
function formatSymptoms(symptoms: Partial<Record<EnvSymptom, number>>): string {
|
|
271
|
-
const parts: string[] = [];
|
|
272
|
-
for (const k of ["missing_var", "base_url", "url_malformed", "auth_expired"] as EnvSymptom[]) {
|
|
273
|
-
const n = symptoms[k];
|
|
274
|
-
if (n) parts.push(`${k}=${n}`);
|
|
275
|
-
}
|
|
276
|
-
return parts.join(", ");
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
/**
|
|
280
|
-
* Build an EnvIssue envelope from clustered failures. Returns null when no
|
|
281
|
-
* cluster exceeded the threshold. When exactly one suite is affected, scope
|
|
282
|
-
* is `suite:<name>`; ≥2 suites collapse into a `run` scope aggregator.
|
|
283
|
-
*/
|
|
284
|
-
export function buildEnvIssue(
|
|
285
|
-
clusters: Array<{ suite: string; symptoms: Partial<Record<EnvSymptom, number>>; total: number }>,
|
|
286
|
-
envFilePath?: string,
|
|
287
|
-
): EnvIssue | null {
|
|
288
|
-
if (clusters.length === 0) return null;
|
|
289
|
-
const envFile = envFilePath ?? ".env.yaml";
|
|
290
|
-
|
|
291
|
-
const merged: Partial<Record<EnvSymptom, number>> = {};
|
|
292
|
-
for (const c of clusters) {
|
|
293
|
-
for (const [k, v] of Object.entries(c.symptoms)) {
|
|
294
|
-
merged[k as EnvSymptom] = (merged[k as EnvSymptom] ?? 0) + (v ?? 0);
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
const affected_suites = clusters.map(c => c.suite).sort();
|
|
298
|
-
|
|
299
|
-
if (clusters.length === 1) {
|
|
300
|
-
const c = clusters[0]!;
|
|
301
|
-
const breakdown = formatSymptoms(c.symptoms);
|
|
302
|
-
return {
|
|
303
|
-
message: `Suite "${c.suite}" looks env-broken (${breakdown}) — check ${envFile}`,
|
|
304
|
-
scope: `suite:${c.suite}`,
|
|
305
|
-
affected_suites,
|
|
306
|
-
symptoms: merged,
|
|
307
|
-
};
|
|
308
|
-
}
|
|
309
|
-
const breakdown = formatSymptoms(merged);
|
|
310
|
-
return {
|
|
311
|
-
message: `${clusters.length} suites look env-broken (${breakdown}) — check ${envFile}`,
|
|
312
|
-
scope: "run",
|
|
313
|
-
affected_suites,
|
|
314
|
-
symptoms: merged,
|
|
315
|
-
};
|
|
316
|
-
}
|
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
* TASK-29: actionable "Suggested fixes" surfaces for `zond db diagnose`.
|
|
3
3
|
*
|
|
4
4
|
* The base diagnose envelope already classifies failures and wires
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* round-trip:
|
|
5
|
+
* recommended_action. This layer adds two concrete, fixable signals that
|
|
6
|
+
* the LLM agent can act on without a second round-trip:
|
|
8
7
|
*
|
|
9
8
|
* 1. Placeholder path-params on 404s — when a 404 hits a URL that still
|
|
10
9
|
* contains literal example/placeholder values (`example`, all-zeros
|
|
@@ -16,14 +16,7 @@ export interface ChunkPlan {
|
|
|
16
16
|
* usable first segment (e.g. `/`) keep the legacy `untagged` key.
|
|
17
17
|
*/
|
|
18
18
|
export function groupEndpointsByTag(endpoints: EndpointInfo[]): Map<string, EndpointInfo[]> {
|
|
19
|
-
|
|
20
|
-
for (const ep of endpoints) {
|
|
21
|
-
const key = ep.tags[0] ?? resourceKeyFromPath(ep.path);
|
|
22
|
-
const list = groups.get(key);
|
|
23
|
-
if (list) list.push(ep);
|
|
24
|
-
else groups.set(key, [ep]);
|
|
25
|
-
}
|
|
26
|
-
return groups;
|
|
19
|
+
return Map.groupBy(endpoints, (ep) => ep.tags[0] ?? resourceKeyFromPath(ep.path));
|
|
27
20
|
}
|
|
28
21
|
|
|
29
22
|
/** Extract the first non-templated path segment for tagless fallback. */
|
|
@@ -70,17 +70,15 @@ export function generateFromSchema(
|
|
|
70
70
|
// the API 422s with "Missing <required-by-other-variant>".
|
|
71
71
|
if (schema.oneOf) {
|
|
72
72
|
const variants = schema.oneOf as OpenAPIV3.SchemaObject[];
|
|
73
|
-
const picked =
|
|
74
|
-
?? pickPreferredVariant(variants);
|
|
73
|
+
const picked = pickBestVariant(variants, schema.discriminator);
|
|
75
74
|
const result = recurse(picked, propertyName);
|
|
76
|
-
return stampDiscriminator(result, picked, schema.discriminator
|
|
75
|
+
return stampDiscriminator(result, picked, schema.discriminator);
|
|
77
76
|
}
|
|
78
77
|
if (schema.anyOf) {
|
|
79
78
|
const variants = schema.anyOf as OpenAPIV3.SchemaObject[];
|
|
80
|
-
const picked =
|
|
81
|
-
?? pickPreferredVariant(variants);
|
|
79
|
+
const picked = pickBestVariant(variants, schema.discriminator);
|
|
82
80
|
const result = recurse(picked, propertyName);
|
|
83
|
-
return stampDiscriminator(result, picked, schema.discriminator
|
|
81
|
+
return stampDiscriminator(result, picked, schema.discriminator);
|
|
84
82
|
}
|
|
85
83
|
|
|
86
84
|
// enum: first value (always valid for the API contract)
|
|
@@ -174,13 +172,29 @@ export function generateFromSchema(
|
|
|
174
172
|
/** Fields the client must not send in a request body: explicit `readOnly: true`,
|
|
175
173
|
* or the literal name `id`. The latter is a heuristic for under-specified specs
|
|
176
174
|
* (common in in-house APIs) that don't mark the server-assigned id readOnly
|
|
177
|
-
* but still 4xx on it being present.
|
|
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. */
|
|
178
184
|
function shouldSkipForRequest(name: string, schema: OpenAPIV3.SchemaObject): boolean {
|
|
179
185
|
if (schema.readOnly === true) return true;
|
|
180
186
|
if (name === "id") return true;
|
|
187
|
+
if (name === "expand" && isStringArray(schema)) return true;
|
|
181
188
|
return false;
|
|
182
189
|
}
|
|
183
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
|
+
|
|
184
198
|
/** When recursion hits the depth cap, return a type-appropriate placeholder
|
|
185
199
|
* rather than `{}` — `{}` for `array<string>` produces `[{}]` which 422s on
|
|
186
200
|
* every realistic API. */
|
|
@@ -199,67 +213,113 @@ function depthLimitDefault(schema: OpenAPIV3.SchemaObject, name?: string): unkno
|
|
|
199
213
|
}
|
|
200
214
|
}
|
|
201
215
|
|
|
202
|
-
/** ARV-
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
*
|
|
206
|
-
|
|
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(
|
|
207
243
|
variants: OpenAPIV3.SchemaObject[],
|
|
208
|
-
|
|
209
|
-
): OpenAPIV3.SchemaObject
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
214
255
|
const en = (prop as { enum?: unknown[] }).enum;
|
|
215
256
|
const cn = (prop as { const?: unknown }).const;
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
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]!;
|
|
220
286
|
}
|
|
221
287
|
|
|
222
288
|
/** Stamp the discriminator key onto a generated object. Without this the
|
|
223
289
|
* variant choice is "anonymous" from the body's point of view — APIs that
|
|
224
290
|
* switch on `type` reject the request even when every other field is
|
|
225
|
-
* perfect.
|
|
226
|
-
*
|
|
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. */
|
|
227
298
|
function stampDiscriminator(
|
|
228
299
|
result: unknown,
|
|
229
300
|
variant: OpenAPIV3.SchemaObject,
|
|
230
|
-
|
|
301
|
+
discriminator: OpenAPIV3.DiscriminatorObject | undefined,
|
|
231
302
|
): unknown {
|
|
303
|
+
const propertyName = discriminator?.propertyName;
|
|
232
304
|
if (!propertyName) return result;
|
|
233
305
|
if (!result || typeof result !== "object" || Array.isArray(result)) return result;
|
|
234
|
-
const prop = variant.properties?.[propertyName] as OpenAPIV3.SchemaObject | undefined;
|
|
235
|
-
if (!prop) return result;
|
|
236
|
-
const en = (prop as { enum?: unknown[] }).enum;
|
|
237
|
-
const cn = (prop as { const?: unknown }).const;
|
|
238
306
|
let stamp: unknown;
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
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;
|
|
242
319
|
(result as Record<string, unknown>)[propertyName] = stamp;
|
|
243
320
|
return result;
|
|
244
321
|
}
|
|
245
322
|
|
|
246
|
-
/** Prefer the most data-shape-informative variant from a oneOf/anyOf list:
|
|
247
|
-
* object-with-properties > non-null > first. Skips `type: "null"` entries
|
|
248
|
-
* introduced by 3.1 nullable shorthand. */
|
|
249
|
-
function pickPreferredVariant(variants: OpenAPIV3.SchemaObject[]): OpenAPIV3.SchemaObject {
|
|
250
|
-
const isNull = (s: OpenAPIV3.SchemaObject) =>
|
|
251
|
-
(s as { type?: unknown }).type === "null";
|
|
252
|
-
const nonNull = variants.filter(v => !isNull(v));
|
|
253
|
-
const pool = nonNull.length > 0 ? nonNull : variants;
|
|
254
|
-
|
|
255
|
-
const objectWithProps = pool.find(
|
|
256
|
-
v => v.type === "object" && v.properties && Object.keys(v.properties).length > 0,
|
|
257
|
-
);
|
|
258
|
-
if (objectWithProps) return objectWithProps;
|
|
259
|
-
|
|
260
|
-
return pool[0]!;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
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;
|
|
264
324
|
|
|
265
325
|
/** Names that strongly imply an email field. Kept in sync with the email
|
|
@@ -269,7 +329,9 @@ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/
|
|
|
269
329
|
* email mapping. */
|
|
270
330
|
function isEmailContextName(name?: string): boolean {
|
|
271
331
|
if (!name) return false;
|
|
272
|
-
|
|
332
|
+
// Canonical snake_case so camelCase (`replyTo`, `userEmail`) matches the
|
|
333
|
+
// same rules as `reply_to` / `user_email`.
|
|
334
|
+
const lower = canonicalVarName(name);
|
|
273
335
|
return (
|
|
274
336
|
lower === "email" ||
|
|
275
337
|
lower === "from" ||
|
|
@@ -279,9 +341,7 @@ function isEmailContextName(name?: string): boolean {
|
|
|
279
341
|
lower === "sender" ||
|
|
280
342
|
lower === "recipient" ||
|
|
281
343
|
lower === "reply_to" ||
|
|
282
|
-
lower === "replyto" ||
|
|
283
344
|
lower.endsWith("_email") ||
|
|
284
|
-
lower.endsWith("Email") ||
|
|
285
345
|
lower.endsWith("_reply_to") ||
|
|
286
346
|
lower.endsWith("_from") ||
|
|
287
347
|
lower.endsWith("_to") ||
|
|
@@ -398,7 +458,7 @@ export function classifyFieldSource(
|
|
|
398
458
|
return "heuristic:domain-from-description";
|
|
399
459
|
}
|
|
400
460
|
if (propertyName) {
|
|
401
|
-
const lower = propertyName
|
|
461
|
+
const lower = canonicalVarName(propertyName);
|
|
402
462
|
if (lower === "slug" || lower.endsWith("_slug")) return "heuristic:slug";
|
|
403
463
|
if (lower === "domain" || lower === "hostname" || lower === "fqdn" || lower.endsWith("_domain")) return "heuristic:domain";
|
|
404
464
|
if (lower === "platform") return "heuristic:platform";
|
|
@@ -412,13 +472,13 @@ export function classifyFieldSource(
|
|
|
412
472
|
if (
|
|
413
473
|
lower === "email" || lower === "from" || lower === "to" || lower === "cc" ||
|
|
414
474
|
lower === "bcc" || lower === "sender" || lower === "recipient" ||
|
|
415
|
-
lower === "reply_to" ||
|
|
416
|
-
lower.endsWith("_email") ||
|
|
475
|
+
lower === "reply_to" ||
|
|
476
|
+
lower.endsWith("_email") ||
|
|
417
477
|
lower.endsWith("_reply_to") || lower.endsWith("_from") ||
|
|
418
478
|
lower.endsWith("_to") || lower.endsWith("_cc") || lower.endsWith("_bcc")
|
|
419
479
|
) return "heuristic:email";
|
|
420
480
|
if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) return "heuristic:id";
|
|
421
|
-
if (lower === "name" || lower.endsWith("_name")
|
|
481
|
+
if (lower === "name" || lower.endsWith("_name")) return "heuristic:name";
|
|
422
482
|
if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") return "heuristic:url";
|
|
423
483
|
if (lower === "password" || lower.endsWith("_password")) return "heuristic:password";
|
|
424
484
|
if (lower === "phone" || lower === "telephone" || lower.endsWith("_phone")) return "heuristic:phone";
|
|
@@ -436,6 +496,83 @@ export function classifyFieldSource(
|
|
|
436
496
|
return "default";
|
|
437
497
|
}
|
|
438
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
|
+
|
|
439
576
|
/**
|
|
440
577
|
* Map an OpenAPI `format` value to a zond generator placeholder. Returns
|
|
441
578
|
* undefined when the format is unknown or absent so callers can fall back
|
|
@@ -533,9 +670,12 @@ function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string):
|
|
|
533
670
|
return "{{$randomDomain}}";
|
|
534
671
|
}
|
|
535
672
|
|
|
536
|
-
// Name-based heuristics
|
|
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.
|
|
537
677
|
if (name) {
|
|
538
|
-
const lower = name
|
|
678
|
+
const lower = canonicalVarName(name);
|
|
539
679
|
if (lower === "slug" || lower.endsWith("_slug")) {
|
|
540
680
|
return "{{$randomSlug}}";
|
|
541
681
|
}
|
|
@@ -575,9 +715,7 @@ function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string):
|
|
|
575
715
|
lower === "sender" ||
|
|
576
716
|
lower === "recipient" ||
|
|
577
717
|
lower === "reply_to" ||
|
|
578
|
-
lower === "replyto" ||
|
|
579
718
|
lower.endsWith("_email") ||
|
|
580
|
-
lower.endsWith("Email") ||
|
|
581
719
|
lower.endsWith("_reply_to") ||
|
|
582
720
|
lower.endsWith("_from") ||
|
|
583
721
|
lower.endsWith("_to") ||
|
|
@@ -589,7 +727,7 @@ function guessStringPlaceholder(schema: OpenAPIV3.SchemaObject, name?: string):
|
|
|
589
727
|
if (lower === "id" || lower === "uuid" || lower.endsWith("_id") || lower.endsWith("id")) {
|
|
590
728
|
return "{{$uuid}}";
|
|
591
729
|
}
|
|
592
|
-
if (lower === "name" || lower.endsWith("_name")
|
|
730
|
+
if (lower === "name" || lower.endsWith("_name")) {
|
|
593
731
|
return "{{$randomName}}";
|
|
594
732
|
}
|
|
595
733
|
if (lower === "url" || lower.endsWith("_url") || lower === "uri" || lower === "href" || lower === "website") {
|