@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,112 +0,0 @@
|
|
|
1
|
-
import type { OpenAPIV3 } from "openapi-types";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Deep-clone an object, replacing circular references with the vendor-extension
|
|
5
|
-
* sentinel `{ "x-circular": true }`. Uses WeakSet to track visited objects.
|
|
6
|
-
*
|
|
7
|
-
* Trade-off (intentional): we mark the SECOND visit of any shared object
|
|
8
|
-
* (post-`dereference()` $ref-target reused under multiple parents) as
|
|
9
|
-
* circular, not just true cycles. The alternative — a DFS path stack —
|
|
10
|
-
* preserves every duplicate fresh, but blows the GitHub spec from 14 MB
|
|
11
|
-
* to 106 MB on disk because shared `per_page`/`page`/`owner` params and
|
|
12
|
-
* response schemas are re-cloned for every endpoint that references them.
|
|
13
|
-
* Downstream tools that need param/schema visibility on every endpoint
|
|
14
|
-
* (e.g. `zond api annotate auto`, ARV-262) handle this by re-reading the
|
|
15
|
-
* source spec themselves rather than relying on `apis/<name>/spec.json`.
|
|
16
|
-
*
|
|
17
|
-
* Why a vendor extension and not `$ref`: the decycled doc is now written to
|
|
18
|
-
* disk (apis/<name>/spec.json) and re-read by `@readme/openapi-parser` in
|
|
19
|
-
* downstream commands (check spec, describe, generate). If the sentinel
|
|
20
|
-
* carried a `$ref` field, the parser would try to resolve its value as a
|
|
21
|
-
* JSON pointer / file path — e.g. `apis/stripe/[Circular]` — and fail
|
|
22
|
-
* (ARV-146). `x-*` keys are explicitly reserved for vendor extensions in
|
|
23
|
-
* OpenAPI 3.x and pass through every parser untouched.
|
|
24
|
-
*/
|
|
25
|
-
export function decycleSchema(obj: unknown): unknown {
|
|
26
|
-
const seen = new WeakSet<object>();
|
|
27
|
-
|
|
28
|
-
function walk(value: unknown): unknown {
|
|
29
|
-
if (value === null || typeof value !== "object") return value;
|
|
30
|
-
|
|
31
|
-
if (Array.isArray(value)) {
|
|
32
|
-
return value.map(item => walk(item));
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
const obj = value as Record<string, unknown>;
|
|
36
|
-
if (seen.has(obj)) {
|
|
37
|
-
// ARV-262: parameter-shaped objects (have both `name` and `in`)
|
|
38
|
-
// appear post-`dereference()` as shared identities across every
|
|
39
|
-
// endpoint that $ref'd them — `per_page`, `page`, `owner`, etc.
|
|
40
|
-
// Past the first visit, the full `{"x-circular": true}` stub
|
|
41
|
-
// erases name+in and downstream tools (annotate auto, generator)
|
|
42
|
-
// can no longer tell what query/header that slot represented.
|
|
43
|
-
// Preserve name+in on revisit; schema/required/description still
|
|
44
|
-
// collapse so the bulk-size trade-off is unchanged (revisits
|
|
45
|
-
// grow by ~30 B each, vs. ~7×-blowup if we cloned everything).
|
|
46
|
-
if (typeof obj.name === "string" && typeof obj.in === "string") {
|
|
47
|
-
return { name: obj.name, in: obj.in };
|
|
48
|
-
}
|
|
49
|
-
return { "x-circular": true };
|
|
50
|
-
}
|
|
51
|
-
seen.add(obj);
|
|
52
|
-
|
|
53
|
-
const result: Record<string, unknown> = {};
|
|
54
|
-
for (const key of Object.keys(obj)) {
|
|
55
|
-
result[key] = walk(obj[key]);
|
|
56
|
-
}
|
|
57
|
-
return result;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return walk(obj);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* Returns true if the schema is effectively `any` — no type, no properties, no constraints.
|
|
65
|
-
*/
|
|
66
|
-
export function isAnySchema(schema: OpenAPIV3.SchemaObject | undefined): boolean {
|
|
67
|
-
if (!schema) return false;
|
|
68
|
-
return Object.keys(schema).length === 0 ||
|
|
69
|
-
(!schema.type && !schema.properties && !schema.enum && !schema.oneOf && !schema.allOf && !schema.anyOf);
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Compress an OpenAPI schema into a concise human-readable string.
|
|
74
|
-
* E.g. { name: string (req), age: integer, tags: [string] }
|
|
75
|
-
*/
|
|
76
|
-
export function compressSchema(schema: OpenAPIV3.SchemaObject, depth = 0): string {
|
|
77
|
-
if (depth > 2) return "{...}";
|
|
78
|
-
|
|
79
|
-
if (schema.type === "object" && schema.properties) {
|
|
80
|
-
const required = new Set(schema.required ?? []);
|
|
81
|
-
const fields = Object.entries(schema.properties).map(([key, propObj]) => {
|
|
82
|
-
const prop = propObj as OpenAPIV3.SchemaObject;
|
|
83
|
-
const type = prop.type ?? "any";
|
|
84
|
-
const flags: string[] = [];
|
|
85
|
-
if (required.has(key)) flags.push("req");
|
|
86
|
-
if (prop.format) flags.push(prop.format);
|
|
87
|
-
if (prop.enum) flags.push(`enum: ${prop.enum.join("|")}`);
|
|
88
|
-
const flagStr = flags.length > 0 ? ` (${flags.join(", ")})` : "";
|
|
89
|
-
return `${key}: ${type}${flagStr}`;
|
|
90
|
-
});
|
|
91
|
-
return `{ ${fields.join(", ")} }`;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
if (schema.type === "array") {
|
|
95
|
-
const items = schema.items as OpenAPIV3.SchemaObject | undefined;
|
|
96
|
-
if (items) return `[${compressSchema(items, depth + 1)}]`;
|
|
97
|
-
return "[]";
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
return schema.type ?? "any";
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Format an OpenAPI parameter into a concise string.
|
|
105
|
-
* E.g. "limit: integer" or "id: string (req)"
|
|
106
|
-
*/
|
|
107
|
-
export function formatParam(p: OpenAPIV3.ParameterObject): string {
|
|
108
|
-
const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
|
|
109
|
-
const type = schema?.type ?? "string";
|
|
110
|
-
const req = p.required ? " (req)" : "";
|
|
111
|
-
return `${p.name}: ${type}${req}`;
|
|
112
|
-
}
|
|
@@ -1,343 +0,0 @@
|
|
|
1
|
-
import { resolve } from "path";
|
|
2
|
-
import type { SourceMetadata } from "../parser/types.ts";
|
|
3
|
-
|
|
4
|
-
// ──────────────────────────────────────────────
|
|
5
|
-
// Utility functions (moved from skeleton.ts)
|
|
6
|
-
// ──────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
function isRelativeUrl(url: string): boolean {
|
|
9
|
-
return url.startsWith("/") && !url.includes("://");
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function resolveSpecPath(specPath: string): string {
|
|
13
|
-
if (specPath.startsWith("http://") || specPath.startsWith("https://")) {
|
|
14
|
-
return specPath;
|
|
15
|
-
}
|
|
16
|
-
return resolve(specPath);
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
function sanitizeEnvName(name: string): string {
|
|
20
|
-
return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").slice(0, 30);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ──────────────────────────────────────────────
|
|
24
|
-
// Types for raw suite serialization
|
|
25
|
-
// ──────────────────────────────────────────────
|
|
26
|
-
|
|
27
|
-
export interface RawStep {
|
|
28
|
-
name: string;
|
|
29
|
-
source?: SourceMetadata;
|
|
30
|
-
[methodKey: string]: unknown;
|
|
31
|
-
expect: {
|
|
32
|
-
status?: number | number[];
|
|
33
|
-
body?: Record<string, Record<string, string>>;
|
|
34
|
-
headers?: Record<string, unknown>;
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export interface RawSuite {
|
|
39
|
-
name: string;
|
|
40
|
-
setup?: boolean;
|
|
41
|
-
tags?: string[];
|
|
42
|
-
source?: SourceMetadata;
|
|
43
|
-
folder?: string;
|
|
44
|
-
fileStem?: string;
|
|
45
|
-
base_url?: string;
|
|
46
|
-
headers?: Record<string, string>;
|
|
47
|
-
tests: RawStep[];
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// ──────────────────────────────────────────────
|
|
51
|
-
// YAML serializer
|
|
52
|
-
// ──────────────────────────────────────────────
|
|
53
|
-
|
|
54
|
-
export function serializeSuite(suite: RawSuite): string {
|
|
55
|
-
const lines: string[] = [];
|
|
56
|
-
lines.push(`name: ${yamlScalar(suite.name)}`);
|
|
57
|
-
if (suite.setup) {
|
|
58
|
-
lines.push("setup: true");
|
|
59
|
-
}
|
|
60
|
-
if (suite.tags && suite.tags.length > 0) {
|
|
61
|
-
lines.push(`tags: [${suite.tags.join(", ")}]`);
|
|
62
|
-
}
|
|
63
|
-
if (suite.base_url) {
|
|
64
|
-
lines.push(`base_url: ${yamlScalar(suite.base_url)}`);
|
|
65
|
-
}
|
|
66
|
-
if (suite.headers && Object.keys(suite.headers).length > 0) {
|
|
67
|
-
lines.push("headers:");
|
|
68
|
-
for (const [hk, hv] of Object.entries(suite.headers)) {
|
|
69
|
-
lines.push(` ${hk}: ${yamlScalar(String(hv))}`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (suite.source && Object.keys(suite.source).length > 0) {
|
|
73
|
-
lines.push("source:");
|
|
74
|
-
serializeValue(suite.source, 1, lines);
|
|
75
|
-
}
|
|
76
|
-
lines.push("tests:");
|
|
77
|
-
|
|
78
|
-
for (const test of suite.tests) {
|
|
79
|
-
lines.push(` - name: ${yamlScalar(test.name)}`);
|
|
80
|
-
|
|
81
|
-
if (test.source && Object.keys(test.source).length > 0) {
|
|
82
|
-
lines.push(" source:");
|
|
83
|
-
serializeValue(test.source, 3, lines);
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
// Write method-as-key (the shorthand)
|
|
87
|
-
for (const method of ["GET", "POST", "PUT", "PATCH", "DELETE"]) {
|
|
88
|
-
if (method in test) {
|
|
89
|
-
lines.push(` ${method}: ${test[method]}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
// headers
|
|
94
|
-
if (test.headers && Object.keys(test.headers as Record<string, string>).length > 0) {
|
|
95
|
-
lines.push(" headers:");
|
|
96
|
-
for (const [hk, hv] of Object.entries(test.headers as Record<string, string>)) {
|
|
97
|
-
lines.push(` ${hk}: ${yamlScalar(String(hv))}`);
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
// json body
|
|
102
|
-
if (test.json !== undefined) {
|
|
103
|
-
lines.push(" json:");
|
|
104
|
-
serializeValue(test.json, 3, lines);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ARV-149: form body (application/x-www-form-urlencoded). The runner
|
|
108
|
-
// serialises this via URLSearchParams; values are flat strings with
|
|
109
|
-
// bracket notation for nested fields (e.g. `address[line1]`).
|
|
110
|
-
//
|
|
111
|
-
// ARV-162 (round-08 F19): form values are ALWAYS strings on the wire —
|
|
112
|
-
// x-www-form-urlencoded has no native numbers/bools/nulls. YAML parsing
|
|
113
|
-
// `phone: +1234567890` or `width: 12.5` as int/float makes `zond check
|
|
114
|
-
// tests` reject the suite ("expected string, received number"), and
|
|
115
|
-
// `zond run` silently skipped 21/68 generated Stripe suites this way.
|
|
116
|
-
// Force-quote every value regardless of shape; key still uses yamlScalar
|
|
117
|
-
// because bracket keys (`address[line1]`) need quoting too.
|
|
118
|
-
if (test.form !== undefined && typeof test.form === "object" && test.form !== null) {
|
|
119
|
-
const formEntries = Object.entries(test.form as Record<string, unknown>);
|
|
120
|
-
if (formEntries.length > 0) {
|
|
121
|
-
lines.push(" form:");
|
|
122
|
-
for (const [fk, fv] of formEntries) {
|
|
123
|
-
lines.push(` ${yamlScalar(fk)}: "${escapeYamlDoubleQuoted(String(fv))}"`);
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
// query
|
|
129
|
-
if (test.query) {
|
|
130
|
-
lines.push(" query:");
|
|
131
|
-
serializeValue(test.query, 3, lines);
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// skip_if
|
|
135
|
-
if (test.skip_if) {
|
|
136
|
-
lines.push(` skip_if: ${yamlScalar(String(test.skip_if))}`);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
// always (cleanup steps that survive cascade-skip on tainted captures)
|
|
140
|
-
if (test.always === true) {
|
|
141
|
-
lines.push(" always: true");
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
// retry_until
|
|
145
|
-
if (test.retry_until && typeof test.retry_until === "object") {
|
|
146
|
-
const rt = test.retry_until as Record<string, unknown>;
|
|
147
|
-
lines.push(" retry_until:");
|
|
148
|
-
if (rt.condition !== undefined) lines.push(` condition: ${yamlScalar(String(rt.condition))}`);
|
|
149
|
-
if (rt.max_attempts !== undefined) lines.push(` max_attempts: ${rt.max_attempts}`);
|
|
150
|
-
if (rt.delay_ms !== undefined) lines.push(` delay_ms: ${rt.delay_ms}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// for_each
|
|
154
|
-
if (test.for_each && typeof test.for_each === "object") {
|
|
155
|
-
const fe = test.for_each as Record<string, unknown>;
|
|
156
|
-
lines.push(" for_each:");
|
|
157
|
-
if (fe.var !== undefined) lines.push(` var: ${yamlScalar(String(fe.var))}`);
|
|
158
|
-
if (fe.in !== undefined) lines.push(` in: ${yamlScalar(String(fe.in))}`);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
// set
|
|
162
|
-
if (test.set && typeof test.set === "object") {
|
|
163
|
-
lines.push(" set:");
|
|
164
|
-
serializeValue(test.set, 3, lines);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
// expect
|
|
168
|
-
const hasExpect = test.expect && (test.expect.status !== undefined || test.expect.body);
|
|
169
|
-
if (hasExpect) {
|
|
170
|
-
lines.push(" expect:");
|
|
171
|
-
if (test.expect.status !== undefined) {
|
|
172
|
-
if (Array.isArray(test.expect.status)) {
|
|
173
|
-
lines.push(` status: [${test.expect.status.join(", ")}]`);
|
|
174
|
-
} else {
|
|
175
|
-
lines.push(` status: ${test.expect.status}`);
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
if (test.expect.body) {
|
|
179
|
-
lines.push(" body:");
|
|
180
|
-
for (const [key, rule] of Object.entries(test.expect.body)) {
|
|
181
|
-
lines.push(` ${key}:`);
|
|
182
|
-
for (const [rk, rv] of Object.entries(rule)) {
|
|
183
|
-
if (typeof rv === "object" && rv !== null) {
|
|
184
|
-
lines.push(` ${rk}:`);
|
|
185
|
-
serializeValue(rv, 6, lines);
|
|
186
|
-
} else {
|
|
187
|
-
// Preserve scalar types — `not_equals: true` (boolean) must not
|
|
188
|
-
// become `not_equals: "true"` (string), or assertions silently
|
|
189
|
-
// mistype against real boolean fields.
|
|
190
|
-
lines.push(` ${rk}: ${formatInlineValue(rv)}`);
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
return lines.join("\n") + "\n";
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
function serializeValue(value: unknown, indent: number, lines: string[]): void {
|
|
202
|
-
const prefix = " ".repeat(indent);
|
|
203
|
-
|
|
204
|
-
if (value === null || value === undefined) {
|
|
205
|
-
lines.push(`${prefix}null`);
|
|
206
|
-
return;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (typeof value === "string" || typeof value === "number" || typeof value === "boolean") {
|
|
210
|
-
lines.push(`${prefix}${yamlScalar(String(value))}`);
|
|
211
|
-
return;
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
if (Array.isArray(value)) {
|
|
215
|
-
for (const item of value) {
|
|
216
|
-
if (typeof item === "object" && item !== null && !Array.isArray(item)) {
|
|
217
|
-
const entries = Object.entries(item as Record<string, unknown>);
|
|
218
|
-
if (entries.length > 0) {
|
|
219
|
-
const [firstKey, firstVal] = entries[0]!;
|
|
220
|
-
if (typeof firstVal === "object" && firstVal !== null) {
|
|
221
|
-
if (isEmptyContainer(firstVal)) {
|
|
222
|
-
lines.push(`${prefix}- ${firstKey}: ${Array.isArray(firstVal) ? "[]" : "{}"}`);
|
|
223
|
-
} else {
|
|
224
|
-
lines.push(`${prefix}- ${firstKey}:`);
|
|
225
|
-
serializeValue(firstVal, indent + 1, lines);
|
|
226
|
-
}
|
|
227
|
-
} else {
|
|
228
|
-
lines.push(`${prefix}- ${firstKey}: ${formatInlineValue(firstVal)}`);
|
|
229
|
-
}
|
|
230
|
-
for (let i = 1; i < entries.length; i++) {
|
|
231
|
-
const [k, v] = entries[i]!;
|
|
232
|
-
if (typeof v === "object" && v !== null) {
|
|
233
|
-
if (isEmptyContainer(v)) {
|
|
234
|
-
lines.push(`${prefix} ${k}: ${Array.isArray(v) ? "[]" : "{}"}`);
|
|
235
|
-
} else {
|
|
236
|
-
lines.push(`${prefix} ${k}:`);
|
|
237
|
-
serializeValue(v, indent + 1, lines);
|
|
238
|
-
}
|
|
239
|
-
} else {
|
|
240
|
-
lines.push(`${prefix} ${k}: ${formatInlineValue(v)}`);
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
} else {
|
|
244
|
-
lines.push(`${prefix}- {}`);
|
|
245
|
-
}
|
|
246
|
-
} else {
|
|
247
|
-
lines.push(`${prefix}- ${formatInlineValue(item)}`);
|
|
248
|
-
}
|
|
249
|
-
}
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
if (typeof value === "object") {
|
|
254
|
-
for (const [key, val] of Object.entries(value as Record<string, unknown>)) {
|
|
255
|
-
if (typeof val === "object" && val !== null) {
|
|
256
|
-
if (isEmptyContainer(val)) {
|
|
257
|
-
lines.push(`${prefix}${key}: ${Array.isArray(val) ? "[]" : "{}"}`);
|
|
258
|
-
} else {
|
|
259
|
-
lines.push(`${prefix}${key}:`);
|
|
260
|
-
serializeValue(val, indent + 1, lines);
|
|
261
|
-
}
|
|
262
|
-
} else {
|
|
263
|
-
lines.push(`${prefix}${key}: ${formatInlineValue(val)}`);
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
}
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Empty `{}` / `[]` written as a bare `key:` (no value, no children) is
|
|
270
|
-
* re-parsed by YAML as `null` — sending `null` for an `object`-typed field
|
|
271
|
-
* guarantees 422 against strict APIs. Emit inline flow form to preserve type. */
|
|
272
|
-
function isEmptyContainer(val: unknown): boolean {
|
|
273
|
-
if (Array.isArray(val)) return val.length === 0;
|
|
274
|
-
if (typeof val === "object" && val !== null) return Object.keys(val).length === 0;
|
|
275
|
-
return false;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function formatInlineValue(val: unknown): string {
|
|
279
|
-
if (val === null || val === undefined) return "null";
|
|
280
|
-
if (typeof val === "string") return yamlScalar(val);
|
|
281
|
-
return String(val);
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
/** ARV-62 (feedback round-01 / F3): security probes emit attack payloads
|
|
285
|
-
* with raw CRLF (`crlf:`, header-injection) and other control bytes.
|
|
286
|
-
* When written into a double-quoted YAML scalar these *must* be escaped
|
|
287
|
-
* (`\r` / `\n` / `\t` / `\xNN`) — emitting the raw byte produces YAML
|
|
288
|
-
* that the parser rejects with "bad indentation of a mapping entry"
|
|
289
|
-
* (`zond run` then fails the whole suite at load-time before sending a
|
|
290
|
-
* single request). The check also covers `\r`, `\t`, `\x00–\x1f`, and
|
|
291
|
-
* `\x7f` so any other control byte that sneaks into a payload survives
|
|
292
|
-
* the YAML round-trip. */
|
|
293
|
-
// eslint-disable-next-line no-control-regex
|
|
294
|
-
const CONTROL_BYTE_RE = /[\x00-\x1f\x7f]/;
|
|
295
|
-
function yamlScalar(value: string): string {
|
|
296
|
-
if (
|
|
297
|
-
value === "" ||
|
|
298
|
-
value === "true" ||
|
|
299
|
-
value === "false" ||
|
|
300
|
-
value === "null" ||
|
|
301
|
-
value.includes(":") ||
|
|
302
|
-
value.includes("#") ||
|
|
303
|
-
value.includes("'") ||
|
|
304
|
-
value.includes('"') ||
|
|
305
|
-
value.includes("{") ||
|
|
306
|
-
value.includes("}") ||
|
|
307
|
-
value.includes("[") ||
|
|
308
|
-
value.includes("]") ||
|
|
309
|
-
value.startsWith("&") ||
|
|
310
|
-
value.startsWith("*") ||
|
|
311
|
-
value.startsWith("!") ||
|
|
312
|
-
value.startsWith("%") ||
|
|
313
|
-
value.startsWith("@") ||
|
|
314
|
-
value.startsWith("`") ||
|
|
315
|
-
/^\d+$/.test(value) ||
|
|
316
|
-
CONTROL_BYTE_RE.test(value)
|
|
317
|
-
) {
|
|
318
|
-
return `"${escapeYamlDoubleQuoted(value)}"`;
|
|
319
|
-
}
|
|
320
|
-
return value;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
function escapeYamlDoubleQuoted(value: string): string {
|
|
324
|
-
let out = "";
|
|
325
|
-
for (let i = 0; i < value.length; i++) {
|
|
326
|
-
const ch = value[i]!;
|
|
327
|
-
const code = ch.charCodeAt(0);
|
|
328
|
-
if (ch === "\\") { out += "\\\\"; continue; }
|
|
329
|
-
if (ch === '"') { out += '\\"'; continue; }
|
|
330
|
-
if (ch === "\n") { out += "\\n"; continue; }
|
|
331
|
-
if (ch === "\r") { out += "\\r"; continue; }
|
|
332
|
-
if (ch === "\t") { out += "\\t"; continue; }
|
|
333
|
-
if (code < 0x20 || code === 0x7f) {
|
|
334
|
-
// YAML 1.2 allows \xNN for any byte in (0..0xff); use this for any
|
|
335
|
-
// control character that doesn't have a dedicated short escape
|
|
336
|
-
// (covers \x00–\x1f minus the three handled above, plus DEL).
|
|
337
|
-
out += "\\x" + code.toString(16).padStart(2, "0");
|
|
338
|
-
continue;
|
|
339
|
-
}
|
|
340
|
-
out += ch;
|
|
341
|
-
}
|
|
342
|
-
return out;
|
|
343
|
-
}
|