@kirrosh/zond 0.26.0 → 0.27.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 +59 -0
- package/README.md +22 -21
- package/bin/zond.mjs +23 -0
- package/package.json +13 -16
- package/scripts/npm/postinstall.mjs +76 -0
- package/src/CLAUDE.md +0 -112
- package/src/bun-types.d.ts +0 -5
- package/src/cli/argv.ts +0 -122
- package/src/cli/commands/add-api.ts +0 -146
- package/src/cli/commands/api/annotate/idempotency.ts +0 -59
- package/src/cli/commands/api/annotate/index.ts +0 -880
- package/src/cli/commands/api/annotate/lifecycle.ts +0 -74
- package/src/cli/commands/api/annotate/overlay.ts +0 -206
- package/src/cli/commands/api/annotate/pagination.ts +0 -64
- package/src/cli/commands/api/annotate/prompts.ts +0 -220
- package/src/cli/commands/api/annotate/readback.ts +0 -58
- package/src/cli/commands/api/annotate/resources.ts +0 -91
- package/src/cli/commands/api/annotate/seed-bodies.ts +0 -61
- package/src/cli/commands/audit.ts +0 -786
- package/src/cli/commands/catalog.ts +0 -97
- package/src/cli/commands/check.ts +0 -361
- package/src/cli/commands/checks.ts +0 -1072
- package/src/cli/commands/ci-init.ts +0 -223
- package/src/cli/commands/clean.ts +0 -212
- package/src/cli/commands/cleanup.ts +0 -236
- package/src/cli/commands/completions.ts +0 -192
- package/src/cli/commands/coverage.ts +0 -872
- package/src/cli/commands/db.ts +0 -611
- package/src/cli/commands/describe.ts +0 -120
- package/src/cli/commands/discover.ts +0 -1356
- package/src/cli/commands/doctor.ts +0 -661
- package/src/cli/commands/fixtures.ts +0 -402
- package/src/cli/commands/generate.ts +0 -577
- package/src/cli/commands/init/agents-md.ts +0 -61
- package/src/cli/commands/init/bootstrap.ts +0 -111
- package/src/cli/commands/init/index.ts +0 -244
- package/src/cli/commands/init/skills.ts +0 -141
- package/src/cli/commands/init/templates/agents.md +0 -86
- package/src/cli/commands/init/templates/markdown.d.ts +0 -4
- package/src/cli/commands/init/templates/skills/warm-up-target.md +0 -122
- package/src/cli/commands/init/templates/skills/zond-checks.md +0 -621
- package/src/cli/commands/init/templates/skills/zond-seed.md +0 -114
- package/src/cli/commands/init/templates/skills/zond-triage.md +0 -272
- package/src/cli/commands/init/templates/skills/zond.md +0 -861
- package/src/cli/commands/init/templates/zond-config.yml +0 -14
- package/src/cli/commands/prepare-fixtures.ts +0 -97
- package/src/cli/commands/probe/_seed-bodies.ts +0 -52
- package/src/cli/commands/probe/mass-assignment.ts +0 -594
- package/src/cli/commands/probe/security.ts +0 -537
- package/src/cli/commands/probe/static.ts +0 -255
- package/src/cli/commands/probe/webhooks.ts +0 -163
- package/src/cli/commands/probe.ts +0 -535
- package/src/cli/commands/reference.ts +0 -87
- package/src/cli/commands/refresh-api.ts +0 -227
- package/src/cli/commands/remove-api.ts +0 -150
- package/src/cli/commands/report-bundle.ts +0 -310
- package/src/cli/commands/report.ts +0 -241
- package/src/cli/commands/request.ts +0 -548
- package/src/cli/commands/run.ts +0 -1142
- package/src/cli/commands/schema-from-runs.ts +0 -128
- package/src/cli/commands/secrets.ts +0 -133
- package/src/cli/commands/session.ts +0 -244
- package/src/cli/commands/use.ts +0 -74
- package/src/cli/index.ts +0 -55
- package/src/cli/json-envelope.ts +0 -108
- package/src/cli/json-schemas.ts +0 -314
- package/src/cli/output.ts +0 -40
- package/src/cli/program.ts +0 -219
- package/src/cli/resolve.ts +0 -105
- package/src/cli/runtime.ts +0 -7
- package/src/cli/safe-live.ts +0 -24
- package/src/cli/status-filter.ts +0 -114
- package/src/cli/util/api-context.ts +0 -85
- package/src/cli/version.ts +0 -8
- package/src/core/audit/persist.ts +0 -183
- package/src/core/checks/budget.ts +0 -59
- package/src/core/checks/checks/_crud-helpers.ts +0 -133
- package/src/core/checks/checks/_negative_mutator.ts +0 -133
- package/src/core/checks/checks/_readback-helpers.ts +0 -133
- package/src/core/checks/checks/content_type_conformance.ts +0 -39
- package/src/core/checks/checks/cross_call_references.ts +0 -147
- package/src/core/checks/checks/cursor_boundary_fuzzing.ts +0 -219
- package/src/core/checks/checks/ensure_resource_availability.ts +0 -62
- package/src/core/checks/checks/idempotency_replay.ts +0 -242
- package/src/core/checks/checks/ignored_auth.ts +0 -254
- package/src/core/checks/checks/index.ts +0 -68
- package/src/core/checks/checks/lifecycle_transitions.ts +0 -416
- package/src/core/checks/checks/missing_required_header.ts +0 -40
- package/src/core/checks/checks/negative_data_rejection.ts +0 -148
- package/src/core/checks/checks/not_a_server_error.ts +0 -35
- package/src/core/checks/checks/open_cors_on_sensitive.ts +0 -160
- package/src/core/checks/checks/pagination_invariants.ts +0 -419
- package/src/core/checks/checks/positive_data_acceptance.ts +0 -33
- package/src/core/checks/checks/rate_limit_headers_absent.ts +0 -77
- package/src/core/checks/checks/response_headers_conformance.ts +0 -74
- package/src/core/checks/checks/response_schema_conformance.ts +0 -30
- package/src/core/checks/checks/status_code_conformance.ts +0 -132
- package/src/core/checks/checks/unsupported_method.ts +0 -63
- package/src/core/checks/checks/use_after_free.ts +0 -78
- package/src/core/checks/index.ts +0 -30
- package/src/core/checks/mode.ts +0 -82
- package/src/core/checks/recommended-action.ts +0 -68
- package/src/core/checks/registry.ts +0 -78
- package/src/core/checks/runner.ts +0 -1461
- package/src/core/checks/sarif.ts +0 -230
- package/src/core/checks/spec-findings.ts +0 -308
- package/src/core/checks/stateful.ts +0 -121
- package/src/core/checks/types.ts +0 -305
- package/src/core/checks/zond-extensions.ts +0 -73
- package/src/core/classifier/recommended-action.ts +0 -251
- package/src/core/context/current.ts +0 -51
- package/src/core/context/session.ts +0 -78
- package/src/core/coverage/loader.ts +0 -216
- package/src/core/coverage/reasons.ts +0 -300
- package/src/core/diagnostics/db-analysis.ts +0 -666
- package/src/core/diagnostics/failure-class.ts +0 -140
- package/src/core/diagnostics/failure-hints.ts +0 -112
- package/src/core/diagnostics/spec-pointer.ts +0 -99
- package/src/core/diagnostics/suggested-fixes.ts +0 -155
- package/src/core/exporter/case-study/index.ts +0 -270
- package/src/core/exporter/curl.ts +0 -40
- package/src/core/exporter/exporter.ts +0 -48
- package/src/core/exporter/html-report/escape.ts +0 -24
- package/src/core/exporter/html-report/index.ts +0 -479
- package/src/core/exporter/html-report/script.ts +0 -100
- package/src/core/exporter/html-report/styles.ts +0 -408
- package/src/core/generator/catalog-builder.ts +0 -179
- package/src/core/generator/chunker.ts +0 -78
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/coverage-scanner.ts +0 -87
- package/src/core/generator/data-factory.ts +0 -759
- package/src/core/generator/describe.ts +0 -302
- package/src/core/generator/endpoint-warnings.ts +0 -52
- package/src/core/generator/fixtures-builder.ts +0 -332
- package/src/core/generator/index.ts +0 -14
- package/src/core/generator/openapi-reader.ts +0 -297
- package/src/core/generator/path-param-disambig.ts +0 -140
- package/src/core/generator/resources-builder.ts +0 -898
- package/src/core/generator/schema-utils.ts +0 -112
- package/src/core/generator/serializer.ts +0 -343
- package/src/core/generator/suite-generator.ts +0 -1301
- package/src/core/generator/types.ts +0 -63
- package/src/core/identity/identity-file.ts +0 -0
- package/src/core/lint/affects.ts +0 -28
- package/src/core/lint/config.ts +0 -96
- package/src/core/lint/format.ts +0 -42
- package/src/core/lint/index.ts +0 -94
- package/src/core/lint/reporter.ts +0 -128
- package/src/core/lint/rules/consistency.ts +0 -158
- package/src/core/lint/rules/heuristics.ts +0 -97
- package/src/core/lint/rules/strictness.ts +0 -109
- package/src/core/lint/types.ts +0 -96
- package/src/core/lint/walker.ts +0 -248
- package/src/core/meta/meta-store.ts +0 -11
- package/src/core/output/README.md +0 -73
- package/src/core/output/index.ts +0 -13
- package/src/core/output/run.ts +0 -91
- package/src/core/output/types.ts +0 -122
- package/src/core/parser/dynamic-values.ts +0 -160
- package/src/core/parser/env-interpolation.ts +0 -104
- package/src/core/parser/filter.ts +0 -97
- package/src/core/parser/schema.ts +0 -359
- package/src/core/parser/types.ts +0 -117
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +0 -181
- package/src/core/probe/bootstrap.ts +0 -34
- package/src/core/probe/dry-run-envelope.ts +0 -61
- package/src/core/probe/mass-assignment/classify.ts +0 -175
- package/src/core/probe/mass-assignment/cleanup.ts +0 -52
- package/src/core/probe/mass-assignment/digest.ts +0 -114
- package/src/core/probe/mass-assignment/orchestrator.ts +0 -459
- package/src/core/probe/mass-assignment/regression.ts +0 -141
- package/src/core/probe/mass-assignment/suspects.ts +0 -92
- package/src/core/probe/mass-assignment/types.ts +0 -135
- package/src/core/probe/mass-assignment-probe-class.ts +0 -198
- package/src/core/probe/mass-assignment-probe.ts +0 -27
- package/src/core/probe/mass-assignment-template.ts +0 -240
- package/src/core/probe/method-probe.ts +0 -164
- package/src/core/probe/method-shared.ts +0 -69
- package/src/core/probe/negative-probe.ts +0 -691
- package/src/core/probe/orphan-tracker.ts +0 -188
- package/src/core/probe/path-discovery.ts +0 -439
- package/src/core/probe/probe-harness.ts +0 -119
- package/src/core/probe/registry.ts +0 -89
- package/src/core/probe/runner.ts +0 -136
- package/src/core/probe/security/baseline.ts +0 -174
- package/src/core/probe/security/classify.ts +0 -341
- package/src/core/probe/security/cleanup.ts +0 -125
- package/src/core/probe/security/detectors.ts +0 -71
- package/src/core/probe/security/digest.ts +0 -104
- package/src/core/probe/security/orchestrator.ts +0 -398
- package/src/core/probe/security/regression.ts +0 -103
- package/src/core/probe/security/types.ts +0 -151
- package/src/core/probe/security-probe-class.ts +0 -207
- package/src/core/probe/security-probe.ts +0 -32
- package/src/core/probe/shared.ts +0 -531
- package/src/core/probe/static-probe-class.ts +0 -125
- package/src/core/probe/types.ts +0 -165
- package/src/core/probe/verdict-aggregator.ts +0 -33
- package/src/core/probe/webhooks-probe.ts +0 -282
- package/src/core/reporter/console.ts +0 -240
- package/src/core/reporter/index.ts +0 -22
- package/src/core/reporter/json.ts +0 -22
- package/src/core/reporter/junit.ts +0 -93
- package/src/core/reporter/ndjson.ts +0 -37
- package/src/core/reporter/types.ts +0 -15
- package/src/core/runner/assertions.ts +0 -383
- package/src/core/runner/async-pool.ts +0 -108
- package/src/core/runner/auth-path.ts +0 -8
- package/src/core/runner/ci-context.ts +0 -72
- package/src/core/runner/executor.ts +0 -652
- package/src/core/runner/expr-eval.ts +0 -41
- package/src/core/runner/form-encode.ts +0 -41
- package/src/core/runner/http-client.ts +0 -224
- package/src/core/runner/learn-drift.ts +0 -293
- package/src/core/runner/preflight-vars.ts +0 -153
- package/src/core/runner/progress-tracker.ts +0 -73
- package/src/core/runner/rate-limiter.ts +0 -185
- package/src/core/runner/run-kind.ts +0 -45
- package/src/core/runner/schema-validator.ts +0 -308
- package/src/core/runner/send-request.ts +0 -232
- package/src/core/runner/transforms.ts +0 -65
- package/src/core/runner/types.ts +0 -94
- package/src/core/secrets/registry.ts +0 -164
- package/src/core/secrets/secrets-file.ts +0 -115
- package/src/core/selectors/operation-filter.ts +0 -144
- package/src/core/setup-api.ts +0 -590
- package/src/core/severity/category.ts +0 -94
- package/src/core/severity/index.ts +0 -58
- package/src/core/spec/infer-schema.ts +0 -102
- package/src/core/spec/layers.ts +0 -154
- package/src/core/spec/merge-specs.ts +0 -156
- package/src/core/spec/schema-from-runs.ts +0 -117
- package/src/core/spec/schema-overlay.ts +0 -130
- package/src/core/util/ajv.ts +0 -13
- package/src/core/util/format-eta.ts +0 -21
- package/src/core/util/headers.ts +0 -9
- package/src/core/util/url.ts +0 -24
- package/src/core/utils.ts +0 -13
- package/src/core/workspace/config.ts +0 -129
- package/src/core/workspace/fixture-gap-report.ts +0 -84
- package/src/core/workspace/fixture-gaps.ts +0 -71
- package/src/core/workspace/manifest.ts +0 -283
- package/src/core/workspace/output-rotation.ts +0 -62
- package/src/core/workspace/root.ts +0 -96
- package/src/core/workspace/triage-path.ts +0 -87
- package/src/db/lint-runs.ts +0 -47
- package/src/db/migrate.ts +0 -128
- package/src/db/migrations/0001_run_kind.sql +0 -25
- package/src/db/migrations/0002_run_kind_request.sql +0 -59
- package/src/db/migrations/sql.d.ts +0 -4
- package/src/db/queries/collections.ts +0 -133
- package/src/db/queries/coverage.ts +0 -9
- package/src/db/queries/dashboard.ts +0 -59
- package/src/db/queries/results.ts +0 -216
- package/src/db/queries/runs.ts +0 -289
- package/src/db/queries/sessions.ts +0 -42
- package/src/db/queries/settings.ts +0 -28
- package/src/db/queries/types.ts +0 -172
- package/src/db/queries.ts +0 -75
- package/src/db/schema.ts +0 -298
|
@@ -1,302 +0,0 @@
|
|
|
1
|
-
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
-
import { readOpenApiSpec } from "./openapi-reader.ts";
|
|
3
|
-
import { decycleSchema } from "./schema-utils.ts";
|
|
4
|
-
|
|
5
|
-
export interface DescribeEndpointResult {
|
|
6
|
-
method: string;
|
|
7
|
-
path: string;
|
|
8
|
-
operationId?: string;
|
|
9
|
-
summary?: string;
|
|
10
|
-
description?: string;
|
|
11
|
-
tags?: string[];
|
|
12
|
-
deprecated: boolean;
|
|
13
|
-
security: string[];
|
|
14
|
-
parameters: Record<string, object[]>;
|
|
15
|
-
requestBody?: object;
|
|
16
|
-
responses: Record<string, object>;
|
|
17
|
-
testSnippet: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface CompactEndpoint {
|
|
21
|
-
method: string;
|
|
22
|
-
path: string;
|
|
23
|
-
operationId?: string;
|
|
24
|
-
summary?: string;
|
|
25
|
-
deprecated: boolean;
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function generateTestSnippet(params: {
|
|
29
|
-
method: string;
|
|
30
|
-
path: string;
|
|
31
|
-
operationId?: string;
|
|
32
|
-
pathParams: string[];
|
|
33
|
-
queryParams: Array<{ name: string; required?: boolean }>;
|
|
34
|
-
requestBody?: { required?: boolean; schema?: OpenAPIV3.SchemaObject };
|
|
35
|
-
hasSecurity: boolean;
|
|
36
|
-
successStatus: string;
|
|
37
|
-
}): string {
|
|
38
|
-
const { method, path, operationId, queryParams, requestBody, hasSecurity, successStatus } = params;
|
|
39
|
-
|
|
40
|
-
const urlPath = path.replace(/\{([^}]+)\}/g, (_, name) => `{{${name}}}`);
|
|
41
|
-
const url = `{{base_url}}${urlPath}`;
|
|
42
|
-
|
|
43
|
-
const lines: string[] = [];
|
|
44
|
-
const testName = operationId ?? `${method} ${path}`;
|
|
45
|
-
lines.push(`- name: "${testName}"`);
|
|
46
|
-
lines.push(` ${method}: "${url}"`);
|
|
47
|
-
|
|
48
|
-
if (hasSecurity) {
|
|
49
|
-
lines.push(` headers:`);
|
|
50
|
-
lines.push(` Authorization: "Bearer {{auth_token}}"`);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const requiredQuery = queryParams.filter(p => p.required);
|
|
54
|
-
if (requiredQuery.length > 0) {
|
|
55
|
-
lines.push(` query:`);
|
|
56
|
-
for (const p of requiredQuery) {
|
|
57
|
-
lines.push(` ${p.name}: "{{${p.name}}}"`);
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
if (requestBody && ["POST", "PUT", "PATCH"].includes(method)) {
|
|
62
|
-
const schema = requestBody.schema as OpenAPIV3.SchemaObject | undefined;
|
|
63
|
-
const required = Array.isArray(schema?.required) ? schema.required : [];
|
|
64
|
-
const properties = schema?.properties as Record<string, OpenAPIV3.SchemaObject> | undefined;
|
|
65
|
-
if (properties && Object.keys(properties).length > 0) {
|
|
66
|
-
lines.push(` json:`);
|
|
67
|
-
for (const [propName, propSchema] of Object.entries(properties)) {
|
|
68
|
-
if (!required.includes(propName)) continue;
|
|
69
|
-
const type = (propSchema as OpenAPIV3.SchemaObject).type ?? "string";
|
|
70
|
-
const placeholder = type === "integer" || type === "number" ? 0 : type === "boolean" ? false : `"{{${propName}}}"`;
|
|
71
|
-
lines.push(` ${propName}: ${placeholder}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
lines.push(` expect:`);
|
|
77
|
-
lines.push(` status: ${successStatus}`);
|
|
78
|
-
|
|
79
|
-
return lines.join("\n");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
export async function describeEndpoint(
|
|
83
|
-
specPath: string,
|
|
84
|
-
method: string,
|
|
85
|
-
endpointPath: string,
|
|
86
|
-
options?: { insecure?: boolean },
|
|
87
|
-
): Promise<DescribeEndpointResult> {
|
|
88
|
-
const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
|
|
89
|
-
|
|
90
|
-
const methodLower = method.toLowerCase() as OpenAPIV3.HttpMethods;
|
|
91
|
-
const normalizedPath = endpointPath.replace(/\/+$/, "") || "/";
|
|
92
|
-
|
|
93
|
-
let operation: OpenAPIV3.OperationObject | undefined;
|
|
94
|
-
let resolvedPath = normalizedPath;
|
|
95
|
-
const paths = doc.paths ?? {};
|
|
96
|
-
|
|
97
|
-
if (paths[normalizedPath]?.[methodLower]) {
|
|
98
|
-
operation = paths[normalizedPath][methodLower] as OpenAPIV3.OperationObject;
|
|
99
|
-
} else {
|
|
100
|
-
const lowerTarget = normalizedPath.toLowerCase();
|
|
101
|
-
for (const [p, pathItem] of Object.entries(paths)) {
|
|
102
|
-
if (p.toLowerCase() === lowerTarget && pathItem?.[methodLower]) {
|
|
103
|
-
operation = pathItem[methodLower] as OpenAPIV3.OperationObject;
|
|
104
|
-
resolvedPath = p;
|
|
105
|
-
break;
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
if (!operation) {
|
|
111
|
-
const available = Object.entries(paths).flatMap(([p, pathItem]) =>
|
|
112
|
-
Object.keys(pathItem ?? {})
|
|
113
|
-
.filter(k => ["get","post","put","patch","delete","head","options","trace"].includes(k))
|
|
114
|
-
.map(k => `${k.toUpperCase()} ${p}`)
|
|
115
|
-
).sort();
|
|
116
|
-
throw new Error(`Endpoint ${method.toUpperCase()} ${endpointPath} not found in spec. Available: ${available.join(", ")}`);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
const pathItem = paths[resolvedPath] ?? {};
|
|
120
|
-
|
|
121
|
-
const pathLevelParams = (pathItem.parameters ?? []) as OpenAPIV3.ParameterObject[];
|
|
122
|
-
const opLevelParams = (operation.parameters ?? []) as OpenAPIV3.ParameterObject[];
|
|
123
|
-
|
|
124
|
-
const paramMap = new Map<string, OpenAPIV3.ParameterObject>();
|
|
125
|
-
for (const p of pathLevelParams) paramMap.set(`${p.in}:${p.name}`, p);
|
|
126
|
-
for (const p of opLevelParams) paramMap.set(`${p.in}:${p.name}`, p);
|
|
127
|
-
|
|
128
|
-
const grouped: Record<string, object[]> = { path: [], query: [], header: [], cookie: [] };
|
|
129
|
-
for (const p of paramMap.values()) {
|
|
130
|
-
const loc = p.in in grouped ? p.in : "query";
|
|
131
|
-
const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
|
|
132
|
-
grouped[loc]!.push({
|
|
133
|
-
name: p.name,
|
|
134
|
-
required: p.required ?? false,
|
|
135
|
-
...(schema?.type ? { type: schema.type } : {}),
|
|
136
|
-
...(schema?.format ? { format: schema.format } : {}),
|
|
137
|
-
...(schema?.enum ? { enum: schema.enum } : {}),
|
|
138
|
-
...(schema?.default !== undefined ? { default: schema.default } : {}),
|
|
139
|
-
...(p.description ? { description: p.description } : {}),
|
|
140
|
-
});
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
let requestBody: object | undefined;
|
|
144
|
-
if (operation.requestBody) {
|
|
145
|
-
const rb = operation.requestBody as OpenAPIV3.RequestBodyObject;
|
|
146
|
-
const contentTypes = Object.keys(rb.content ?? {});
|
|
147
|
-
const preferredCt = contentTypes.find(ct => ct.includes("application/json")) ?? contentTypes[0];
|
|
148
|
-
const mediaObj = preferredCt ? rb.content[preferredCt] : undefined;
|
|
149
|
-
requestBody = {
|
|
150
|
-
required: rb.required ?? false,
|
|
151
|
-
...(preferredCt ? { contentType: preferredCt } : {}),
|
|
152
|
-
...(mediaObj?.schema ? { schema: mediaObj.schema } : {}),
|
|
153
|
-
...(rb.description ? { description: rb.description } : {}),
|
|
154
|
-
};
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
const responses: Record<string, object> = {};
|
|
158
|
-
for (const [statusCode, respObj] of Object.entries(operation.responses ?? {})) {
|
|
159
|
-
const resp = respObj as OpenAPIV3.ResponseObject;
|
|
160
|
-
const contentTypes = Object.keys(resp.content ?? {});
|
|
161
|
-
const preferredCt = contentTypes.find(ct => ct.includes("application/json")) ?? contentTypes[0];
|
|
162
|
-
const mediaObj = preferredCt ? resp.content?.[preferredCt] : undefined;
|
|
163
|
-
|
|
164
|
-
const headers: Record<string, object> = {};
|
|
165
|
-
for (const [hName, hObj] of Object.entries(resp.headers ?? {})) {
|
|
166
|
-
const h = hObj as OpenAPIV3.HeaderObject;
|
|
167
|
-
headers[hName] = {
|
|
168
|
-
...(h.description ? { description: h.description } : {}),
|
|
169
|
-
...(h.schema ? { schema: h.schema } : {}),
|
|
170
|
-
};
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
responses[statusCode] = {
|
|
174
|
-
description: resp.description,
|
|
175
|
-
headers,
|
|
176
|
-
...(preferredCt ? { contentType: preferredCt } : {}),
|
|
177
|
-
...(mediaObj?.schema ? { schema: mediaObj.schema } : {}),
|
|
178
|
-
};
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
const docSecurity = (doc.security ?? []) as OpenAPIV3.SecurityRequirementObject[];
|
|
182
|
-
const opSecurity = (operation.security ?? docSecurity) as OpenAPIV3.SecurityRequirementObject[];
|
|
183
|
-
const securityNames = [...new Set(opSecurity.flatMap(req => Object.keys(req)))];
|
|
184
|
-
|
|
185
|
-
const responseCodes = Object.keys(operation.responses ?? {});
|
|
186
|
-
const successStatus = responseCodes.find(c => c.startsWith("2")) ?? responseCodes[0] ?? "200";
|
|
187
|
-
|
|
188
|
-
const pathParamNames = [...paramMap.values()]
|
|
189
|
-
.filter(p => p.in === "path")
|
|
190
|
-
.map(p => p.name);
|
|
191
|
-
const queryParamsList = [...paramMap.values()]
|
|
192
|
-
.filter(p => p.in === "query")
|
|
193
|
-
.map(p => ({ name: p.name, required: p.required }));
|
|
194
|
-
const reqBodyForSnippet = requestBody
|
|
195
|
-
? { required: (operation.requestBody as OpenAPIV3.RequestBodyObject)?.required, schema: (requestBody as any).schema }
|
|
196
|
-
: undefined;
|
|
197
|
-
|
|
198
|
-
const testSnippet = generateTestSnippet({
|
|
199
|
-
method: method.toUpperCase(),
|
|
200
|
-
path: resolvedPath,
|
|
201
|
-
operationId: operation.operationId,
|
|
202
|
-
pathParams: pathParamNames,
|
|
203
|
-
queryParams: queryParamsList,
|
|
204
|
-
requestBody: reqBodyForSnippet,
|
|
205
|
-
hasSecurity: securityNames.length > 0,
|
|
206
|
-
successStatus,
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
const result: DescribeEndpointResult = {
|
|
210
|
-
method: method.toUpperCase(),
|
|
211
|
-
path: resolvedPath,
|
|
212
|
-
...(operation.operationId ? { operationId: operation.operationId } : {}),
|
|
213
|
-
...(operation.summary ? { summary: operation.summary } : {}),
|
|
214
|
-
...(operation.description ? { description: operation.description } : {}),
|
|
215
|
-
...(operation.tags?.length ? { tags: operation.tags } : {}),
|
|
216
|
-
deprecated: operation.deprecated ?? false,
|
|
217
|
-
security: securityNames,
|
|
218
|
-
parameters: grouped,
|
|
219
|
-
...(requestBody ? { requestBody } : {}),
|
|
220
|
-
responses,
|
|
221
|
-
testSnippet,
|
|
222
|
-
};
|
|
223
|
-
|
|
224
|
-
return decycleSchema(result) as DescribeEndpointResult;
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
export async function describeCompact(
|
|
228
|
-
specPath: string,
|
|
229
|
-
options?: { insecure?: boolean },
|
|
230
|
-
): Promise<CompactEndpoint[]> {
|
|
231
|
-
const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
|
|
232
|
-
const paths = doc.paths ?? {};
|
|
233
|
-
const result: CompactEndpoint[] = [];
|
|
234
|
-
|
|
235
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
236
|
-
for (const method of ["get","post","put","patch","delete","head","options","trace"]) {
|
|
237
|
-
const op = (pathItem as any)?.[method] as OpenAPIV3.OperationObject | undefined;
|
|
238
|
-
if (!op) continue;
|
|
239
|
-
result.push({
|
|
240
|
-
method: method.toUpperCase(),
|
|
241
|
-
path,
|
|
242
|
-
...(op.operationId ? { operationId: op.operationId } : {}),
|
|
243
|
-
...(op.summary ? { summary: op.summary } : {}),
|
|
244
|
-
deprecated: op.deprecated ?? false,
|
|
245
|
-
});
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
return result;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
export interface ParamInfo {
|
|
253
|
-
name: string;
|
|
254
|
-
in: string;
|
|
255
|
-
type?: string;
|
|
256
|
-
required: boolean;
|
|
257
|
-
usedBy: string[];
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
export async function describeAllParams(
|
|
261
|
-
specPath: string,
|
|
262
|
-
options?: { insecure?: boolean },
|
|
263
|
-
): Promise<ParamInfo[]> {
|
|
264
|
-
const doc = await readOpenApiSpec(specPath, options) as OpenAPIV3.Document;
|
|
265
|
-
const paths = doc.paths ?? {};
|
|
266
|
-
const paramMap = new Map<string, ParamInfo>();
|
|
267
|
-
|
|
268
|
-
for (const [path, pathItem] of Object.entries(paths)) {
|
|
269
|
-
const pathParams = ((pathItem as any)?.parameters ?? []) as OpenAPIV3.ParameterObject[];
|
|
270
|
-
|
|
271
|
-
for (const method of ["get", "post", "put", "patch", "delete"]) {
|
|
272
|
-
const op = (pathItem as any)?.[method] as OpenAPIV3.OperationObject | undefined;
|
|
273
|
-
if (!op) continue;
|
|
274
|
-
|
|
275
|
-
const allParams = [...pathParams, ...((op.parameters ?? []) as OpenAPIV3.ParameterObject[])];
|
|
276
|
-
const endpoint = `${method.toUpperCase()} ${path}`;
|
|
277
|
-
|
|
278
|
-
for (const p of allParams) {
|
|
279
|
-
const key = `${p.in}:${p.name}`;
|
|
280
|
-
const existing = paramMap.get(key);
|
|
281
|
-
const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
|
|
282
|
-
if (existing) {
|
|
283
|
-
existing.usedBy.push(endpoint);
|
|
284
|
-
if (p.required) existing.required = true;
|
|
285
|
-
} else {
|
|
286
|
-
paramMap.set(key, {
|
|
287
|
-
name: p.name,
|
|
288
|
-
in: p.in,
|
|
289
|
-
type: schema?.type,
|
|
290
|
-
required: p.required ?? false,
|
|
291
|
-
usedBy: [endpoint],
|
|
292
|
-
});
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
}
|
|
297
|
-
|
|
298
|
-
return [...paramMap.values()].sort((a, b) => {
|
|
299
|
-
if (a.in !== b.in) return a.in.localeCompare(b.in);
|
|
300
|
-
return a.name.localeCompare(b.name);
|
|
301
|
-
});
|
|
302
|
-
}
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { EndpointInfo } from "./types.ts";
|
|
2
|
-
|
|
3
|
-
export type WarningCode = "deprecated" | "no_response_schema" | "no_responses_defined" | "required_params_no_examples" | "post_body_as_query";
|
|
4
|
-
|
|
5
|
-
export interface EndpointWarning {
|
|
6
|
-
method: string;
|
|
7
|
-
path: string;
|
|
8
|
-
warnings: string[];
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function analyzeEndpoints(endpoints: EndpointInfo[]): EndpointWarning[] {
|
|
12
|
-
const result: EndpointWarning[] = [];
|
|
13
|
-
|
|
14
|
-
for (const ep of endpoints) {
|
|
15
|
-
const warnings: string[] = [];
|
|
16
|
-
|
|
17
|
-
if (ep.deprecated) {
|
|
18
|
-
warnings.push("deprecated");
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (ep.responses.length === 0) {
|
|
22
|
-
warnings.push("no_responses_defined");
|
|
23
|
-
} else {
|
|
24
|
-
const has2xx = ep.responses.filter(r => r.statusCode >= 200 && r.statusCode < 300);
|
|
25
|
-
if (has2xx.length > 0 && has2xx.every(r => !r.schema)) {
|
|
26
|
-
warnings.push("no_response_schema");
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const missingExamples = ep.parameters
|
|
31
|
-
.filter(p => p.required && !p.example && !(p.schema && (p.schema as any).example) && !(p.schema && (p.schema as any).default))
|
|
32
|
-
.map(p => p.name);
|
|
33
|
-
if (missingExamples.length > 0) {
|
|
34
|
-
warnings.push(`required_params_no_examples: ${missingExamples.join(", ")}`);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
// SpringDoc quirk: POST/PUT/PATCH with query param named "body" or single complex object query param
|
|
38
|
-
if (["POST", "PUT", "PATCH"].includes(ep.method)) {
|
|
39
|
-
const queryParams = ep.parameters.filter(p => p.in === "query");
|
|
40
|
-
const hasBodyQuery = queryParams.some(p => p.name.toLowerCase() === "body");
|
|
41
|
-
if (hasBodyQuery) {
|
|
42
|
-
warnings.push("post_body_as_query: query param 'body' on POST/PUT/PATCH likely means request body (SpringDoc quirk)");
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (warnings.length > 0) {
|
|
47
|
-
result.push({ method: ep.method, path: ep.path, warnings });
|
|
48
|
-
}
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
return result;
|
|
52
|
-
}
|
|
@@ -1,332 +0,0 @@
|
|
|
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
|
-
}
|