@kirrosh/zond 0.22.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 +811 -0
- package/README.md +59 -6
- package/package.json +9 -7
- package/src/CLAUDE.md +112 -0
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +146 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +880 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +64 -0
- package/src/cli/commands/api/annotate/prompts.ts +220 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +786 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +361 -0
- package/src/cli/commands/checks.ts +1072 -0
- package/src/cli/commands/ci-init.ts +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +236 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +823 -132
- package/src/cli/commands/db.ts +486 -12
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1356 -0
- package/src/cli/commands/doctor.ts +661 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +438 -47
- package/src/cli/commands/init/bootstrap.ts +34 -2
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +99 -3
- package/src/cli/commands/init/templates/agents.md +77 -64
- package/src/cli/commands/init/templates/skills/warm-up-target.md +122 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +621 -0
- package/src/cli/commands/init/templates/skills/zond-seed.md +114 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +272 -0
- package/src/cli/commands/init/templates/skills/zond.md +802 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- package/src/cli/commands/prepare-fixtures.ts +97 -0
- package/src/cli/commands/probe/_seed-bodies.ts +52 -0
- package/src/cli/commands/probe/mass-assignment.ts +594 -0
- package/src/cli/commands/probe/security.ts +537 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +163 -0
- package/src/cli/commands/probe.ts +535 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +227 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +310 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +495 -4
- package/src/cli/commands/run.ts +870 -53
- package/src/cli/commands/schema-from-runs.ts +128 -0
- package/src/cli/commands/secrets.ts +133 -0
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +92 -3
- package/src/cli/json-schemas.ts +314 -0
- package/src/cli/output.ts +17 -1
- package/src/cli/program.ts +199 -635
- package/src/cli/resolve.ts +105 -0
- package/src/cli/safe-live.ts +24 -0
- package/src/cli/status-filter.ts +114 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +5 -0
- package/src/core/audit/persist.ts +183 -0
- package/src/core/checks/budget.ts +59 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +147 -0
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +219 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +242 -0
- package/src/core/checks/checks/ignored_auth.ts +254 -0
- package/src/core/checks/checks/index.ts +68 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +416 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +148 -0
- package/src/core/checks/checks/not_a_server_error.ts +35 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +160 -0
- package/src/core/checks/checks/pagination_invariants.ts +419 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +33 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +132 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +82 -0
- package/src/core/checks/recommended-action.ts +68 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +1461 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/spec-findings.ts +308 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +305 -0
- package/src/core/checks/zond-extensions.ts +73 -0
- package/src/core/classifier/recommended-action.ts +251 -0
- package/src/core/context/current.ts +22 -6
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +216 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +293 -59
- package/src/core/diagnostics/failure-class.ts +140 -0
- package/src/core/diagnostics/failure-hints.ts +88 -89
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +155 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +38 -19
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/data-factory.ts +586 -22
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +332 -0
- package/src/core/generator/index.ts +5 -5
- package/src/core/generator/openapi-reader.ts +135 -7
- package/src/core/generator/path-param-disambig.ts +140 -0
- package/src/core/generator/resources-builder.ts +898 -0
- package/src/core/generator/schema-utils.ts +33 -3
- package/src/core/generator/serializer.ts +103 -13
- package/src/core/generator/suite-generator.ts +583 -122
- package/src/core/generator/types.ts +14 -0
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +73 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +91 -0
- package/src/core/output/types.ts +122 -0
- package/src/core/parser/dynamic-values.ts +160 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +61 -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-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +27 -0
- package/src/core/probe/mass-assignment-template.ts +240 -0
- package/src/core/probe/method-probe.ts +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +439 -0
- package/src/core/probe/probe-harness.ts +119 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- 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 +207 -0
- package/src/core/probe/security-probe.ts +32 -0
- package/src/core/probe/shared.ts +531 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +282 -0
- package/src/core/reporter/console.ts +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +59 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +265 -36
- package/src/core/runner/form-encode.ts +41 -0
- package/src/core/runner/http-client.ts +112 -2
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +153 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +87 -33
- package/src/core/runner/run-kind.ts +45 -0
- package/src/core/runner/schema-validator.ts +308 -0
- package/src/core/runner/send-request.ts +158 -20
- package/src/core/runner/types.ts +44 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +457 -20
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +58 -0
- package/src/core/spec/infer-schema.ts +102 -0
- package/src/core/spec/layers.ts +154 -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/format-eta.ts +21 -0
- package/src/core/util/headers.ts +9 -0
- package/src/core/util/url.ts +24 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/fixture-gap-report.ts +84 -0
- package/src/core/workspace/fixture-gaps.ts +71 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +13 -11
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +128 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/0002_run_kind_request.sql +59 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +216 -0
- package/src/db/queries/runs.ts +289 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +75 -802
- package/src/db/schema.ts +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -1,16 +1,63 @@
|
|
|
1
1
|
import type { OpenAPIV3 } from "openapi-types";
|
|
2
2
|
import type { EndpointInfo, SecuritySchemeInfo, CrudGroup } from "./types.ts";
|
|
3
3
|
import type { RawSuite, RawStep } from "./serializer.ts";
|
|
4
|
-
import {
|
|
4
|
+
import type { SourceMetadata } from "../parser/types.ts";
|
|
5
|
+
import { generateFromSchema, generateMultipartFromSchema, isFkFixtureField, canonicalVarName, effectiveObjectShape } from "./data-factory.ts";
|
|
5
6
|
import { groupEndpointsByTag } from "./chunker.ts";
|
|
7
|
+
import { getAuthHeaders as sharedGetAuthHeaders } from "../probe/shared.ts";
|
|
8
|
+
import { flattenToFormFields } from "../runner/form-encode.ts";
|
|
6
9
|
|
|
7
10
|
// ──────────────────────────────────────────────
|
|
8
11
|
// Helpers
|
|
9
12
|
// ──────────────────────────────────────────────
|
|
10
13
|
|
|
11
|
-
/**
|
|
12
|
-
|
|
13
|
-
|
|
14
|
+
/**
|
|
15
|
+
* Singularize an English plural noun for use in suite names and capture
|
|
16
|
+
* variables. Handles the cases that matter for typical OpenAPI resource
|
|
17
|
+
* names — `properties → property`, `addresses → address`, `boxes → box`,
|
|
18
|
+
* `users → user`. Words that don't match any rule are returned unchanged
|
|
19
|
+
* (so already-singular `series`, `news`, `data`, etc. survive).
|
|
20
|
+
*/
|
|
21
|
+
export function singularizeResource(word: string): string {
|
|
22
|
+
if (word.length > 3 && /ies$/i.test(word)) return word.slice(0, -3) + "y";
|
|
23
|
+
// ARV-100 (F5): the inner alternative was `s` — but a single trailing `s`
|
|
24
|
+
// catches every regular plural whose stem ends in any vowel + `s` (e.g.
|
|
25
|
+
// `releases`, `phases`, `houses`), and `slice(-2)` then chops "es" instead
|
|
26
|
+
// of just "s". The result was `releas_id` / `phas_id` capture vars that
|
|
27
|
+
// matched nothing on the manifest side. Restrict the rule to the genuine
|
|
28
|
+
// sibilant double — `ss` — so `addresses → address` keeps working without
|
|
29
|
+
// dragging single-s plurals along.
|
|
30
|
+
if (word.length > 3 && /(ch|sh|x|ss|z)es$/i.test(word)) return word.slice(0, -2);
|
|
31
|
+
if (word.length > 1 && /[^s]s$/i.test(word)) return word.slice(0, -1);
|
|
32
|
+
return word;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build a `<resource>_id` capture/var name. Strips dashes so the result is
|
|
37
|
+
* a safe template variable identifier — `contact-properties` becomes
|
|
38
|
+
* `contact_property_id` rather than `contact-propertie_id` (TASK-214).
|
|
39
|
+
*
|
|
40
|
+
* ARV-100 (F5): always lowercase. Path-params/headers in fixtures-builder
|
|
41
|
+
* are normalised to lowercase (line 157), so capture vars must match — a
|
|
42
|
+
* `Groups` resource would otherwise produce `Group_id` while path-params on
|
|
43
|
+
* the same endpoint produce `group_id`, splitting the `{{var}}` namespace
|
|
44
|
+
* and triggering "Undefined variables" in `zond run`.
|
|
45
|
+
*/
|
|
46
|
+
export function resourceVar(resource: string, suffix: string): string {
|
|
47
|
+
const singular = singularizeResource(resource);
|
|
48
|
+
return `${singular.replace(/[^a-zA-Z0-9]+/g, "_")}_${suffix}`.toLowerCase();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Convert OpenAPI path params {param} to test interpolation {{param}}.
|
|
52
|
+
* When `ambiguous` is given (ARV-369), a param name reused across
|
|
53
|
+
* distinct resources becomes a resource-scoped var (`{{templates_code}}`)
|
|
54
|
+
* instead of the raw name, so `.env.yaml` can hold distinct values per
|
|
55
|
+
* resource instead of one value silently applying everywhere. */
|
|
56
|
+
function convertPath(path: string, ambiguous?: Set<string>): string {
|
|
57
|
+
if (!ambiguous || ambiguous.size === 0) {
|
|
58
|
+
return path.replace(/\{([^}]+)\}/g, "{{$1}}");
|
|
59
|
+
}
|
|
60
|
+
return path.replace(/\{([^}]+)\}/g, (_, name) => `{{${fixtureVarNameForPathParam(path, name, ambiguous)}}}`);
|
|
14
61
|
}
|
|
15
62
|
|
|
16
63
|
/**
|
|
@@ -56,6 +103,75 @@ function slugify(s: string): string {
|
|
|
56
103
|
return s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "");
|
|
57
104
|
}
|
|
58
105
|
|
|
106
|
+
/** Strip trailing API-version path segments (`v1`, `v30`, `version2`) so
|
|
107
|
+
* resource-name derivation doesn't mistake a version marker for the
|
|
108
|
+
* resource itself on APIs shaped `/api/<resource>/v{N}` (ARV-372). */
|
|
109
|
+
export function stripTrailingVersionSegments(segments: string[]): string[] {
|
|
110
|
+
const out = [...segments];
|
|
111
|
+
while (out.length > 1 && /^v(ersion)?\d+$/i.test(out[out.length - 1]!)) {
|
|
112
|
+
out.pop();
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Owning collection for a path-param: the last non-version path segment
|
|
118
|
+
* before its `{name}` placeholder. Used to disambiguate a path-param name
|
|
119
|
+
* (`code`, `id`, `key`) that's reused across genuinely distinct resources
|
|
120
|
+
* (ARV-369) — e.g. `/api/templates/v30/{code}` → "templates" vs
|
|
121
|
+
* `/api/macros/v30/{code}` → "macros". */
|
|
122
|
+
// ARV-376: read-by-id accessor markers — kept in sync with the set in
|
|
123
|
+
// path-param-disambig.ts. `/business-segment20/byid/{id}` is owned by
|
|
124
|
+
// `business-segment20`, not the `byid` accessor verb.
|
|
125
|
+
const ACCESSOR_MARKER_SEGS = new Set(["byid", "by-id", "by_id"]);
|
|
126
|
+
|
|
127
|
+
export function owningCollectionForPathParam(path: string, paramName: string): string | null {
|
|
128
|
+
const marker = `{${paramName}}`;
|
|
129
|
+
const idx = path.indexOf(marker);
|
|
130
|
+
if (idx === -1) return null;
|
|
131
|
+
const segments = stripTrailingVersionSegments(path.slice(0, idx).split("/").filter(Boolean));
|
|
132
|
+
// Skip trailing accessor markers so the owner is the collection, not the
|
|
133
|
+
// `byid`/`by-code` verb — otherwise a param behind `/byid/{id}` and its
|
|
134
|
+
// sibling `DELETE /{id}` disagree on owner and get double-scoped.
|
|
135
|
+
let end = segments.length - 1;
|
|
136
|
+
while (end >= 0 && ACCESSOR_MARKER_SEGS.has(segments[end]!.toLowerCase())) end--;
|
|
137
|
+
return end >= 0 ? segments[end]! : null;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Path-param names reused by more than one distinct owning collection
|
|
141
|
+
* across the whole spec — these need a resource-scoped fixture var name
|
|
142
|
+
* (`templates_code`) instead of the raw name (`code`), otherwise one
|
|
143
|
+
* `.env.yaml` value silently applies to unrelated resources (ARV-369). */
|
|
144
|
+
export function computeAmbiguousPathParams(endpoints: EndpointInfo[]): Set<string> {
|
|
145
|
+
const owners = new Map<string, Set<string>>();
|
|
146
|
+
for (const ep of endpoints) {
|
|
147
|
+
for (const p of ep.parameters) {
|
|
148
|
+
if (p.in !== "path" || p.required === false) continue;
|
|
149
|
+
const owner = owningCollectionForPathParam(ep.path, p.name) ?? "";
|
|
150
|
+
if (!owners.has(p.name)) owners.set(p.name, new Set());
|
|
151
|
+
owners.get(p.name)!.add(owner);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
const ambiguous = new Set<string>();
|
|
155
|
+
for (const [name, ownerSet] of owners) {
|
|
156
|
+
if (ownerSet.size > 1) ambiguous.add(name);
|
|
157
|
+
}
|
|
158
|
+
return ambiguous;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Resource-scoped fixture var name for a path-param, when ambiguous;
|
|
162
|
+
* otherwise the raw param name (single-owner APIs — the common case —
|
|
163
|
+
* keep producing the same var names as before ARV-369). */
|
|
164
|
+
export function fixtureVarNameForPathParam(
|
|
165
|
+
path: string,
|
|
166
|
+
paramName: string,
|
|
167
|
+
ambiguous: Set<string>,
|
|
168
|
+
): string {
|
|
169
|
+
if (!ambiguous.has(paramName)) return paramName;
|
|
170
|
+
const owner = owningCollectionForPathParam(path, paramName);
|
|
171
|
+
if (!owner) return paramName;
|
|
172
|
+
return `${owner.replace(/[^a-zA-Z0-9]+/g, "_")}_${paramName}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
59
175
|
function escapeRegex(s: string): string {
|
|
60
176
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
61
177
|
}
|
|
@@ -73,11 +189,32 @@ function selectHealthcheckEndpoint(gets: EndpointInfo[]): EndpointInfo | undefin
|
|
|
73
189
|
);
|
|
74
190
|
}
|
|
75
191
|
|
|
192
|
+
/**
|
|
193
|
+
* Pick the success status the test should assert.
|
|
194
|
+
*
|
|
195
|
+
* Order:
|
|
196
|
+
* 1. First 2xx declared in the spec (most authoritative).
|
|
197
|
+
* 2. Method-aware default when the spec lists only non-2xx responses or none
|
|
198
|
+
* at all (many OpenAPI specs is silent for several mutating endpoints — the
|
|
199
|
+
* actual runtime returns 201/204, while the old default of 200 caused
|
|
200
|
+
* tests to fail at runtime). We never assert a 4xx/5xx as the success
|
|
201
|
+
* status — that would generate guaranteed-failing tests.
|
|
202
|
+
*/
|
|
76
203
|
function getExpectedStatus(ep: EndpointInfo): number {
|
|
77
204
|
const success = ep.responses.find(r => r.statusCode >= 200 && r.statusCode < 300);
|
|
78
205
|
if (success) return success.statusCode;
|
|
79
|
-
|
|
80
|
-
|
|
206
|
+
return defaultStatusByMethod(ep.method);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function defaultStatusByMethod(method: string): number {
|
|
210
|
+
switch (method.toUpperCase()) {
|
|
211
|
+
case "POST":
|
|
212
|
+
return 201;
|
|
213
|
+
case "DELETE":
|
|
214
|
+
return 204;
|
|
215
|
+
default:
|
|
216
|
+
return 200;
|
|
217
|
+
}
|
|
81
218
|
}
|
|
82
219
|
|
|
83
220
|
function getSuccessSchema(ep: EndpointInfo): OpenAPIV3.SchemaObject | undefined {
|
|
@@ -105,7 +242,7 @@ function getBodyAssertions(ep: EndpointInfo): Record<string, Record<string, stri
|
|
|
105
242
|
}
|
|
106
243
|
|
|
107
244
|
/** Derive a variable name for a security scheme's token */
|
|
108
|
-
function schemeVarName(scheme: SecuritySchemeInfo, allSchemes: SecuritySchemeInfo[]): string {
|
|
245
|
+
export function schemeVarName(scheme: SecuritySchemeInfo, allSchemes: SecuritySchemeInfo[]): string {
|
|
109
246
|
// Count how many bearer-like schemes exist
|
|
110
247
|
const bearerSchemes = allSchemes.filter(s =>
|
|
111
248
|
(s.type === "http" && (s.scheme === "bearer" || !s.scheme)) ||
|
|
@@ -122,29 +259,7 @@ function getAuthHeaders(
|
|
|
122
259
|
ep: EndpointInfo,
|
|
123
260
|
schemes: SecuritySchemeInfo[],
|
|
124
261
|
): Record<string, string> | undefined {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
for (const secName of ep.security) {
|
|
128
|
-
const scheme = schemes.find(s => s.name === secName);
|
|
129
|
-
if (!scheme) continue;
|
|
130
|
-
|
|
131
|
-
if (scheme.type === "http") {
|
|
132
|
-
if (scheme.scheme === "bearer" || !scheme.scheme) {
|
|
133
|
-
return { Authorization: `Bearer {{${schemeVarName(scheme, schemes)}}}` };
|
|
134
|
-
}
|
|
135
|
-
if (scheme.scheme === "basic") {
|
|
136
|
-
return { Authorization: `Basic {{${schemeVarName(scheme, schemes)}}}` };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
if (scheme.type === "apiKey" && scheme.in === "header" && scheme.apiKeyName) {
|
|
140
|
-
if (scheme.apiKeyName === "Authorization") {
|
|
141
|
-
return { Authorization: `Bearer {{${schemeVarName(scheme, schemes)}}}` };
|
|
142
|
-
}
|
|
143
|
-
return { [scheme.apiKeyName]: "{{api_key}}" };
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
return undefined;
|
|
262
|
+
return sharedGetAuthHeaders(ep, schemes, s => schemeVarName(s, schemes));
|
|
148
263
|
}
|
|
149
264
|
|
|
150
265
|
function getRequiredQueryParams(ep: EndpointInfo): Record<string, string> | undefined {
|
|
@@ -172,23 +287,95 @@ function getSuiteHeaders(
|
|
|
172
287
|
|
|
173
288
|
const headerSets = endpoints.map(ep => getAuthHeaders(ep, schemes));
|
|
174
289
|
const first = headerSets[0];
|
|
175
|
-
if (!first)
|
|
290
|
+
if (!first) {
|
|
291
|
+
// ARV-212 (R13/F16): spec has no securitySchemes (GitHub publishes its
|
|
292
|
+
// OpenAPI this way) so per-endpoint auth-header derivation returns
|
|
293
|
+
// undefined for every step. When the API workspace nonetheless wires
|
|
294
|
+
// `auth_token` end-to-end (ARV-201 seeds it in .env.yaml on bare specs,
|
|
295
|
+
// and zond request / runner auto-attach Authorization: Bearer when
|
|
296
|
+
// auth_token is present), generated suites should not silently go
|
|
297
|
+
// unauth — that bricks them on the first rate-limited 60 requests.
|
|
298
|
+
// Fall back to a generic Bearer header at the suite level. The header
|
|
299
|
+
// is harmless when .secrets.yaml.auth_token is empty (zond runner
|
|
300
|
+
// still substitutes `{{auth_token}}` to an empty string, just like
|
|
301
|
+
// before; the server then 401s — same outcome as today).
|
|
302
|
+
if (schemes.length === 0 && _suiteDefaultAuthVar !== null) {
|
|
303
|
+
return { Authorization: `Bearer {{${_suiteDefaultAuthVar}}}` };
|
|
304
|
+
}
|
|
305
|
+
return undefined;
|
|
306
|
+
}
|
|
176
307
|
|
|
177
308
|
const firstJson = JSON.stringify(first);
|
|
178
309
|
const allSame = headerSets.every(h => JSON.stringify(h) === firstJson);
|
|
179
310
|
return allSame ? first : undefined;
|
|
180
311
|
}
|
|
181
312
|
|
|
182
|
-
|
|
183
|
-
|
|
313
|
+
// ARV-212 (R13/F16): generator-level "the caller wired auth_token in
|
|
314
|
+
// .env.yaml even though the spec has no securitySchemes" hint. Set at the
|
|
315
|
+
// top of generateSuites and consulted by getSuiteHeaders / generateCrudSuite
|
|
316
|
+
// / generateSanitySuite. Module-scoped to avoid threading through ~7 call
|
|
317
|
+
// sites. Always reset to null at the end of generateSuites so the helper
|
|
318
|
+
// stays stateless from the caller's perspective.
|
|
319
|
+
let _suiteDefaultAuthVar: string | null = null;
|
|
320
|
+
|
|
321
|
+
/** Same module-scoping rationale as `_suiteDefaultAuthVar` above: computed
|
|
322
|
+
* once from the full endpoint list at the top of generateSuites (ARV-369
|
|
323
|
+
* needs the whole spec to detect a param-name collision), consulted by
|
|
324
|
+
* `generateStep`'s path conversion. Reset to empty at the end of
|
|
325
|
+
* generateSuites. */
|
|
326
|
+
let _ambiguousPathParams: Set<string> = new Set();
|
|
327
|
+
|
|
328
|
+
/** Common id-like field names looked up after `id` itself.
|
|
329
|
+
* TASK-139: many real-world APIs return `slug`, `uuid`, `version`, `key`,
|
|
330
|
+
* or `name` instead of an `id` field on create responses. Without these,
|
|
331
|
+
* CRUD chains fall back to capturing `"id"` from a body that doesn't have
|
|
332
|
+
* one, breaking the `{id}` substitution in follow-up reads. */
|
|
333
|
+
const ID_LIKE_NAMES = ["slug", "uuid", "key", "version", "name"];
|
|
334
|
+
|
|
335
|
+
/** Find the best field to capture from POST response (for CRUD chains).
|
|
336
|
+
*
|
|
337
|
+
* Priority:
|
|
338
|
+
* 1. Field whose name matches the path-param (e.g. `{rule_id}` → `rule_id`
|
|
339
|
+
* or `{slug}` → `slug`). The path-param name is the strongest hint —
|
|
340
|
+
* whatever the response calls "the id of this resource" is what gets
|
|
341
|
+
* interpolated back into the read/update/delete URLs.
|
|
342
|
+
* 2. `id` (most common case).
|
|
343
|
+
* 3. Conventional id-like names: `slug`, `uuid`, `key`, `version`, `name`
|
|
344
|
+
* — but only if they are typed as a string (avoids capturing a `name`
|
|
345
|
+
* object on resources that nest metadata).
|
|
346
|
+
* 4. Any field with `type: integer` or `format: uuid`.
|
|
347
|
+
* 5. Fallback: `"id"` (the YAML capture will simply be empty if absent —
|
|
348
|
+
* the runner already handles this gracefully).
|
|
349
|
+
*/
|
|
350
|
+
function getCaptureField(ep: EndpointInfo, idParam?: string): string {
|
|
184
351
|
const schema = getSuccessSchema(ep);
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
352
|
+
const props = schema?.properties;
|
|
353
|
+
if (!props) return "id";
|
|
354
|
+
|
|
355
|
+
// 1. Path-param name match.
|
|
356
|
+
if (idParam) {
|
|
357
|
+
if (idParam in props) return idParam;
|
|
358
|
+
// Also try the conventional `<resource>_id` ↔ `id` swap.
|
|
359
|
+
if (idParam.endsWith("_id") && "id" in props) return "id";
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// 2. Plain `id`.
|
|
363
|
+
if ("id" in props) return "id";
|
|
364
|
+
|
|
365
|
+
// 3. Conventional id-like names (string-typed only).
|
|
366
|
+
for (const candidate of ID_LIKE_NAMES) {
|
|
367
|
+
if (candidate in props) {
|
|
368
|
+
const s = props[candidate] as OpenAPIV3.SchemaObject;
|
|
369
|
+
if (s.type === "string") return candidate;
|
|
190
370
|
}
|
|
191
371
|
}
|
|
372
|
+
|
|
373
|
+
// 4. Any integer or uuid-shaped field.
|
|
374
|
+
for (const [name, propSchema] of Object.entries(props)) {
|
|
375
|
+
const s = propSchema as OpenAPIV3.SchemaObject;
|
|
376
|
+
if (s.type === "integer" || s.format === "uuid") return name;
|
|
377
|
+
}
|
|
378
|
+
|
|
192
379
|
return "id";
|
|
193
380
|
}
|
|
194
381
|
|
|
@@ -200,10 +387,86 @@ function isAuthEndpoint(ep: EndpointInfo): boolean {
|
|
|
200
387
|
return false;
|
|
201
388
|
}
|
|
202
389
|
|
|
390
|
+
// ──────────────────────────────────────────────
|
|
391
|
+
// Provenance helpers
|
|
392
|
+
// ──────────────────────────────────────────────
|
|
393
|
+
|
|
394
|
+
function escapeJsonPointerSegment(s: string): string {
|
|
395
|
+
return s.replace(/~/g, "~0").replace(/\//g, "~1");
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function pickPrimaryStatus(status: number | number[]): number {
|
|
399
|
+
return Array.isArray(status) ? (status[0] ?? 200) : status;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/** Build step-level provenance for an endpoint + chosen response status. */
|
|
403
|
+
export function buildStepSource(
|
|
404
|
+
ep: EndpointInfo,
|
|
405
|
+
statusOverride?: number | number[],
|
|
406
|
+
): SourceMetadata {
|
|
407
|
+
const method = ep.method.toUpperCase();
|
|
408
|
+
const status = statusOverride ?? getExpectedStatus(ep);
|
|
409
|
+
const primary = pickPrimaryStatus(status);
|
|
410
|
+
const responseBranch = Array.isArray(status) ? status.map(String).join("|") : String(status);
|
|
411
|
+
const escapedPath = escapeJsonPointerSegment(ep.path);
|
|
412
|
+
return {
|
|
413
|
+
endpoint: `${method} ${ep.path}`,
|
|
414
|
+
response_branch: responseBranch,
|
|
415
|
+
schema_pointer: `#/paths/${escapedPath}/${method.toLowerCase()}/responses/${primary}`,
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/** Build suite-level provenance for an openapi-generated suite. */
|
|
420
|
+
export function buildOpenApiSuiteSource(specPath?: string): SourceMetadata | undefined {
|
|
421
|
+
if (!specPath) return undefined;
|
|
422
|
+
return {
|
|
423
|
+
type: "openapi-generated",
|
|
424
|
+
spec: specPath,
|
|
425
|
+
generator: "zond-generate",
|
|
426
|
+
generated_at: new Date().toISOString(),
|
|
427
|
+
};
|
|
428
|
+
}
|
|
429
|
+
|
|
203
430
|
// ──────────────────────────────────────────────
|
|
204
431
|
// Public API
|
|
205
432
|
// ──────────────────────────────────────────────
|
|
206
433
|
|
|
434
|
+
/**
|
|
435
|
+
* ARV-45: swap FK-shaped required body fields for `{{fixture}}` references so
|
|
436
|
+
* their values come from `.env.yaml` instead of `{{$randomString}}`/`{{$uuid}}`
|
|
437
|
+
* junk that 400s and kills the CRUD chain at step 1. Field names that motivated
|
|
438
|
+
* this — `sequenceTypeCode`, `templateGroupCode` — are closed-vocab codes the
|
|
439
|
+
* generator can't guess. The var name is canonicalised (`sequence_type_code`)
|
|
440
|
+
* to match the manifest entry (fixtures-builder step 5); the HTTP body key
|
|
441
|
+
* keeps the raw spec spelling. Recurses into nested objects for nested FKs.
|
|
442
|
+
*
|
|
443
|
+
* Keep the detection predicate (`isFkFixtureField`) in lockstep with the
|
|
444
|
+
* manifest builder — every var the tests reference must appear in the manifest
|
|
445
|
+
* (decision-7).
|
|
446
|
+
*/
|
|
447
|
+
export function wireBodyFkRefs(schema: OpenAPIV3.SchemaObject, body: unknown): unknown {
|
|
448
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) return body;
|
|
449
|
+
// `effectiveObjectShape` merges allOf so allOf-wrapped bodies (the .NET/
|
|
450
|
+
// Swagger norm) resolve their properties — a direct schema.properties read
|
|
451
|
+
// misses every FK on those specs.
|
|
452
|
+
const { properties, required } = effectiveObjectShape(schema);
|
|
453
|
+
const out = body as Record<string, unknown>;
|
|
454
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
455
|
+
if (!(name in out)) continue;
|
|
456
|
+
if (required.has(name) && isFkFixtureField(name, propSchema)) {
|
|
457
|
+
out[name] = `{{${canonicalVarName(name)}}}`;
|
|
458
|
+
} else if (out[name] && typeof out[name] === "object" && !Array.isArray(out[name])) {
|
|
459
|
+
out[name] = wireBodyFkRefs(propSchema, out[name]);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
return out;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
/** Generate a request body from a schema with FK fields wired to fixtures. */
|
|
466
|
+
function generateBody(schema: OpenAPIV3.SchemaObject): unknown {
|
|
467
|
+
return wireBodyFkRefs(schema, generateFromSchema(schema));
|
|
468
|
+
}
|
|
469
|
+
|
|
207
470
|
/** Generate a single test step from an EndpointInfo */
|
|
208
471
|
export function generateStep(
|
|
209
472
|
ep: EndpointInfo,
|
|
@@ -211,10 +474,11 @@ export function generateStep(
|
|
|
211
474
|
): RawStep {
|
|
212
475
|
const method = ep.method.toUpperCase();
|
|
213
476
|
const name = ep.operationId ?? ep.summary ?? `${method} ${ep.path}`;
|
|
214
|
-
const path = convertPath(ep.path);
|
|
477
|
+
const path = convertPath(ep.path, _ambiguousPathParams);
|
|
215
478
|
|
|
216
479
|
const step: RawStep = {
|
|
217
480
|
name,
|
|
481
|
+
source: buildStepSource(ep),
|
|
218
482
|
[method]: path,
|
|
219
483
|
expect: {
|
|
220
484
|
status: getExpectedStatus(ep),
|
|
@@ -229,8 +493,14 @@ export function generateStep(
|
|
|
229
493
|
if (["POST", "PUT", "PATCH"].includes(method) && ep.requestBodySchema) {
|
|
230
494
|
if (ep.requestBodyContentType === "multipart/form-data") {
|
|
231
495
|
step.multipart = generateMultipartFromSchema(ep.requestBodySchema);
|
|
496
|
+
} else if (ep.requestBodyContentType === "application/x-www-form-urlencoded") {
|
|
497
|
+
// ARV-149: form-encoded endpoints (Stripe v1 et al.) — emit `form:` so
|
|
498
|
+
// the runner posts URL-encoded bodies with bracket notation. Without
|
|
499
|
+
// this, generate baked `json:` blocks and every POST 400'd with
|
|
500
|
+
// "wrong content type".
|
|
501
|
+
step.form = flattenToFormFields(generateBody(ep.requestBodySchema));
|
|
232
502
|
} else {
|
|
233
|
-
step.json =
|
|
503
|
+
step.json = generateBody(ep.requestBodySchema);
|
|
234
504
|
}
|
|
235
505
|
}
|
|
236
506
|
|
|
@@ -247,33 +517,173 @@ export function generateStep(
|
|
|
247
517
|
return step;
|
|
248
518
|
}
|
|
249
519
|
|
|
250
|
-
/**
|
|
520
|
+
/** Strip a single trailing slash for comparison purposes. We never rewrite
|
|
521
|
+
* endpoint paths in the spec — we just normalise the matching regex so
|
|
522
|
+
* `POST /alerts/` + `GET /alerts/{id}/` lines up the same as the no-slash
|
|
523
|
+
* variant. */
|
|
524
|
+
function stripTrailingSlash(p: string): string {
|
|
525
|
+
return p.length > 1 && p.endsWith("/") ? p.slice(0, -1) : p;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Per-resource diagnostic record used by `zond generate --explain`.
|
|
529
|
+
* Captures every POST candidate the detector considered and the verdict
|
|
530
|
+
* with a human reason — so users can see "I have a CRUD-looking pair, why
|
|
531
|
+
* didn't generate emit a chain?" without grepping the spec. */
|
|
532
|
+
export interface CrudDetectionDiagnostic {
|
|
533
|
+
resource: string;
|
|
534
|
+
basePath: string;
|
|
535
|
+
postPath: string;
|
|
536
|
+
hasGetById: boolean;
|
|
537
|
+
hasUpdate: boolean;
|
|
538
|
+
hasDelete: boolean;
|
|
539
|
+
hasList: boolean;
|
|
540
|
+
verdict: "chain" | "skipped";
|
|
541
|
+
reason: string;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
export interface DetectCrudResult {
|
|
545
|
+
groups: CrudGroup[];
|
|
546
|
+
diagnostics: CrudDetectionDiagnostic[];
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/** Detect CRUD groups from a list of endpoints.
|
|
550
|
+
*
|
|
551
|
+
* Match logic (TASK-139):
|
|
552
|
+
* - basePath = POST endpoint's path with any trailing slash trimmed.
|
|
553
|
+
* - item path = `<basePath>/{param}` with optional trailing slash.
|
|
554
|
+
* This catches common SaaS-style `POST /alert-rules/` + `GET /alert-rules/{id}/`
|
|
555
|
+
* pairs that previously fell through because the regex required the same
|
|
556
|
+
* slash form on both. */
|
|
251
557
|
export function detectCrudGroups(endpoints: EndpointInfo[]): CrudGroup[] {
|
|
558
|
+
return detectCrudGroupsWithDiagnostics(endpoints).groups;
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
export function detectCrudGroupsWithDiagnostics(
|
|
562
|
+
endpoints: EndpointInfo[],
|
|
563
|
+
): DetectCrudResult {
|
|
252
564
|
const groups: CrudGroup[] = [];
|
|
253
|
-
const
|
|
565
|
+
const diagnostics: CrudDetectionDiagnostic[] = [];
|
|
566
|
+
const postEndpoints = endpoints.filter(
|
|
567
|
+
ep => ep.method.toUpperCase() === "POST" && !ep.deprecated,
|
|
568
|
+
);
|
|
254
569
|
|
|
255
570
|
for (const createEp of postEndpoints) {
|
|
256
|
-
const basePath = createEp.path;
|
|
571
|
+
const basePath = stripTrailingSlash(createEp.path);
|
|
572
|
+
// ARV-372: skip trailing version segments (`v1`, `v30`) before taking the
|
|
573
|
+
// last segment as the resource name — otherwise every collection on a
|
|
574
|
+
// spec shaped `/api/<resource>/v{N}` (a common convention) resolves to
|
|
575
|
+
// the SAME resource name ("v30"), and multiple CRUD suites collide on
|
|
576
|
+
// the same `crud-<resource>.yaml` output filename, silently overwriting
|
|
577
|
+
// each other.
|
|
578
|
+
const resource = stripTrailingVersionSegments(basePath.split("/").filter(Boolean)).pop() ?? "resource";
|
|
579
|
+
|
|
580
|
+
// Match `<basePath>/{param}` with optional trailing slash. Tolerates
|
|
581
|
+
// both `POST /alerts/` + `GET /alerts/{id}` and `POST /alerts` +
|
|
582
|
+
// `GET /alerts/{id}/`, which some real-world specs mix.
|
|
583
|
+
const itemPattern = new RegExp(`^${escapeRegex(basePath)}/\\{([^}]+)\\}/?$`);
|
|
584
|
+
const itemEndpoints = endpoints.filter(
|
|
585
|
+
ep => !ep.deprecated && itemPattern.test(ep.path),
|
|
586
|
+
);
|
|
257
587
|
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
588
|
+
// Fallback for "subdomain"/nested-item routing (common SaaS-style):
|
|
589
|
+
// create lives under one root (`/api/0/organizations/{org}/teams/`)
|
|
590
|
+
// but item-path lives under another (`/api/0/teams/{org}/{team}/`).
|
|
591
|
+
// The strict basePath/{id} regex misses these. Match instead by:
|
|
592
|
+
// 1. shared OpenAPI tag with the create operation,
|
|
593
|
+
// 2. terminal {param} matching the singular form of the resource
|
|
594
|
+
// (`{team}` / `{team_id}` / `{team_id_or_slug}`).
|
|
595
|
+
let resolvedItemEndpoints = itemEndpoints;
|
|
596
|
+
if (resolvedItemEndpoints.length === 0) {
|
|
597
|
+
const singular = singularizeResource(resource).toLowerCase();
|
|
598
|
+
const itemTerminalRe = /\{([^}]+)\}\/?$/;
|
|
599
|
+
const matchesResourceParam = (p: string) => {
|
|
600
|
+
const m = p.match(itemTerminalRe);
|
|
601
|
+
if (!m) return false;
|
|
602
|
+
const param = m[1]!.toLowerCase();
|
|
603
|
+
return (
|
|
604
|
+
param === singular ||
|
|
605
|
+
param === `${singular}_id` ||
|
|
606
|
+
param === `${singular}_id_or_slug` ||
|
|
607
|
+
param === `${singular}_slug`
|
|
608
|
+
);
|
|
609
|
+
};
|
|
610
|
+
const createTags = new Set(createEp.tags ?? []);
|
|
611
|
+
const sharedTag = (ep: EndpointInfo) =>
|
|
612
|
+
(ep.tags ?? []).some(t => createTags.has(t));
|
|
613
|
+
|
|
614
|
+
resolvedItemEndpoints = endpoints.filter(
|
|
615
|
+
ep =>
|
|
616
|
+
!ep.deprecated &&
|
|
617
|
+
ep.path !== createEp.path &&
|
|
618
|
+
matchesResourceParam(ep.path) &&
|
|
619
|
+
sharedTag(ep),
|
|
620
|
+
);
|
|
621
|
+
}
|
|
261
622
|
|
|
262
|
-
|
|
623
|
+
const read = resolvedItemEndpoints.find(ep => ep.method.toUpperCase() === "GET");
|
|
624
|
+
const update = resolvedItemEndpoints.find(
|
|
625
|
+
ep => ["PUT", "PATCH"].includes(ep.method.toUpperCase()),
|
|
626
|
+
);
|
|
627
|
+
const del = resolvedItemEndpoints.find(ep => ep.method.toUpperCase() === "DELETE");
|
|
628
|
+
// List endpoint matches with the same trailing-slash tolerance. ARV-376:
|
|
629
|
+
// also accept a `/list`, `/search`, `/find` verb suffix on the base path
|
|
630
|
+
// (`GET /api/deal-kind20/list`) — common in RPC-flavoured REST specs
|
|
631
|
+
// where the bare collection path has no GET.
|
|
632
|
+
const listStems = [basePath, `${basePath}/list`, `${basePath}/search`, `${basePath}/find`];
|
|
633
|
+
const list = endpoints.find(
|
|
634
|
+
ep =>
|
|
635
|
+
ep.method.toUpperCase() === "GET" &&
|
|
636
|
+
listStems.includes(stripTrailingSlash(ep.path)) &&
|
|
637
|
+
!ep.deprecated,
|
|
638
|
+
);
|
|
263
639
|
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
640
|
+
const diag: CrudDetectionDiagnostic = {
|
|
641
|
+
resource,
|
|
642
|
+
basePath,
|
|
643
|
+
postPath: createEp.path,
|
|
644
|
+
hasGetById: !!read,
|
|
645
|
+
hasUpdate: !!update,
|
|
646
|
+
hasDelete: !!del,
|
|
647
|
+
hasList: !!list,
|
|
648
|
+
verdict: "skipped",
|
|
649
|
+
reason: "",
|
|
650
|
+
};
|
|
268
651
|
|
|
269
|
-
|
|
270
|
-
|
|
652
|
+
if (resolvedItemEndpoints.length === 0) {
|
|
653
|
+
diag.reason = `no item endpoint matching ${basePath}/{...}`;
|
|
654
|
+
diagnostics.push(diag);
|
|
655
|
+
continue;
|
|
656
|
+
}
|
|
657
|
+
// TASK-260: accept headless chains — POST + (GET/PUT/PATCH/DELETE on /{id}).
|
|
658
|
+
// Resources with no GET-by-id (e.g. external-teams, some user-binding endpoints)
|
|
659
|
+
// were previously skipped entirely, even though POST captures the ID and PUT/DELETE
|
|
660
|
+
// can drive the chain on their own. The Read/Verify steps in the suite generator
|
|
661
|
+
// are already conditional on `group.read`, so headless chains generate cleanly.
|
|
662
|
+
if (!read && !update && !del) {
|
|
663
|
+
diag.reason = "item endpoint exists but no GET/PUT/PATCH/DELETE on /{id}";
|
|
664
|
+
diagnostics.push(diag);
|
|
665
|
+
continue;
|
|
666
|
+
}
|
|
271
667
|
|
|
272
|
-
const
|
|
273
|
-
const
|
|
274
|
-
|
|
668
|
+
const itemPath = resolvedItemEndpoints[0]!.path;
|
|
669
|
+
const idMatch = itemPath.match(/\{([^}]+)\}\/?$/);
|
|
670
|
+
if (!idMatch) {
|
|
671
|
+
diag.reason = "item path has no terminal {param}";
|
|
672
|
+
diagnostics.push(diag);
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
const idParam = idMatch[1]!;
|
|
275
676
|
|
|
276
|
-
|
|
677
|
+
diag.verdict = "chain";
|
|
678
|
+
if (read) {
|
|
679
|
+
diag.reason = "POST + GET/{id} matched";
|
|
680
|
+
} else {
|
|
681
|
+
// TASK-260: explicit headless reason so `--explain` differentiates the
|
|
682
|
+
// two chain shapes — useful for debugging fixture flow.
|
|
683
|
+
const partner = update ? `${update.method.toUpperCase()}/{id}` : `DELETE/{id}`;
|
|
684
|
+
diag.reason = `POST + ${partner} matched (headless: no GET-by-id)`;
|
|
685
|
+
}
|
|
686
|
+
diagnostics.push(diag);
|
|
277
687
|
|
|
278
688
|
groups.push({
|
|
279
689
|
resource,
|
|
@@ -288,16 +698,44 @@ export function detectCrudGroups(endpoints: EndpointInfo[]): CrudGroup[] {
|
|
|
288
698
|
});
|
|
289
699
|
}
|
|
290
700
|
|
|
291
|
-
return groups;
|
|
701
|
+
return { groups, diagnostics };
|
|
292
702
|
}
|
|
293
703
|
|
|
294
704
|
/** Generate a CRUD chain suite from a CrudGroup */
|
|
705
|
+
/** ARV-368: does the create's success response actually carry the capture
|
|
706
|
+
* field? If not — a 204 no-body create, or a response schema without the id —
|
|
707
|
+
* the runtime capture is empty and `{{captureVar}}` falls back to the
|
|
708
|
+
* read-fixture of the SAME name (ARV-137 deliberately shares it). A PUT/DELETE
|
|
709
|
+
* then targets a *pre-existing* resource whose id the user harvested for read
|
|
710
|
+
* coverage — silent data-loss. Gate mutating chain steps on this so the suite
|
|
711
|
+
* can only ever update/delete what it actually captured, never live data. */
|
|
712
|
+
function createCapturesId(
|
|
713
|
+
create: EndpointInfo | undefined,
|
|
714
|
+
captureField: string,
|
|
715
|
+
): boolean {
|
|
716
|
+
if (!create) return false;
|
|
717
|
+
const props = getSuccessSchema(create)?.properties;
|
|
718
|
+
return !!props && captureField in props;
|
|
719
|
+
}
|
|
720
|
+
|
|
295
721
|
export function generateCrudSuite(
|
|
296
722
|
group: CrudGroup,
|
|
297
723
|
securitySchemes: SecuritySchemeInfo[],
|
|
298
724
|
): RawSuite {
|
|
299
|
-
const captureField = group.create ? getCaptureField(group.create) : "id";
|
|
300
|
-
|
|
725
|
+
const captureField = group.create ? getCaptureField(group.create, group.idParam) : "id";
|
|
726
|
+
// ARV-368: only chain PUT/DELETE when the create response yields the id we'd
|
|
727
|
+
// capture. Otherwise the capture is empty at runtime and the mutating step
|
|
728
|
+
// falls back to the shared read-fixture → deletes/overwrites pre-existing data.
|
|
729
|
+
const canChainMutations = createCapturesId(group.create, captureField);
|
|
730
|
+
// ARV-137: use the spec's path-param name as the capture var. Previously
|
|
731
|
+
// we synthesised `<resource>_id` via `resourceVar(...)`, which produced
|
|
732
|
+
// phantom manifest dupes whenever the spec named the path-param anything
|
|
733
|
+
// other than `<resource>_id` (e.g. `monitor_id_or_slug`, `version`, or
|
|
734
|
+
// collection-stem mismatches like resource=`saved`/idParam=`query_id`).
|
|
735
|
+
// Aligning on `group.idParam` keeps tests, manifest, and spec consistent.
|
|
736
|
+
// Fallback to `resourceVar` only when the group has no idParam (defensive
|
|
737
|
+
// — shouldn't happen for any group with a read/update/delete endpoint).
|
|
738
|
+
const captureVar = group.idParam || resourceVar(group.resource, "id");
|
|
301
739
|
const tests: RawStep[] = [];
|
|
302
740
|
|
|
303
741
|
const allEps = [group.create, group.list, group.read, group.update, group.delete].filter(Boolean) as EndpointInfo[];
|
|
@@ -322,7 +760,8 @@ export function generateCrudSuite(
|
|
|
322
760
|
// 2. Read created
|
|
323
761
|
if (group.read) {
|
|
324
762
|
const step: RawStep = {
|
|
325
|
-
name: group.read.operationId ?? `Read created ${group.resource
|
|
763
|
+
name: group.read.operationId ?? `Read created ${singularizeResource(group.resource)}`,
|
|
764
|
+
source: buildStepSource(group.read),
|
|
326
765
|
GET: convertPath(group.itemPath).replace(`{{${group.idParam}}}`, `{{${captureVar}}}`),
|
|
327
766
|
expect: {
|
|
328
767
|
status: getExpectedStatus(group.read),
|
|
@@ -332,16 +771,18 @@ export function generateCrudSuite(
|
|
|
332
771
|
tests.push(step);
|
|
333
772
|
}
|
|
334
773
|
|
|
335
|
-
// 3. Update
|
|
336
|
-
|
|
774
|
+
// 3. Update (ARV-368: only if the chain self-captures its id — else the
|
|
775
|
+
// fixture-fallback would overwrite a pre-existing resource)
|
|
776
|
+
if (group.update && canChainMutations) {
|
|
337
777
|
const method = group.update.method.toUpperCase();
|
|
338
778
|
const itemPath = convertPath(group.itemPath).replace(`{{${group.idParam}}}`, `{{${captureVar}}}`);
|
|
339
|
-
const etagVar =
|
|
779
|
+
const etagVar = resourceVar(group.resource, "etag");
|
|
340
780
|
|
|
341
781
|
// If endpoint requires ETag (optimistic locking), capture it from a GET step first
|
|
342
782
|
if (group.update.requiresEtag && group.read) {
|
|
343
783
|
tests.push({
|
|
344
|
-
name: `Get ETag before update ${group.resource
|
|
784
|
+
name: `Get ETag before update ${singularizeResource(group.resource)}`,
|
|
785
|
+
source: buildStepSource(group.read),
|
|
345
786
|
GET: itemPath,
|
|
346
787
|
expect: {
|
|
347
788
|
status: getExpectedStatus(group.read),
|
|
@@ -351,7 +792,8 @@ export function generateCrudSuite(
|
|
|
351
792
|
}
|
|
352
793
|
|
|
353
794
|
const step: RawStep = {
|
|
354
|
-
name: group.update.operationId ?? `Update ${group.resource
|
|
795
|
+
name: group.update.operationId ?? `Update ${singularizeResource(group.resource)}`,
|
|
796
|
+
source: buildStepSource(group.update),
|
|
355
797
|
[method]: itemPath,
|
|
356
798
|
expect: {
|
|
357
799
|
status: getExpectedStatus(group.update),
|
|
@@ -361,21 +803,23 @@ export function generateCrudSuite(
|
|
|
361
803
|
step.headers = { "If-Match": `"{{${etagVar}}}"` };
|
|
362
804
|
}
|
|
363
805
|
if (group.update.requestBodySchema) {
|
|
364
|
-
step.json =
|
|
806
|
+
step.json = generateBody(group.update.requestBodySchema);
|
|
365
807
|
}
|
|
366
808
|
tests.push(step);
|
|
367
809
|
}
|
|
368
810
|
|
|
369
|
-
// 4. Delete
|
|
370
|
-
|
|
811
|
+
// 4. Delete (ARV-368: only if the chain self-captures its id — else the
|
|
812
|
+
// fixture-fallback would DELETE a pre-existing resource → data-loss)
|
|
813
|
+
if (group.delete && canChainMutations) {
|
|
371
814
|
const itemPath = convertPath(group.itemPath).replace(`{{${group.idParam}}}`, `{{${captureVar}}}`);
|
|
372
|
-
const etagVar =
|
|
815
|
+
const etagVar = resourceVar(group.resource, "etag");
|
|
373
816
|
|
|
374
817
|
// If delete requires ETag and update didn't already capture it, add a GET step
|
|
375
818
|
const updateAlreadyCapturedEtag = group.update?.requiresEtag;
|
|
376
819
|
if (group.delete.requiresEtag && group.read && !updateAlreadyCapturedEtag) {
|
|
377
820
|
tests.push({
|
|
378
|
-
name: `Get ETag before delete ${group.resource
|
|
821
|
+
name: `Get ETag before delete ${singularizeResource(group.resource)}`,
|
|
822
|
+
source: buildStepSource(group.read),
|
|
379
823
|
GET: itemPath,
|
|
380
824
|
expect: {
|
|
381
825
|
status: getExpectedStatus(group.read),
|
|
@@ -386,7 +830,8 @@ export function generateCrudSuite(
|
|
|
386
830
|
|
|
387
831
|
// T44: cleanup must run even if earlier assertions failed (tainted captures)
|
|
388
832
|
const step: RawStep = {
|
|
389
|
-
name: group.delete.operationId ?? `Delete ${group.resource
|
|
833
|
+
name: group.delete.operationId ?? `Delete ${singularizeResource(group.resource)}`,
|
|
834
|
+
source: buildStepSource(group.delete),
|
|
390
835
|
DELETE: itemPath,
|
|
391
836
|
always: true,
|
|
392
837
|
expect: {
|
|
@@ -401,7 +846,8 @@ export function generateCrudSuite(
|
|
|
401
846
|
// 5. Verify deleted — also always, so we confirm cleanup happened
|
|
402
847
|
if (group.read) {
|
|
403
848
|
tests.push({
|
|
404
|
-
name: `Verify ${group.resource
|
|
849
|
+
name: `Verify ${singularizeResource(group.resource)} deleted`,
|
|
850
|
+
source: buildStepSource(group.read, 404),
|
|
405
851
|
GET: convertPath(group.itemPath).replace(`{{${group.idParam}}}`, `{{${captureVar}}}`),
|
|
406
852
|
always: true,
|
|
407
853
|
expect: {
|
|
@@ -594,7 +1040,7 @@ function generateConsistentAuthSuite(
|
|
|
594
1040
|
}
|
|
595
1041
|
|
|
596
1042
|
/** Generate 1-2 minimal tests for quick connectivity and auth validation */
|
|
597
|
-
|
|
1043
|
+
function generateSanitySuite(opts: {
|
|
598
1044
|
authEndpoints: EndpointInfo[];
|
|
599
1045
|
nonAuthGetEndpoints: EndpointInfo[];
|
|
600
1046
|
securitySchemes: SecuritySchemeInfo[];
|
|
@@ -631,11 +1077,23 @@ export function generateSanitySuite(opts: {
|
|
|
631
1077
|
export function generateSuites(opts: {
|
|
632
1078
|
endpoints: EndpointInfo[];
|
|
633
1079
|
securitySchemes: SecuritySchemeInfo[];
|
|
1080
|
+
/** Path to OpenAPI spec, recorded in suite-level provenance. */
|
|
1081
|
+
specPath?: string;
|
|
1082
|
+
/** When true, deprecated endpoints are included instead of filtered out. */
|
|
1083
|
+
includeDeprecated?: boolean;
|
|
1084
|
+
/** ARV-212 (R13/F16): inject `Authorization: Bearer {{<varName>}}` at the
|
|
1085
|
+
* suite level when the spec declares no securitySchemes but the workspace
|
|
1086
|
+
* .env.yaml carries this auth-token variable. Lets generated suites talk
|
|
1087
|
+
* to bare-spec APIs (GitHub) without going unauth. */
|
|
1088
|
+
defaultAuthVar?: string;
|
|
634
1089
|
}): RawSuite[] {
|
|
635
|
-
const { endpoints, securitySchemes } = opts;
|
|
1090
|
+
const { endpoints, securitySchemes, specPath, includeDeprecated, defaultAuthVar } = opts;
|
|
1091
|
+
_suiteDefaultAuthVar = defaultAuthVar ?? null;
|
|
1092
|
+
_ambiguousPathParams = computeAmbiguousPathParams(endpoints);
|
|
636
1093
|
|
|
637
|
-
// Filter deprecated
|
|
638
|
-
|
|
1094
|
+
// Filter deprecated unless caller opted in. The list of skipped paths is
|
|
1095
|
+
// exposed separately via `getSkippedDeprecated` for stdout reporting.
|
|
1096
|
+
const active = includeDeprecated ? endpoints : endpoints.filter(ep => !ep.deprecated);
|
|
639
1097
|
|
|
640
1098
|
// Separate auth endpoints
|
|
641
1099
|
const authEndpoints = active.filter(isAuthEndpoint);
|
|
@@ -670,27 +1128,52 @@ export function generateSuites(opts: {
|
|
|
670
1128
|
const paramlessGets = getEndpoints.filter(ep => !endpointHasPathParams(ep));
|
|
671
1129
|
const pathParamGets = getEndpoints.filter(ep => endpointHasPathParams(ep));
|
|
672
1130
|
|
|
673
|
-
//
|
|
674
|
-
|
|
675
|
-
|
|
1131
|
+
// Positive smoke: paramless GETs (no env needed) + path-param GETs
|
|
1132
|
+
// (with skip_if guards). TASK-240 — unified naming convention:
|
|
1133
|
+
// always emit `smoke-<tag>-positive.yaml`, never the bare
|
|
1134
|
+
// `smoke-<tag>.yaml`, so file listings don't have to explain why a
|
|
1135
|
+
// tag has only `-negative` (e.g. a vendor-specific tag) or why two
|
|
1136
|
+
// siblings differ in suffix shape.
|
|
1137
|
+
const positiveTests = [
|
|
1138
|
+
...paramlessGets.map(ep => {
|
|
676
1139
|
const step = generateStep(ep, securitySchemes);
|
|
677
1140
|
const seededPath = convertPathWithSeeds(ep.path, ep);
|
|
678
1141
|
(step as any)[ep.method.toUpperCase()] = seededPath;
|
|
679
1142
|
return step;
|
|
680
|
-
})
|
|
681
|
-
|
|
1143
|
+
}),
|
|
1144
|
+
...pathParamGets.map(ep => {
|
|
1145
|
+
const step = generateStep(ep, securitySchemes);
|
|
1146
|
+
// Path stays as {{param}} so user-provided env values flow in.
|
|
1147
|
+
// skip_if guards an unset path-param without skipping paramless
|
|
1148
|
+
// siblings that don't need a fixture.
|
|
1149
|
+
const firstPathParam = ep.parameters.find(p => p.in === "path");
|
|
1150
|
+
if (firstPathParam) {
|
|
1151
|
+
step.skip_if = `{{${firstPathParam.name}}} ==`;
|
|
1152
|
+
}
|
|
1153
|
+
return step;
|
|
1154
|
+
}),
|
|
1155
|
+
];
|
|
1156
|
+
|
|
1157
|
+
if (positiveTests.length > 0) {
|
|
1158
|
+
const positiveEndpoints = [...paramlessGets, ...pathParamGets];
|
|
1159
|
+
const headers = getSuiteHeaders(positiveEndpoints, securitySchemes);
|
|
1160
|
+
// needs-id only when at least one test depends on a path-param
|
|
1161
|
+
// fixture — coverage downgrades these suites when env is empty.
|
|
1162
|
+
const tags = pathParamGets.length > 0
|
|
1163
|
+
? ["smoke", "positive", "needs-id"]
|
|
1164
|
+
: ["smoke", "positive"];
|
|
682
1165
|
|
|
683
1166
|
const suite: RawSuite = {
|
|
684
|
-
name: `${tagSlug}-smoke`,
|
|
685
|
-
tags
|
|
686
|
-
fileStem: `smoke-${tagSlug}`,
|
|
1167
|
+
name: `${tagSlug}-smoke-positive`,
|
|
1168
|
+
tags,
|
|
1169
|
+
fileStem: `smoke-${tagSlug}-positive`,
|
|
687
1170
|
base_url: "{{base_url}}",
|
|
688
|
-
tests,
|
|
1171
|
+
tests: positiveTests,
|
|
689
1172
|
};
|
|
690
1173
|
|
|
691
1174
|
if (headers) {
|
|
692
1175
|
suite.headers = headers;
|
|
693
|
-
for (const t of
|
|
1176
|
+
for (const t of positiveTests) {
|
|
694
1177
|
if (t.headers && JSON.stringify(t.headers) === JSON.stringify(headers)) {
|
|
695
1178
|
delete (t as any).headers;
|
|
696
1179
|
}
|
|
@@ -731,40 +1214,6 @@ export function generateSuites(opts: {
|
|
|
731
1214
|
suites.push(suite);
|
|
732
1215
|
}
|
|
733
1216
|
|
|
734
|
-
// Positive smoke: path-param GETs with {{var}} placeholders + skip_if for unset env
|
|
735
|
-
if (pathParamGets.length > 0) {
|
|
736
|
-
const tests = pathParamGets.map(ep => {
|
|
737
|
-
const step = generateStep(ep, securitySchemes);
|
|
738
|
-
// Path stays as {{param}} so user-provided env values flow in
|
|
739
|
-
// Pick the first path param for skip_if guard (the resource ID)
|
|
740
|
-
const firstPathParam = ep.parameters.find(p => p.in === "path");
|
|
741
|
-
if (firstPathParam) {
|
|
742
|
-
step.skip_if = `{{${firstPathParam.name}}} ==`;
|
|
743
|
-
}
|
|
744
|
-
return step;
|
|
745
|
-
});
|
|
746
|
-
const headers = getSuiteHeaders(pathParamGets, securitySchemes);
|
|
747
|
-
|
|
748
|
-
const suite: RawSuite = {
|
|
749
|
-
name: `${tagSlug}-smoke-positive`,
|
|
750
|
-
tags: ["smoke", "positive", "needs-id"],
|
|
751
|
-
fileStem: `smoke-${tagSlug}-positive`,
|
|
752
|
-
base_url: "{{base_url}}",
|
|
753
|
-
tests,
|
|
754
|
-
};
|
|
755
|
-
|
|
756
|
-
if (headers) {
|
|
757
|
-
suite.headers = headers;
|
|
758
|
-
for (const t of tests) {
|
|
759
|
-
if (t.headers && JSON.stringify(t.headers) === JSON.stringify(headers)) {
|
|
760
|
-
delete (t as any).headers;
|
|
761
|
-
}
|
|
762
|
-
}
|
|
763
|
-
}
|
|
764
|
-
|
|
765
|
-
suites.push(suite);
|
|
766
|
-
}
|
|
767
|
-
|
|
768
1217
|
// Non-GET endpoints: split reset/system endpoints out of smoke-unsafe
|
|
769
1218
|
const nonGetEndpoints = tagEndpoints.filter(ep => ep.method.toUpperCase() !== "GET");
|
|
770
1219
|
const resetEndpoints = nonGetEndpoints.filter(ep => RESET_PATH_RE.test(ep.path));
|
|
@@ -836,5 +1285,17 @@ export function generateSuites(opts: {
|
|
|
836
1285
|
const nonAuthGetEndpoints = nonAuth.filter(ep => ep.method.toUpperCase() === "GET");
|
|
837
1286
|
const sanitySuite = generateSanitySuite({ authEndpoints, nonAuthGetEndpoints, securitySchemes });
|
|
838
1287
|
|
|
839
|
-
|
|
1288
|
+
const allSuites = sanitySuite ? [sanitySuite, ...suites] : suites;
|
|
1289
|
+
|
|
1290
|
+
// Stamp suite-level provenance when a spec path is known.
|
|
1291
|
+
const suiteSrc = buildOpenApiSuiteSource(specPath);
|
|
1292
|
+
if (suiteSrc) {
|
|
1293
|
+
for (const s of allSuites) {
|
|
1294
|
+
s.source = suiteSrc;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
_suiteDefaultAuthVar = null; // ARV-212
|
|
1299
|
+
_ambiguousPathParams = new Set(); // ARV-369
|
|
1300
|
+
return allSuites;
|
|
840
1301
|
}
|