@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
|
@@ -0,0 +1,332 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build `.api-fixtures.yaml` — manifest of variables this API needs from
|
|
3
|
+
* the user's `.env.yaml`.
|
|
4
|
+
*
|
|
5
|
+
* Purpose: when the skill (or `zond doctor`, future) needs to tell the
|
|
6
|
+
* user what to fill in before scenarios will run, it reads this manifest
|
|
7
|
+
* instead of inferring fixtures from generated tests. The manifest is
|
|
8
|
+
* derived purely from the OpenAPI spec — auth schemes, required path
|
|
9
|
+
* params, server URL — so it's stable across re-runs of `generate`.
|
|
10
|
+
*
|
|
11
|
+
* Manifest is read-only (regenerate via `zond refresh-api`); user edits
|
|
12
|
+
* land in `.env.yaml`, not here.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { EndpointInfo, SecuritySchemeInfo } from "./types.ts";
|
|
16
|
+
import type { OpenAPIV3 } from "openapi-types";
|
|
17
|
+
import { schemeVarName, resourceVar, computeAmbiguousPathParams, fixtureVarNameForPathParam, owningCollectionForPathParam } from "./suite-generator.ts";
|
|
18
|
+
import type { ApiResourceMap } from "./resources-builder.ts";
|
|
19
|
+
import { canonicalVarName, isFkFixtureField, effectiveObjectShape } from "./data-factory.ts";
|
|
20
|
+
|
|
21
|
+
// canonicalVarName now lives in data-factory (leaf module, shared with the
|
|
22
|
+
// suite generator). Re-exported here for back-compat: create-body.ts and
|
|
23
|
+
// existing callers still import it from fixtures-builder.
|
|
24
|
+
export { canonicalVarName };
|
|
25
|
+
|
|
26
|
+
export type FixtureSource = "auth" | "server" | "path" | "header" | "body-fk" | "capture-chain";
|
|
27
|
+
|
|
28
|
+
export interface FixtureRequirement {
|
|
29
|
+
/** Variable name as referenced via {{var}} in tests. */
|
|
30
|
+
name: string;
|
|
31
|
+
/** Where this fixture comes from in the spec. */
|
|
32
|
+
source: FixtureSource;
|
|
33
|
+
/** Free-text description for the user (one line). */
|
|
34
|
+
description: string;
|
|
35
|
+
/** Endpoints affected if this fixture is missing (sample, ≤10). */
|
|
36
|
+
affectedEndpoints: string[];
|
|
37
|
+
/** True when at least one consumer marks this required. */
|
|
38
|
+
required: boolean;
|
|
39
|
+
/** Suggested placeholder value, used to seed `.env.yaml`. */
|
|
40
|
+
defaultValue?: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface ApiFixtureManifest {
|
|
44
|
+
generatedAt: string;
|
|
45
|
+
specHash: string;
|
|
46
|
+
fixtureCount: number;
|
|
47
|
+
fixtures: FixtureRequirement[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function epLabel(ep: EndpointInfo): string {
|
|
51
|
+
return `${ep.method.toUpperCase()} ${ep.path}`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function pushAffected(req: FixtureRequirement, ep: EndpointInfo): void {
|
|
55
|
+
if (req.affectedEndpoints.length >= 10) return;
|
|
56
|
+
const label = epLabel(ep);
|
|
57
|
+
if (!req.affectedEndpoints.includes(label)) req.affectedEndpoints.push(label);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export interface BuildFixturesParams {
|
|
61
|
+
endpoints: EndpointInfo[];
|
|
62
|
+
securitySchemes: SecuritySchemeInfo[];
|
|
63
|
+
baseUrl?: string;
|
|
64
|
+
specHash: string;
|
|
65
|
+
/**
|
|
66
|
+
* Resource map (CRUD groups + body-FK refs) — when provided, the manifest
|
|
67
|
+
* also lists body-FK and capture-chain variables that the test generator
|
|
68
|
+
* will reference. Keeps `.api-fixtures.yaml` in sync with what generated
|
|
69
|
+
* tests actually consume (per decision-7: manifest = source of truth for
|
|
70
|
+
* the *list* of variables).
|
|
71
|
+
*/
|
|
72
|
+
resourceMap?: ApiResourceMap;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function buildApiFixtureManifest(params: BuildFixturesParams): ApiFixtureManifest {
|
|
76
|
+
const fixtures = new Map<string, FixtureRequirement>();
|
|
77
|
+
|
|
78
|
+
// 1. Server URL → base_url
|
|
79
|
+
fixtures.set("base_url", {
|
|
80
|
+
name: "base_url",
|
|
81
|
+
source: "server",
|
|
82
|
+
description: params.baseUrl
|
|
83
|
+
? `Base URL of the API (from spec: ${params.baseUrl}).`
|
|
84
|
+
: `Base URL of the API. Spec did not declare a server — fill in manually.`,
|
|
85
|
+
affectedEndpoints: ["*"],
|
|
86
|
+
required: true,
|
|
87
|
+
defaultValue: params.baseUrl ?? "",
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
// 2. Auth schemes → auth tokens
|
|
91
|
+
// We map each scheme that endpoints actually reference into an env var.
|
|
92
|
+
const usedSchemeNames = new Set<string>();
|
|
93
|
+
for (const ep of params.endpoints) {
|
|
94
|
+
for (const s of ep.security) usedSchemeNames.add(s);
|
|
95
|
+
}
|
|
96
|
+
for (const scheme of params.securitySchemes) {
|
|
97
|
+
if (!usedSchemeNames.has(scheme.name)) continue;
|
|
98
|
+
const varName = schemeVarName(scheme, params.securitySchemes);
|
|
99
|
+
let description: string;
|
|
100
|
+
if (scheme.type === "http" && scheme.scheme === "bearer") {
|
|
101
|
+
description = `Bearer token for security scheme "${scheme.name}".`;
|
|
102
|
+
} else if (scheme.type === "apiKey") {
|
|
103
|
+
description = `API key for "${scheme.name}" (sent as ${scheme.in === "header" ? `header ${scheme.apiKeyName}` : `${scheme.in} param ${scheme.apiKeyName}`}).`;
|
|
104
|
+
} else if (scheme.type === "oauth2") {
|
|
105
|
+
description = `OAuth2 access token for scheme "${scheme.name}".`;
|
|
106
|
+
} else {
|
|
107
|
+
description = `Token for security scheme "${scheme.name}" (${scheme.type}).`;
|
|
108
|
+
}
|
|
109
|
+
const existing = fixtures.get(varName);
|
|
110
|
+
if (existing) {
|
|
111
|
+
// Multiple schemes might collapse to one var (single-bearer case).
|
|
112
|
+
// Keep the most informative description.
|
|
113
|
+
if (description.length > existing.description.length) existing.description = description;
|
|
114
|
+
} else {
|
|
115
|
+
fixtures.set(varName, {
|
|
116
|
+
name: varName,
|
|
117
|
+
source: "auth",
|
|
118
|
+
description,
|
|
119
|
+
affectedEndpoints: [],
|
|
120
|
+
required: true,
|
|
121
|
+
defaultValue: "",
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
const req = fixtures.get(varName)!;
|
|
125
|
+
for (const ep of params.endpoints) {
|
|
126
|
+
if (ep.security.includes(scheme.name)) pushAffected(req, ep);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// 3. Required path params → one var per unique name, disambiguated by
|
|
131
|
+
// owning resource when the raw name is genuinely reused across
|
|
132
|
+
// distinct collections (ARV-369). Without this, a spec where
|
|
133
|
+
// `{code}` appears under both `/macros/v30/{code}` and
|
|
134
|
+
// `/templates/v30/{code}` collapses to ONE `code` fixture — one
|
|
135
|
+
// `.env.yaml` value then silently misapplies to whichever resources
|
|
136
|
+
// don't own it, producing false-negative 404s in generated suites.
|
|
137
|
+
// Single-owner param names (the common case) keep their raw name,
|
|
138
|
+
// unchanged from pre-ARV-369 behavior.
|
|
139
|
+
const ambiguousPathParams = computeAmbiguousPathParams(params.endpoints);
|
|
140
|
+
for (const ep of params.endpoints) {
|
|
141
|
+
for (const p of ep.parameters) {
|
|
142
|
+
if (p.in !== "path") continue;
|
|
143
|
+
if (p.required === false) continue;
|
|
144
|
+
const varName = fixtureVarNameForPathParam(ep.path, p.name, ambiguousPathParams);
|
|
145
|
+
let req = fixtures.get(varName);
|
|
146
|
+
if (!req) {
|
|
147
|
+
const schema = p.schema as { type?: string; format?: string; example?: unknown } | undefined;
|
|
148
|
+
let defaultValue = "";
|
|
149
|
+
if (schema?.example !== undefined) defaultValue = String(schema.example);
|
|
150
|
+
else if (schema?.format === "uuid") defaultValue = "";
|
|
151
|
+
else if (schema?.type === "integer" || schema?.type === "number") defaultValue = "";
|
|
152
|
+
|
|
153
|
+
const scopeNote = ambiguousPathParams.has(p.name)
|
|
154
|
+
? ` Scoped to "${owningCollectionForPathParam(ep.path, p.name) ?? "?"}" — the raw name "${p.name}" is also used by other resources with unrelated ids.`
|
|
155
|
+
: "";
|
|
156
|
+
req = {
|
|
157
|
+
name: varName,
|
|
158
|
+
source: "path",
|
|
159
|
+
description: `Path parameter ${p.name}${schema?.format ? ` (${schema.format})` : schema?.type ? ` (${schema.type})` : ""}.${scopeNote} Set to a real id from your account, or leave blank to skip dependent tests.`,
|
|
160
|
+
affectedEndpoints: [],
|
|
161
|
+
required: true,
|
|
162
|
+
defaultValue,
|
|
163
|
+
};
|
|
164
|
+
fixtures.set(varName, req);
|
|
165
|
+
}
|
|
166
|
+
pushAffected(req, ep);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
// 4. Required header params → one var per unique name (skip Authorization
|
|
171
|
+
// & Content-Type — those are handled by auth + suite headers).
|
|
172
|
+
for (const ep of params.endpoints) {
|
|
173
|
+
for (const p of ep.parameters) {
|
|
174
|
+
if (p.in !== "header") continue;
|
|
175
|
+
if (p.required === false) continue;
|
|
176
|
+
const lname = p.name.toLowerCase();
|
|
177
|
+
if (lname === "authorization" || lname === "content-type" || lname === "accept") continue;
|
|
178
|
+
const varName = lname.replace(/-/g, "_");
|
|
179
|
+
let req = fixtures.get(varName);
|
|
180
|
+
if (!req) {
|
|
181
|
+
req = {
|
|
182
|
+
name: varName,
|
|
183
|
+
source: "header",
|
|
184
|
+
description: `Required header ${p.name}.`,
|
|
185
|
+
affectedEndpoints: [],
|
|
186
|
+
required: true,
|
|
187
|
+
defaultValue: "",
|
|
188
|
+
};
|
|
189
|
+
fixtures.set(varName, req);
|
|
190
|
+
}
|
|
191
|
+
pushAffected(req, ep);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 5. Body-FK fields — required parent-id fields in request bodies that
|
|
196
|
+
// the generator copies from `.env.yaml` (e.g. `audience_id` in
|
|
197
|
+
// POST /contacts). Without these in the manifest, prepare-fixtures
|
|
198
|
+
// discovers/seeds nothing for them and `zond audit` 422s on first
|
|
199
|
+
// nested resource. Walks ALL mutating endpoints (not only full
|
|
200
|
+
// CRUD groups) so POST-only resources still surface their FK deps.
|
|
201
|
+
// Source precedence: path > body-fk (path-params more constraining).
|
|
202
|
+
for (const ep of params.endpoints) {
|
|
203
|
+
const method = ep.method.toUpperCase();
|
|
204
|
+
if (method !== "POST" && method !== "PUT" && method !== "PATCH") continue;
|
|
205
|
+
const schema = ep.requestBodySchema as OpenAPIV3.SchemaObject | undefined;
|
|
206
|
+
if (!schema) continue;
|
|
207
|
+
// effectiveObjectShape merges allOf — .NET/Swagger bodies wrap models in
|
|
208
|
+
// allOf, so a direct schema.properties read misses every FK field.
|
|
209
|
+
const { properties, required } = effectiveObjectShape(schema);
|
|
210
|
+
if (Object.keys(properties).length === 0) continue;
|
|
211
|
+
for (const [fieldName, propSchema] of Object.entries(properties)) {
|
|
212
|
+
if (!required.has(fieldName)) continue;
|
|
213
|
+
// ARV-45: catch FK ids AND closed-vocab reference codes
|
|
214
|
+
// (`sequenceTypeCode`) the generator can't synthesise — kept in
|
|
215
|
+
// lockstep with `wireBodyFkRefs` in suite-generator so every var the
|
|
216
|
+
// tests reference appears here (decision-7 manifest contract).
|
|
217
|
+
if (!isFkFixtureField(fieldName, propSchema as OpenAPIV3.SchemaObject)) continue;
|
|
218
|
+
// ARV-138: canonicalise camelCase to snake_case so `issueId` shares
|
|
219
|
+
// a manifest slot with path-param `issue_id`. The raw `fieldName`
|
|
220
|
+
// still goes to the server unchanged via `create-body.ts` —
|
|
221
|
+
// canonicalisation only affects the manifest var-name namespace.
|
|
222
|
+
const varName = canonicalVarName(fieldName);
|
|
223
|
+
const existing = fixtures.get(varName);
|
|
224
|
+
if (existing) {
|
|
225
|
+
// Already covered (likely as path-param). Keep the existing entry
|
|
226
|
+
// and just surface the additional affected endpoint.
|
|
227
|
+
pushAffected(existing, ep);
|
|
228
|
+
continue;
|
|
229
|
+
}
|
|
230
|
+
fixtures.set(varName, {
|
|
231
|
+
name: varName,
|
|
232
|
+
source: "body-fk",
|
|
233
|
+
description: `Foreign-key / reference field "${fieldName}" consumed by ${epLabel(ep)} request body. Set to a real value from your account, or leave blank to skip dependent tests.`,
|
|
234
|
+
affectedEndpoints: [epLabel(ep)],
|
|
235
|
+
required: true,
|
|
236
|
+
defaultValue: "",
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// 6. CRUD-chain capture vars — the generator emits `capture: <idParam>`
|
|
242
|
+
// in POST steps and references {{<idParam>}} downstream. These are
|
|
243
|
+
// auto-captured at runtime; surfacing them in the manifest keeps the
|
|
244
|
+
// "var in tests but not in manifest" contract intact (per decision-7)
|
|
245
|
+
// and lets prepare-fixtures distinguish "captured automatically" from
|
|
246
|
+
// "user must fill". required: false — env override is advanced-only.
|
|
247
|
+
//
|
|
248
|
+
// ARV-137: capture name = `r.idParam` (the spec's path-param name), not
|
|
249
|
+
// `resourceVar(r.resource, "id")`. The synthesised form produced phantom
|
|
250
|
+
// manifest dupes whenever the spec's path-param didn't equal
|
|
251
|
+
// `<resource>_id` (e.g. monitors/monitor_id_or_slug, saved/query_id,
|
|
252
|
+
// releases/version). Now the manifest carries one var per resource id
|
|
253
|
+
// that matches both the path-source entry and the generated test refs.
|
|
254
|
+
if (params.resourceMap) {
|
|
255
|
+
for (const r of params.resourceMap.resources) {
|
|
256
|
+
if (!r.endpoints.create) continue;
|
|
257
|
+
const captureName = r.idParam || resourceVar(r.resource, "id");
|
|
258
|
+
if (fixtures.has(captureName)) continue;
|
|
259
|
+
const description = `Captured automatically from ${r.endpoints.create} response (field "${r.captureField}") and used in downstream CRUD steps. Set in .env.yaml only to override the captured value.`;
|
|
260
|
+
const affectedFromGroup = Object.entries(r.endpoints)
|
|
261
|
+
.filter(([k]) => k !== "list" && k !== "create")
|
|
262
|
+
.map(([, v]) => v as string);
|
|
263
|
+
const req: FixtureRequirement = {
|
|
264
|
+
name: captureName,
|
|
265
|
+
source: "capture-chain",
|
|
266
|
+
description,
|
|
267
|
+
affectedEndpoints: affectedFromGroup.slice(0, 10),
|
|
268
|
+
required: false,
|
|
269
|
+
defaultValue: "",
|
|
270
|
+
};
|
|
271
|
+
fixtures.set(captureName, req);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const ordered = Array.from(fixtures.values()).sort((a, b) => {
|
|
276
|
+
const sourceOrder: Record<FixtureSource, number> = {
|
|
277
|
+
server: 0,
|
|
278
|
+
auth: 1,
|
|
279
|
+
header: 2,
|
|
280
|
+
path: 3,
|
|
281
|
+
"body-fk": 4,
|
|
282
|
+
"capture-chain": 5,
|
|
283
|
+
};
|
|
284
|
+
if (sourceOrder[a.source] !== sourceOrder[b.source]) {
|
|
285
|
+
return sourceOrder[a.source] - sourceOrder[b.source];
|
|
286
|
+
}
|
|
287
|
+
return a.name.localeCompare(b.name);
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
return {
|
|
291
|
+
generatedAt: new Date().toISOString(),
|
|
292
|
+
specHash: params.specHash,
|
|
293
|
+
fixtureCount: ordered.length,
|
|
294
|
+
fixtures: ordered,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ── YAML serialization ──
|
|
299
|
+
|
|
300
|
+
function escape(s: string): string {
|
|
301
|
+
if (/[:#\[\]{}&*!|>'"@`,%]/.test(s) || s.includes("\n") || s === "") {
|
|
302
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
303
|
+
}
|
|
304
|
+
return s;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function serializeApiFixtureManifest(m: ApiFixtureManifest): string {
|
|
308
|
+
const lines: string[] = [];
|
|
309
|
+
lines.push("# Auto-generated by zond. Do not edit by hand.");
|
|
310
|
+
lines.push("# Read-only manifest of variables this API needs in .env.yaml.");
|
|
311
|
+
lines.push("# Regenerate via: zond refresh-api <name>");
|
|
312
|
+
lines.push(`generatedAt: ${escape(m.generatedAt)}`);
|
|
313
|
+
lines.push(`specHash: ${escape(m.specHash)}`);
|
|
314
|
+
lines.push(`fixtureCount: ${m.fixtureCount}`);
|
|
315
|
+
lines.push("fixtures:");
|
|
316
|
+
for (const f of m.fixtures) {
|
|
317
|
+
lines.push(` - name: ${escape(f.name)}`);
|
|
318
|
+
lines.push(` source: ${f.source}`);
|
|
319
|
+
lines.push(` required: ${f.required}`);
|
|
320
|
+
lines.push(` description: ${escape(f.description)}`);
|
|
321
|
+
if (f.defaultValue !== undefined) {
|
|
322
|
+
lines.push(` defaultValue: ${escape(f.defaultValue)}`);
|
|
323
|
+
}
|
|
324
|
+
if (f.affectedEndpoints.length === 0) {
|
|
325
|
+
lines.push(` affectedEndpoints: []`);
|
|
326
|
+
} else {
|
|
327
|
+
lines.push(` affectedEndpoints:`);
|
|
328
|
+
for (const e of f.affectedEndpoints) lines.push(` - ${escape(e)}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
return lines.join("\n") + "\n";
|
|
332
|
+
}
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
export { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "./openapi-reader.ts";
|
|
2
|
-
export { serializeSuite
|
|
2
|
+
export { serializeSuite } from "./serializer.ts";
|
|
3
3
|
export type { RawSuite, RawStep } from "./serializer.ts";
|
|
4
|
-
export { generateFromSchema } from "./data-factory.ts";
|
|
5
4
|
export { scanCoveredEndpoints, filterUncoveredEndpoints, normalizePath, specPathToRegex } from "./coverage-scanner.ts";
|
|
6
5
|
export type { CoveredEndpoint } from "./coverage-scanner.ts";
|
|
7
6
|
export { analyzeEndpoints } from "./endpoint-warnings.ts";
|
|
8
|
-
export { compressEndpointsWithSchemas, buildGenerationGuide } from "./guide-builder.ts";
|
|
9
|
-
export type { GuideOptions } from "./guide-builder.ts";
|
|
10
7
|
export type { EndpointWarning, WarningCode } from "./endpoint-warnings.ts";
|
|
11
8
|
export type { EndpointInfo, ResponseInfo, GenerateOptions, SecuritySchemeInfo, CrudGroup } from "./types.ts";
|
|
12
|
-
export { generateSuites, generateStep, detectCrudGroups, generateCrudSuite, generateSanitySuite, findUnresolvedVars } from "./suite-generator.ts";
|
|
13
9
|
export { buildCatalog, serializeCatalog } from "./catalog-builder.ts";
|
|
14
10
|
export type { ApiCatalog, CatalogEndpoint } from "./catalog-builder.ts";
|
|
11
|
+
export { buildApiResourceMap, serializeApiResourceMap } from "./resources-builder.ts";
|
|
12
|
+
export type { ApiResourceMap, ApiResourceEntry, ResourceFkRef } from "./resources-builder.ts";
|
|
13
|
+
export { buildApiFixtureManifest, serializeApiFixtureManifest } from "./fixtures-builder.ts";
|
|
14
|
+
export type { ApiFixtureManifest, FixtureRequirement, FixtureSource } from "./fixtures-builder.ts";
|
|
@@ -1,15 +1,54 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { rootCertificates } from "node:tls";
|
|
1
3
|
import { dereference } from "@readme/openapi-parser";
|
|
2
4
|
import type { OpenAPIV3 } from "openapi-types";
|
|
3
5
|
import type { EndpointInfo, ResponseInfo, SecuritySchemeInfo } from "./types.ts";
|
|
6
|
+
import { disambiguateGenericPathParams } from "./path-param-disambig.ts";
|
|
4
7
|
|
|
5
8
|
const HTTP_METHODS = ["get", "post", "put", "patch", "delete"] as const;
|
|
6
9
|
|
|
7
|
-
export
|
|
10
|
+
export interface SpecFetchTlsOptions {
|
|
11
|
+
/** Disable TLS verification entirely (bun `--insecure`). Last resort. */
|
|
12
|
+
insecure?: boolean;
|
|
13
|
+
/** Path to a PEM CA bundle to trust in addition to the public roots.
|
|
14
|
+
* Falls back to the `NODE_EXTRA_CA_CERTS` env var. */
|
|
15
|
+
caPath?: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** MF1 (ARV-367): resolve the bun `fetch` `tls` option for a spec fetch so a
|
|
19
|
+
* self-signed / internal corporate CA validates *without* disabling TLS.
|
|
20
|
+
*
|
|
21
|
+
* Precedence: `insecure` (verification off) > explicit `caPath` /
|
|
22
|
+
* `NODE_EXTRA_CA_CERTS` (extra CA APPENDED to the public roots — never
|
|
23
|
+
* replacing them, so public specs keep validating) > default (undefined).
|
|
24
|
+
* Returns undefined when nothing special is needed. Throws if a CA path is
|
|
25
|
+
* set but unreadable — a misconfigured CA should surface, not fall through
|
|
26
|
+
* to a confusing "self signed certificate" error. */
|
|
27
|
+
export function resolveSpecFetchTls(
|
|
28
|
+
options?: SpecFetchTlsOptions,
|
|
29
|
+
): { rejectUnauthorized: false } | { ca: string[] } | undefined {
|
|
30
|
+
if (options?.insecure) return { rejectUnauthorized: false };
|
|
31
|
+
const caPath = options?.caPath ?? process.env.NODE_EXTRA_CA_CERTS;
|
|
32
|
+
if (caPath) {
|
|
33
|
+
let extra: string;
|
|
34
|
+
try {
|
|
35
|
+
extra = readFileSync(caPath, "utf8");
|
|
36
|
+
} catch (e) {
|
|
37
|
+
throw new Error(
|
|
38
|
+
`CA bundle not readable: ${caPath} (${(e as Error).message}). ` +
|
|
39
|
+
`Set --ca / NODE_EXTRA_CA_CERTS to a valid PEM file, or use --insecure.`,
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
if (extra.trim()) return { ca: [extra, ...rootCertificates] };
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function readOpenApiSpec(specPath: string, options?: SpecFetchTlsOptions): Promise<OpenAPIV3.Document> {
|
|
8
48
|
// For HTTP URLs, fetch the spec first then dereference the parsed object
|
|
9
49
|
if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
|
|
10
|
-
const
|
|
11
|
-
|
|
12
|
-
});
|
|
50
|
+
const tls = resolveSpecFetchTls(options);
|
|
51
|
+
const resp = await fetch(specPath, { ...(tls ? { tls } : {}) });
|
|
13
52
|
if (!resp.ok) throw new Error(`Failed to fetch spec: ${resp.status} ${resp.statusText}`);
|
|
14
53
|
const spec = await resp.json();
|
|
15
54
|
const api = await dereference(spec as string);
|
|
@@ -19,6 +58,29 @@ export async function readOpenApiSpec(specPath: string, options?: { insecure?: b
|
|
|
19
58
|
return api as OpenAPIV3.Document;
|
|
20
59
|
}
|
|
21
60
|
|
|
61
|
+
/** ARV-376: align declared path-param names with the path template when they
|
|
62
|
+
* diverge (spec quirk: path `/byid/{id}` but param declared `byid_id`). The
|
|
63
|
+
* template is authoritative for substitution, so unmatched params are renamed
|
|
64
|
+
* to the unmatched template segment names, in positional order. No-op when
|
|
65
|
+
* names already agree (the common, well-formed case). Mutates in place. */
|
|
66
|
+
export function reconcilePathParamNames(
|
|
67
|
+
path: string,
|
|
68
|
+
parameters: OpenAPIV3.ParameterObject[],
|
|
69
|
+
): void {
|
|
70
|
+
const tplNames = [...path.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]!);
|
|
71
|
+
if (tplNames.length === 0) return;
|
|
72
|
+
const pathParams = parameters.filter((p) => p.in === "path");
|
|
73
|
+
const declared = new Set(pathParams.map((p) => p.name));
|
|
74
|
+
const unmatchedTpl = tplNames.filter((n) => !declared.has(n));
|
|
75
|
+
const unmatchedParams = pathParams.filter((p) => !tplNames.includes(p.name));
|
|
76
|
+
// Only reconcile a clean 1:1 correspondence — if the counts differ we can't
|
|
77
|
+
// safely guess the mapping, so leave the (already odd) spec untouched.
|
|
78
|
+
if (unmatchedTpl.length === 0 || unmatchedTpl.length !== unmatchedParams.length) return;
|
|
79
|
+
unmatchedParams.forEach((p, i) => {
|
|
80
|
+
(p as { name: string }).name = unmatchedTpl[i]!;
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
22
84
|
export function extractSecuritySchemes(doc: OpenAPIV3.Document): SecuritySchemeInfo[] {
|
|
23
85
|
const schemes: SecuritySchemeInfo[] = [];
|
|
24
86
|
const securitySchemes = doc.components?.securitySchemes;
|
|
@@ -57,9 +119,16 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
57
119
|
|
|
58
120
|
const parameters: OpenAPIV3.ParameterObject[] = [];
|
|
59
121
|
|
|
122
|
+
// Skip circular-ref sentinel stubs emitted by decycleSchema —
|
|
123
|
+
// they look like `{ "x-circular": true }` (no .name, no .in) and
|
|
124
|
+
// crash downstream code that expects p.name/p.in. ARV-200 (R10/F1).
|
|
125
|
+
const isUsableParam = (p: any): p is OpenAPIV3.ParameterObject =>
|
|
126
|
+
p != null && typeof p === "object" && typeof p.name === "string" && typeof p.in === "string";
|
|
127
|
+
|
|
60
128
|
// Path-level parameters
|
|
61
129
|
if (pathItem.parameters) {
|
|
62
130
|
for (const p of pathItem.parameters) {
|
|
131
|
+
if (!isUsableParam(p)) continue;
|
|
63
132
|
parameters.push(p as OpenAPIV3.ParameterObject);
|
|
64
133
|
}
|
|
65
134
|
}
|
|
@@ -67,6 +136,7 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
67
136
|
// Operation-level parameters (override path-level)
|
|
68
137
|
if (operation.parameters) {
|
|
69
138
|
for (const p of operation.parameters) {
|
|
139
|
+
if (!isUsableParam(p)) continue;
|
|
70
140
|
const param = p as OpenAPIV3.ParameterObject;
|
|
71
141
|
const existingIdx = parameters.findIndex(
|
|
72
142
|
(existing) => existing.name === param.name && existing.in === param.in,
|
|
@@ -79,6 +149,15 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
79
149
|
}
|
|
80
150
|
}
|
|
81
151
|
|
|
152
|
+
// ARV-376: reconcile a malformed spec where a path *template* segment
|
|
153
|
+
// (`/byid/{id}`) disagrees with the declared path *parameter* name
|
|
154
|
+
// (`byid_id`) — a docgen-style quirk. The path template is what the
|
|
155
|
+
// runner substitutes, so it wins: rename the diverging param(s) to the
|
|
156
|
+
// template segment name(s), positionally. Without this the fixture var
|
|
157
|
+
// (from the param) never matches the `{...}` in the path, and every
|
|
158
|
+
// downstream layer (disambig, manifest, resource-graph) diverges.
|
|
159
|
+
reconcilePathParamNames(path, parameters);
|
|
160
|
+
|
|
82
161
|
// Request body schema + content type
|
|
83
162
|
let requestBodySchema: OpenAPIV3.SchemaObject | undefined;
|
|
84
163
|
let requestBodyContentType: string | undefined;
|
|
@@ -120,9 +199,12 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
120
199
|
const responseContentTypesSet = new Set<string>();
|
|
121
200
|
if (operation.responses) {
|
|
122
201
|
for (const [statusCode, responseObj] of Object.entries(operation.responses)) {
|
|
202
|
+
const parsedStatus = parseInt(statusCode, 10);
|
|
203
|
+
// Skip non-numeric keys like "default" — they have no asserting status code.
|
|
204
|
+
if (!Number.isFinite(parsedStatus)) continue;
|
|
123
205
|
const resp = responseObj as OpenAPIV3.ResponseObject;
|
|
124
206
|
const info: ResponseInfo = {
|
|
125
|
-
statusCode:
|
|
207
|
+
statusCode: parsedStatus,
|
|
126
208
|
description: resp.description || "",
|
|
127
209
|
};
|
|
128
210
|
if (resp.content) {
|
|
@@ -147,6 +229,12 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
147
229
|
responses.some(r => r.statusCode === 412) ||
|
|
148
230
|
parameters.some(p => p.name.toLowerCase() === "if-match" && p.in === "header");
|
|
149
231
|
|
|
232
|
+
// ARV-189: harvest `x-*` vendor extensions. Operation-level wins on
|
|
233
|
+
// key collision over path-level so spec authors can default at the
|
|
234
|
+
// path and override per-operation. Empty result = undefined (avoids
|
|
235
|
+
// a churn-y `extensions: {}` field in serialised endpoint snapshots).
|
|
236
|
+
const extensions = collectExtensions(pathItem, operation);
|
|
237
|
+
|
|
150
238
|
endpoints.push({
|
|
151
239
|
path,
|
|
152
240
|
method: method.toUpperCase(),
|
|
@@ -159,11 +247,51 @@ export function extractEndpoints(doc: OpenAPIV3.Document): EndpointInfo[] {
|
|
|
159
247
|
responseContentTypes: [...responseContentTypesSet],
|
|
160
248
|
responses,
|
|
161
249
|
security,
|
|
162
|
-
deprecated: operation.deprecated ?? false,
|
|
250
|
+
deprecated: (operation.deprecated ?? false) || isMarkedDeprecatedInText(operation.summary, operation.description, operation.operationId),
|
|
163
251
|
requiresEtag,
|
|
252
|
+
...(extensions ? { extensions } : {}),
|
|
164
253
|
});
|
|
165
254
|
}
|
|
166
255
|
}
|
|
167
256
|
|
|
168
|
-
|
|
257
|
+
// ARV-40: when generic path-param names (`{id}`, `{slug}`, ...) collide
|
|
258
|
+
// across multiple resources, rewrite each to `<parent_singular>_<param>`
|
|
259
|
+
// so the manifest derives per-resource vars and tests stop sharing one
|
|
260
|
+
// global `id`. In-memory only; on-disk spec stays untouched.
|
|
261
|
+
return disambiguateGenericPathParams(endpoints);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/** Collect `x-*` vendor extensions from a path item and operation,
|
|
265
|
+
* with operation values winning on key collision. Returns undefined
|
|
266
|
+
* when neither has any so callers can omit the field cleanly. */
|
|
267
|
+
function collectExtensions(
|
|
268
|
+
pathItem: OpenAPIV3.PathItemObject,
|
|
269
|
+
operation: OpenAPIV3.OperationObject,
|
|
270
|
+
): Record<string, unknown> | undefined {
|
|
271
|
+
const out: Record<string, unknown> = {};
|
|
272
|
+
for (const [k, v] of Object.entries(pathItem)) {
|
|
273
|
+
if (k.startsWith("x-")) out[k] = v;
|
|
274
|
+
}
|
|
275
|
+
for (const [k, v] of Object.entries(operation)) {
|
|
276
|
+
if (k.startsWith("x-")) out[k] = v;
|
|
277
|
+
}
|
|
278
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
/** Spec authors often mark endpoints as deprecated in the summary or
|
|
282
|
+
* description text instead of (or in addition to) the `deprecated: true`
|
|
283
|
+
* flag — common across many SaaS and legacy specs. Without this
|
|
284
|
+
* fallback, generator emits CRUD suites whose POST returns 404 from a dead
|
|
285
|
+
* endpoint. (TASK-245) */
|
|
286
|
+
/** Matches `(DEPRECATED) ...`, `[DEPRECATED] ...`, `DEPRECATED: ...` at the
|
|
287
|
+
* start of a string. Also matches markdown `## Deprecated` headings, which
|
|
288
|
+
* some spec authors use in operation `description` to flag end-of-life
|
|
289
|
+
* endpoints. */
|
|
290
|
+
const DEPRECATED_PREFIX_RE = /^\s*[\(\[]?\s*DEPRECATED\s*[\)\]:\-—\s]/i;
|
|
291
|
+
const DEPRECATED_HEADING_RE = /^\s*#+\s*Deprecated\b/im;
|
|
292
|
+
function isMarkedDeprecatedInText(summary?: string, description?: string, operationId?: string): boolean {
|
|
293
|
+
if (summary && DEPRECATED_PREFIX_RE.test(summary)) return true;
|
|
294
|
+
if (operationId && DEPRECATED_PREFIX_RE.test(operationId)) return true;
|
|
295
|
+
if (description && (DEPRECATED_PREFIX_RE.test(description) || DEPRECATED_HEADING_RE.test(description))) return true;
|
|
296
|
+
return false;
|
|
169
297
|
}
|