@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,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-176: response-schema overlay. `schema-from-runs` (ARV-175) emits a
|
|
3
|
+
* `patch.schema.json` of inferred response schemas keyed by `METHOD /path` →
|
|
4
|
+
* status. `refresh-api --merge-schema` folds that patch into a persistent
|
|
5
|
+
* overlay (`apis/<name>/.api-schema.local.yaml`) and applies it onto the
|
|
6
|
+
* freshly-pulled spec.json.
|
|
7
|
+
*
|
|
8
|
+
* Why a dedicated overlay file (not `.api-resources.local.yaml`): that file
|
|
9
|
+
* is resource-shaped (ResourceYaml[]); response schemas are a different
|
|
10
|
+
* dimension and shoehorning them in would muddy both. Same survives-refresh
|
|
11
|
+
* contract as ARV-111's resource overlay — refresh-api re-applies it on every
|
|
12
|
+
* run, so an upstream re-pull never loses the mined schemas.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
17
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
18
|
+
import type { JsonSchema } from "./infer-schema.ts";
|
|
19
|
+
|
|
20
|
+
export const SCHEMA_OVERLAY_FILENAME = ".api-schema.local.yaml";
|
|
21
|
+
|
|
22
|
+
/** `METHOD /path` → status code → JSON Schema. */
|
|
23
|
+
export type ResponseSchemaPatch = Record<string, Record<string, JsonSchema>>;
|
|
24
|
+
|
|
25
|
+
interface SchemaOverlayFile {
|
|
26
|
+
version: 1;
|
|
27
|
+
response_schemas: ResponseSchemaPatch;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function overlayPath(baseDir: string): string {
|
|
31
|
+
return join(baseDir, SCHEMA_OVERLAY_FILENAME);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Load the overlay for an API, or null when absent. */
|
|
35
|
+
export function loadSchemaOverlay(baseDir: string): ResponseSchemaPatch | null {
|
|
36
|
+
const p = overlayPath(baseDir);
|
|
37
|
+
if (!existsSync(p)) return null;
|
|
38
|
+
try {
|
|
39
|
+
const parsed = parseYaml(readFileSync(p, "utf-8")) as Partial<SchemaOverlayFile> | null;
|
|
40
|
+
if (!parsed || typeof parsed !== "object") return null;
|
|
41
|
+
return parsed.response_schemas ?? null;
|
|
42
|
+
} catch {
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Union two patches; entries in `incoming` win on collision. */
|
|
48
|
+
export function mergePatch(base: ResponseSchemaPatch | null, incoming: ResponseSchemaPatch): ResponseSchemaPatch {
|
|
49
|
+
const out: ResponseSchemaPatch = {};
|
|
50
|
+
for (const [ep, byStatus] of Object.entries(base ?? {})) out[ep] = { ...byStatus };
|
|
51
|
+
for (const [ep, byStatus] of Object.entries(incoming)) {
|
|
52
|
+
out[ep] = { ...(out[ep] ?? {}), ...byStatus };
|
|
53
|
+
}
|
|
54
|
+
return out;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Persist the overlay (sorted for a stable, diff-friendly file). */
|
|
58
|
+
export function saveSchemaOverlay(baseDir: string, patch: ResponseSchemaPatch): void {
|
|
59
|
+
const sorted: ResponseSchemaPatch = {};
|
|
60
|
+
for (const ep of Object.keys(patch).sort()) {
|
|
61
|
+
const byStatus = patch[ep]!;
|
|
62
|
+
sorted[ep] = {};
|
|
63
|
+
for (const st of Object.keys(byStatus).sort()) sorted[ep]![st] = byStatus[st]!;
|
|
64
|
+
}
|
|
65
|
+
const file: SchemaOverlayFile = { version: 1, response_schemas: sorted };
|
|
66
|
+
writeFileSync(overlayPath(baseDir), stringifyYaml(file), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ApplyOverlayResult {
|
|
70
|
+
/** `METHOD /path status` labels that got a schema written. */
|
|
71
|
+
applied: string[];
|
|
72
|
+
/** Labels skipped because a schema already existed (no --force). */
|
|
73
|
+
preserved: string[];
|
|
74
|
+
/** Labels skipped because the endpoint no longer exists upstream. */
|
|
75
|
+
conflicts: string[];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const HTTP_METHODS = ["get", "post", "put", "patch", "delete", "head", "options"];
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Apply a response-schema patch onto a dereferenced OpenAPI doc, in place.
|
|
82
|
+
* Fills `responses.<status>.content['application/json'].schema` where it is
|
|
83
|
+
* absent (or with `force`). An endpoint/method missing from the doc is a
|
|
84
|
+
* conflict (upstream drift) and is skipped, not fabricated.
|
|
85
|
+
*/
|
|
86
|
+
export function applySchemaOverlay(
|
|
87
|
+
doc: unknown,
|
|
88
|
+
patch: ResponseSchemaPatch,
|
|
89
|
+
opts: { force?: boolean } = {},
|
|
90
|
+
): ApplyOverlayResult {
|
|
91
|
+
const result: ApplyOverlayResult = { applied: [], preserved: [], conflicts: [] };
|
|
92
|
+
const paths = (doc as { paths?: Record<string, unknown> }).paths;
|
|
93
|
+
if (!paths) {
|
|
94
|
+
for (const [ep, byStatus] of Object.entries(patch)) {
|
|
95
|
+
for (const st of Object.keys(byStatus)) result.conflicts.push(`${ep} ${st}`);
|
|
96
|
+
}
|
|
97
|
+
return result;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const [endpoint, byStatus] of Object.entries(patch)) {
|
|
101
|
+
const sp = endpoint.indexOf(" ");
|
|
102
|
+
const method = endpoint.slice(0, sp).toLowerCase();
|
|
103
|
+
const path = endpoint.slice(sp + 1);
|
|
104
|
+
const pathItem = paths[path] as Record<string, unknown> | undefined;
|
|
105
|
+
const op = pathItem && HTTP_METHODS.includes(method)
|
|
106
|
+
? (pathItem[method] as { responses?: Record<string, unknown> } | undefined)
|
|
107
|
+
: undefined;
|
|
108
|
+
|
|
109
|
+
for (const [status, schema] of Object.entries(byStatus)) {
|
|
110
|
+
const label = `${endpoint} ${status}`;
|
|
111
|
+
if (!op) {
|
|
112
|
+
result.conflicts.push(label);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (!op.responses) op.responses = {};
|
|
116
|
+
const responses = op.responses as Record<string, { content?: Record<string, { schema?: unknown }> }>;
|
|
117
|
+
if (!responses[status]) responses[status] = { content: {} } as { content: Record<string, { schema?: unknown }> };
|
|
118
|
+
const resp = responses[status]!;
|
|
119
|
+
if (!resp.content) resp.content = {};
|
|
120
|
+
const mt = resp.content["application/json"] ?? (resp.content["application/json"] = {});
|
|
121
|
+
if (mt.schema !== undefined && !opts.force) {
|
|
122
|
+
result.preserved.push(label);
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
mt.schema = schema;
|
|
126
|
+
result.applied.push(label);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return result;
|
|
130
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import Ajv2020 from "ajv/dist/2020.js";
|
|
2
|
+
import Ajv from "ajv";
|
|
3
|
+
import addFormats from "ajv-formats";
|
|
4
|
+
|
|
5
|
+
/** OpenAPI 3.1 → JSON Schema Draft 2020-12 (Ajv2020); 3.0 → Draft 4/7-ish
|
|
6
|
+
* (plain Ajv). Both get the `ajv-formats` format keywords registered. */
|
|
7
|
+
export function makeAjv(isV31: boolean, opts: ConstructorParameters<typeof Ajv>[0] = {}): Ajv {
|
|
8
|
+
const ajv = isV31
|
|
9
|
+
? new (Ajv2020 as unknown as typeof Ajv)(opts)
|
|
10
|
+
: new Ajv(opts);
|
|
11
|
+
addFormats(ajv);
|
|
12
|
+
return ajv;
|
|
13
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-249: human-readable duration formatter for ETA / progress lines.
|
|
3
|
+
*
|
|
4
|
+
* Distinct from `core/reporter/console.formatDuration`, which is geared to
|
|
5
|
+
* per-step latencies (sub-second resolution, single unit). Here we drop
|
|
6
|
+
* sub-second precision but emit two units once we cross a minute, so a
|
|
7
|
+
* five-minute ETA reads `5m12s` instead of `5m 12s`.
|
|
8
|
+
*/
|
|
9
|
+
export function formatEta(seconds: number): string {
|
|
10
|
+
if (!Number.isFinite(seconds) || seconds < 0) return "?";
|
|
11
|
+
const s = Math.round(seconds);
|
|
12
|
+
if (s < 60) return `${s}s`;
|
|
13
|
+
if (s < 3600) {
|
|
14
|
+
const m = Math.floor(s / 60);
|
|
15
|
+
const rem = s % 60;
|
|
16
|
+
return rem > 0 ? `${m}m${rem}s` : `${m}m`;
|
|
17
|
+
}
|
|
18
|
+
const h = Math.floor(s / 3600);
|
|
19
|
+
const m = Math.floor((s % 3600) / 60);
|
|
20
|
+
return m > 0 ? `${h}h${m}m` : `${h}h`;
|
|
21
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Case-insensitive check whether a header is already present in the map.
|
|
2
|
+
* Deliberately not `new Headers(headers).has(name)` — the WHATWG Headers
|
|
3
|
+
* constructor throws on values with invalid characters (e.g. raw CRLF),
|
|
4
|
+
* which real interpolated var values can contain; a plain key scan never
|
|
5
|
+
* throws on the value shape. */
|
|
6
|
+
export function hasHeaderCI(headers: Record<string, string>, name: string): boolean {
|
|
7
|
+
const lower = name.toLowerCase();
|
|
8
|
+
return Object.keys(headers).some((k) => k.toLowerCase() === lower);
|
|
9
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** Concatenate baseUrl and path, stripping any trailing slashes from base. */
|
|
2
|
+
export function joinBaseAndPath(baseUrl: string | undefined, path: string): string {
|
|
3
|
+
if (!baseUrl) return path;
|
|
4
|
+
return `${baseUrl.replace(/\/+$/, "")}${path}`;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export type QueryValue = string | number | boolean;
|
|
8
|
+
|
|
9
|
+
/** Build a URL from base + path with an optional query record. Values are
|
|
10
|
+
* coerced to strings via String(); URL-encoding is delegated to
|
|
11
|
+
* URLSearchParams. */
|
|
12
|
+
export function buildUrl(
|
|
13
|
+
baseUrl: string | undefined,
|
|
14
|
+
path: string,
|
|
15
|
+
query?: Record<string, QueryValue>,
|
|
16
|
+
): string {
|
|
17
|
+
let url = joinBaseAndPath(baseUrl, path);
|
|
18
|
+
if (query && Object.keys(query).length > 0) {
|
|
19
|
+
const params = new URLSearchParams();
|
|
20
|
+
for (const [k, v] of Object.entries(query)) params.append(k, String(v));
|
|
21
|
+
url += `?${params.toString()}`;
|
|
22
|
+
}
|
|
23
|
+
return url;
|
|
24
|
+
}
|
package/src/core/utils.ts
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
export function getByPath(obj: unknown, path: string, defaultVal?: unknown): unknown {
|
|
2
|
-
|
|
2
|
+
// Normalize JSONPath-like bracket indexing (`data[0].id`) to dotted form
|
|
3
|
+
// (`data.0.id`) so callers can use either spelling. Numeric segments also
|
|
4
|
+
// index arrays correctly because `array["0"]` is equivalent to `array[0]`.
|
|
5
|
+
const normalized = path.replace(/\[(\d+)\]/g, ".$1").replace(/^\./, "");
|
|
6
|
+
const keys = normalized.split(".");
|
|
3
7
|
let result: unknown = obj;
|
|
4
8
|
for (const key of keys) {
|
|
5
9
|
result = (result as Record<string, unknown>)?.[key];
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Workspace `zond.config.yml` loader (TASK-301).
|
|
3
|
+
*
|
|
4
|
+
* Centralises read-only access to workspace-level defaults. Right now only
|
|
5
|
+
* two fields are honoured:
|
|
6
|
+
*
|
|
7
|
+
* defaults:
|
|
8
|
+
* timeout_ms: 30000 # used by cleanup / prepare-fixtures / probe
|
|
9
|
+
* # mass-assignment / probe security / request
|
|
10
|
+
* rate_limit: 5 # used by `zond run` (number, or "auto")
|
|
11
|
+
*
|
|
12
|
+
* Resolution chain across the CLI (highest wins):
|
|
13
|
+
*
|
|
14
|
+
* CLI flag → per-API .env.yaml meta → workspace defaults → hard-coded fallback
|
|
15
|
+
*
|
|
16
|
+
* Per-API overrides live in `apis/<name>/.env.yaml` as `rateLimit:` /
|
|
17
|
+
* `timeoutMs:` (see `loadEnvMeta`); we deliberately don't carve a second
|
|
18
|
+
* channel into this file to avoid two ways of saying the same thing.
|
|
19
|
+
*
|
|
20
|
+
* Read-once-and-cache: the file is parsed at most once per process from
|
|
21
|
+
* the workspace root resolved by `findWorkspaceRoot`. Tests can call
|
|
22
|
+
* `_resetWorkspaceConfigCache()` between runs.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
26
|
+
import { join } from "node:path";
|
|
27
|
+
import { findWorkspaceRoot } from "./root.ts";
|
|
28
|
+
|
|
29
|
+
export interface WorkspaceDefaults {
|
|
30
|
+
/** Per-request timeout in ms applied when the CLI flag and `.env.yaml` are silent. */
|
|
31
|
+
timeoutMs?: number;
|
|
32
|
+
/** Run-time rate limit (rps) or `"auto"`. */
|
|
33
|
+
rateLimit?: number | "auto";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
let cache: { root: string; defaults: WorkspaceDefaults } | null = null;
|
|
37
|
+
|
|
38
|
+
function parseTimeoutMs(v: unknown): number | undefined {
|
|
39
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
40
|
+
if (typeof v === "string") {
|
|
41
|
+
const n = Number.parseInt(v, 10);
|
|
42
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
43
|
+
}
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseRateLimit(v: unknown): number | "auto" | undefined {
|
|
48
|
+
if (v === "auto") return "auto";
|
|
49
|
+
if (typeof v === "number" && Number.isFinite(v) && v > 0) return v;
|
|
50
|
+
if (typeof v === "string") {
|
|
51
|
+
if (v === "auto") return "auto";
|
|
52
|
+
const n = Number.parseFloat(v);
|
|
53
|
+
if (Number.isFinite(n) && n > 0) return n;
|
|
54
|
+
}
|
|
55
|
+
return undefined;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function readDefaults(configPath: string): WorkspaceDefaults {
|
|
59
|
+
if (!existsSync(configPath)) return {};
|
|
60
|
+
let parsed: unknown;
|
|
61
|
+
try {
|
|
62
|
+
const text = readFileSync(configPath, "utf8");
|
|
63
|
+
parsed = Bun.YAML.parse(text);
|
|
64
|
+
} catch {
|
|
65
|
+
return {};
|
|
66
|
+
}
|
|
67
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) return {};
|
|
68
|
+
const obj = parsed as Record<string, unknown>;
|
|
69
|
+
const defaultsRaw = obj.defaults;
|
|
70
|
+
if (typeof defaultsRaw !== "object" || defaultsRaw === null || Array.isArray(defaultsRaw)) return {};
|
|
71
|
+
const d = defaultsRaw as Record<string, unknown>;
|
|
72
|
+
const out: WorkspaceDefaults = {};
|
|
73
|
+
const t = parseTimeoutMs(d.timeout_ms ?? d.timeoutMs);
|
|
74
|
+
if (t !== undefined) out.timeoutMs = t;
|
|
75
|
+
const r = parseRateLimit(d.rate_limit ?? d.rateLimit);
|
|
76
|
+
if (r !== undefined) out.rateLimit = r;
|
|
77
|
+
return out;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Returns the workspace defaults block, cached for the lifetime of the
|
|
82
|
+
* process. When no workspace marker is found, returns `{}` (no defaults).
|
|
83
|
+
*/
|
|
84
|
+
export function loadWorkspaceDefaults(cwd?: string): WorkspaceDefaults {
|
|
85
|
+
const ws = findWorkspaceRoot(cwd);
|
|
86
|
+
if (ws.fromFallback) return {};
|
|
87
|
+
if (cache && cache.root === ws.root) return cache.defaults;
|
|
88
|
+
const defaults = readDefaults(join(ws.root, "zond.config.yml"));
|
|
89
|
+
cache = { root: ws.root, defaults };
|
|
90
|
+
return defaults;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Test helper: drop the parse cache so the next call re-reads from disk. */
|
|
94
|
+
export function _resetWorkspaceConfigCache(): void {
|
|
95
|
+
cache = null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const HARD_DEFAULT_TIMEOUT_MS = 30000;
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Resolve `--timeout` via CLI > per-API `.env.yaml` (`timeoutMs`) > workspace
|
|
102
|
+
* `defaults.timeout_ms` > 30000. Each layer accepts `undefined` to mean
|
|
103
|
+
* "fall through".
|
|
104
|
+
*/
|
|
105
|
+
export function resolveTimeoutMs(
|
|
106
|
+
cliFlag: number | undefined,
|
|
107
|
+
envMetaTimeout: number | undefined,
|
|
108
|
+
cwd?: string,
|
|
109
|
+
): number {
|
|
110
|
+
if (cliFlag !== undefined) return cliFlag;
|
|
111
|
+
if (envMetaTimeout !== undefined) return envMetaTimeout;
|
|
112
|
+
const ws = loadWorkspaceDefaults(cwd);
|
|
113
|
+
return ws.timeoutMs ?? HARD_DEFAULT_TIMEOUT_MS;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Resolve `--rate-limit` via CLI > per-API `.env.yaml` (`rateLimit`) >
|
|
118
|
+
* workspace `defaults.rate_limit` > undefined (no throttling).
|
|
119
|
+
*/
|
|
120
|
+
export function resolveRateLimit(
|
|
121
|
+
cliFlag: number | "auto" | undefined,
|
|
122
|
+
envMetaRateLimit: number | "auto" | undefined,
|
|
123
|
+
cwd?: string,
|
|
124
|
+
): number | "auto" | undefined {
|
|
125
|
+
if (cliFlag !== undefined) return cliFlag;
|
|
126
|
+
if (envMetaRateLimit !== undefined) return envMetaRateLimit;
|
|
127
|
+
const ws = loadWorkspaceDefaults(cwd);
|
|
128
|
+
return ws.rateLimit;
|
|
129
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-349/350: deterministic fixture-gap report for `prepare-fixtures`.
|
|
3
|
+
*
|
|
4
|
+
* prepare-fixtures is a single-pass discover (ARV-336 removed the autonomous
|
|
5
|
+
* seed engine). It fills FK ids it can resolve from list endpoints, but it
|
|
6
|
+
* used to neither fill NOR flag two other classes of gap, so suites ran with
|
|
7
|
+
* unresolved placeholders and produced noisy 400/404s:
|
|
8
|
+
*
|
|
9
|
+
* - undefinedVars (ARV-349): a suite references {{bank_code}} / {{tax_id}}
|
|
10
|
+
* that nothing produces — not an env value, not a prior-step capture, not
|
|
11
|
+
* a manifest entry. The user / agent must supply it.
|
|
12
|
+
* - unseededRoots (ARV-350): a REQUIRED manifest var (e.g. {{account}}) that
|
|
13
|
+
* is empty in env, referenced by a suite, and captured by no step — the
|
|
14
|
+
* dependency-chain HEAD that gates dependent CRUD suites (persons-crud,
|
|
15
|
+
* external_accounts-crud all skip: "required fixture {{account}} is empty").
|
|
16
|
+
* Source-agnostic on purpose: real manifests model this root as `path`
|
|
17
|
+
* required:true with an empty default, not necessarily `capture-chain`.
|
|
18
|
+
*
|
|
19
|
+
* REPORT ONLY — never invents a value (ARV-349 #2) and never auto-seeds
|
|
20
|
+
* (ARV-350 #2). Deterministic (same suites+env+manifest → same report), so it
|
|
21
|
+
* belongs in zond; supplying the values is the agent's/user's job.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import type { TestSuite } from "../parser/types.ts";
|
|
25
|
+
import {
|
|
26
|
+
preflightCheckVars,
|
|
27
|
+
collectCapturesAndSets,
|
|
28
|
+
collectStepRefs,
|
|
29
|
+
} from "../runner/preflight-vars.ts";
|
|
30
|
+
|
|
31
|
+
export interface FixtureGapReport {
|
|
32
|
+
/** Suite {{vars}} with no producer (not env, capture, param, or generator). */
|
|
33
|
+
undefinedVars: { variable: string; refs: number; suites: string[] }[];
|
|
34
|
+
/** Required manifest vars that are empty, suite-referenced, and step-unseeded. */
|
|
35
|
+
unseededRoots: { variable: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function reportFixtureGaps(
|
|
39
|
+
suites: TestSuite[],
|
|
40
|
+
env: Record<string, string>,
|
|
41
|
+
requiredEmptyVars: Set<string>,
|
|
42
|
+
): FixtureGapReport {
|
|
43
|
+
// Match runtime capture scoping: only `setup: true` suites share their
|
|
44
|
+
// captures into other suites (TestSuite.setup); a regular suite's captures
|
|
45
|
+
// stay local to itself. So a var is "seeded" for suite S iff env holds it,
|
|
46
|
+
// a setup suite captures it, or S itself captures it.
|
|
47
|
+
const setupProduced = new Set<string>();
|
|
48
|
+
for (const suite of suites) {
|
|
49
|
+
if (!suite.setup) continue;
|
|
50
|
+
for (const step of suite.tests) collectCapturesAndSets(step, setupProduced);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Unseeded root = required + empty in env + SOME referencing suite cannot
|
|
54
|
+
// produce it (not a setup capture, not captured within that suite). This
|
|
55
|
+
// catches cross-suite roots like {{account}}: crud-accounts creates it, but
|
|
56
|
+
// persons-crud references it without a create → that suite skips at runtime.
|
|
57
|
+
const rootSet = new Set<string>();
|
|
58
|
+
for (const suite of suites) {
|
|
59
|
+
const seeded = new Set<string>(setupProduced);
|
|
60
|
+
for (const step of suite.tests) collectCapturesAndSets(step, seeded);
|
|
61
|
+
for (const step of suite.tests) {
|
|
62
|
+
for (const v of collectStepRefs(step)) {
|
|
63
|
+
if (requiredEmptyVars.has(v) && !seeded.has(v)) rootSet.add(v);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
const unseededRoots = [...rootSet].sort().map(variable => ({ variable }));
|
|
68
|
+
|
|
69
|
+
// Undefined vars = preflight hits, minus the roots we already called out
|
|
70
|
+
// (a root absent from env would otherwise land in both buckets).
|
|
71
|
+
const byVar = new Map<string, { refs: number; suites: Set<string> }>();
|
|
72
|
+
for (const h of preflightCheckVars(suites, env)) {
|
|
73
|
+
if (rootSet.has(h.variable)) continue;
|
|
74
|
+
let e = byVar.get(h.variable);
|
|
75
|
+
if (!e) { e = { refs: 0, suites: new Set() }; byVar.set(h.variable, e); }
|
|
76
|
+
e.refs++;
|
|
77
|
+
e.suites.add(h.suite);
|
|
78
|
+
}
|
|
79
|
+
const undefinedVars = [...byVar.entries()]
|
|
80
|
+
.map(([variable, e]) => ({ variable, refs: e.refs, suites: [...e.suites].sort() }))
|
|
81
|
+
.sort((a, b) => a.variable.localeCompare(b.variable));
|
|
82
|
+
|
|
83
|
+
return { undefinedVars, unseededRoots };
|
|
84
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-324: `.fixture-gaps.yaml` — per-API sidecar recording operations
|
|
3
|
+
* that `prepare-fixtures`/`discover` already confirmed return a client
|
|
4
|
+
* error (or an empty list) while hunting for fixture values — e.g. an
|
|
5
|
+
* empty/under-provisioned test account.
|
|
6
|
+
*
|
|
7
|
+
* `checks run` runs as a separate process invocation and, without this
|
|
8
|
+
* file, has no way to tell "known fixture gap" apart from "new backend
|
|
9
|
+
* bug" — both would land on the same operation with
|
|
10
|
+
* `recommended_action: report_backend_bug`. Loading this file lets the
|
|
11
|
+
* classifier downgrade to `fix_fixture` instead (see
|
|
12
|
+
* `core/classifier/recommended-action.ts`).
|
|
13
|
+
*
|
|
14
|
+
* Not the manifest (`.api-fixtures.yaml` stays spec-derived/read-only,
|
|
15
|
+
* regenerated only by `refresh-api`) — this is runtime-observed state,
|
|
16
|
+
* so it gets its own file, rewritten wholesale on every
|
|
17
|
+
* discover/prepare-fixtures run so a since-fixed gap doesn't linger.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import { join } from "node:path";
|
|
21
|
+
import { writeFile } from "node:fs/promises";
|
|
22
|
+
|
|
23
|
+
export interface FixtureGap {
|
|
24
|
+
method: string;
|
|
25
|
+
path: string;
|
|
26
|
+
resource: string;
|
|
27
|
+
var: string;
|
|
28
|
+
reason: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const FILENAME = ".fixture-gaps.yaml";
|
|
32
|
+
|
|
33
|
+
export async function readFixtureGaps(apiDir: string): Promise<FixtureGap[]> {
|
|
34
|
+
const file = Bun.file(join(apiDir, FILENAME));
|
|
35
|
+
if (!(await file.exists())) return [];
|
|
36
|
+
let parsed: unknown;
|
|
37
|
+
try {
|
|
38
|
+
parsed = Bun.YAML.parse(await file.text());
|
|
39
|
+
} catch {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
if (!parsed || typeof parsed !== "object" || !Array.isArray((parsed as { gaps?: unknown }).gaps)) {
|
|
43
|
+
return [];
|
|
44
|
+
}
|
|
45
|
+
return (parsed as { gaps: FixtureGap[] }).gaps;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export async function writeFixtureGaps(apiDir: string, gaps: FixtureGap[]): Promise<void> {
|
|
49
|
+
const path = join(apiDir, FILENAME);
|
|
50
|
+
const header = `# .fixture-gaps.yaml — auto-written by prepare-fixtures/discover (ARV-324)
|
|
51
|
+
#
|
|
52
|
+
# Operations that returned a client error (or an empty list) while zond
|
|
53
|
+
# hunted for a fixture value — a resource that's empty/inaccessible in
|
|
54
|
+
# the target API. Read by \`checks run\` so it doesn't relabel the same
|
|
55
|
+
# known gap as report_backend_bug. Safe to delete — regenerated on the
|
|
56
|
+
# next prepare-fixtures / discover run.
|
|
57
|
+
`;
|
|
58
|
+
const { stringify } = await import("yaml");
|
|
59
|
+
const body = stringify({ gaps }, { lineWidth: 0, defaultStringType: "PLAIN" });
|
|
60
|
+
await writeFile(path, header + "\n" + body, "utf-8");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Key an operation the same way on both the write side (discover/bootstrap,
|
|
64
|
+
* always GET list-probes) and the read side (`CheckFinding.operation`). */
|
|
65
|
+
export function gapKey(method: string, path: string): string {
|
|
66
|
+
return `${method.toUpperCase()} ${path}`;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function gapIndex(gaps: FixtureGap[]): Set<string> {
|
|
70
|
+
return new Set(gaps.map((g) => gapKey(g.method, g.path)));
|
|
71
|
+
}
|