@kirrosh/zond 0.26.1 → 0.27.1
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 +65 -0
- package/README.md +39 -22
- package/bin/zond.mjs +23 -0
- package/package.json +36 -23
- 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 -1162
- 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,383 +0,0 @@
|
|
|
1
|
-
import type { TestStepExpect, AssertionRule } from "../parser/types.ts";
|
|
2
|
-
import type { HttpResponse, AssertionResult } from "./types.ts";
|
|
3
|
-
import { getByPath } from "../utils.ts";
|
|
4
|
-
|
|
5
|
-
function checkType(value: unknown, expectedType: string): boolean {
|
|
6
|
-
switch (expectedType) {
|
|
7
|
-
case "string": return typeof value === "string";
|
|
8
|
-
case "integer": return typeof value === "number" && Number.isInteger(value);
|
|
9
|
-
case "number": return typeof value === "number";
|
|
10
|
-
case "boolean": return typeof value === "boolean";
|
|
11
|
-
case "array": return Array.isArray(value);
|
|
12
|
-
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
-
case "null": return value === null;
|
|
14
|
-
default: return false;
|
|
15
|
-
}
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function describeType(value: unknown): string {
|
|
19
|
-
if (value === null) return "null";
|
|
20
|
-
if (value === undefined) return "undefined";
|
|
21
|
-
if (Array.isArray(value)) return "array";
|
|
22
|
-
if (typeof value === "number" && Number.isInteger(value)) return "integer";
|
|
23
|
-
return typeof value;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function deepEquals(a: unknown, b: unknown): boolean {
|
|
27
|
-
if (a === b) return true;
|
|
28
|
-
// Loose numeric comparison: "123" == 123
|
|
29
|
-
if (typeof a === "number" && typeof b === "string") return a === Number(b);
|
|
30
|
-
if (typeof a === "string" && typeof b === "number") return Number(a) === b;
|
|
31
|
-
if (typeof a !== typeof b) return false;
|
|
32
|
-
if (typeof a !== "object" || a === null || b === null) return false;
|
|
33
|
-
return Bun.deepEquals(a, b);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function checkRule(path: string, rule: AssertionRule, actual: unknown): AssertionResult[] {
|
|
37
|
-
const results: AssertionResult[] = [];
|
|
38
|
-
const field = `body.${path}`;
|
|
39
|
-
|
|
40
|
-
if (rule.exists !== undefined) {
|
|
41
|
-
// Key-presence semantics: null counts as "exists" (key present in response).
|
|
42
|
-
// Use `not_equals: null` or `type: "null"` to assert non-null specifically.
|
|
43
|
-
const doesExist = actual !== undefined;
|
|
44
|
-
results.push({
|
|
45
|
-
field, rule: `exists ${rule.exists}`,
|
|
46
|
-
passed: doesExist === rule.exists, actual: doesExist, expected: rule.exists,
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (rule.type !== undefined) {
|
|
51
|
-
results.push({
|
|
52
|
-
field, rule: `type ${rule.type}`,
|
|
53
|
-
passed: checkType(actual, rule.type), actual: describeType(actual), expected: rule.type,
|
|
54
|
-
});
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
if (rule.equals !== undefined) {
|
|
58
|
-
results.push({
|
|
59
|
-
field, rule: `equals ${JSON.stringify(rule.equals)}`,
|
|
60
|
-
passed: deepEquals(actual, rule.equals), actual, expected: rule.equals,
|
|
61
|
-
});
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (rule.contains !== undefined) {
|
|
65
|
-
const passed = typeof actual === "string" && actual.includes(rule.contains);
|
|
66
|
-
results.push({
|
|
67
|
-
field, rule: `contains "${rule.contains}"`,
|
|
68
|
-
passed, actual, expected: rule.contains,
|
|
69
|
-
});
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
if (rule.matches !== undefined) {
|
|
73
|
-
const passed = typeof actual === "string" && new RegExp(rule.matches).test(actual);
|
|
74
|
-
results.push({
|
|
75
|
-
field, rule: `matches ${rule.matches}`,
|
|
76
|
-
passed, actual, expected: rule.matches,
|
|
77
|
-
});
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
if (rule.gt !== undefined) {
|
|
81
|
-
const passed = typeof actual === "number" && actual > rule.gt;
|
|
82
|
-
results.push({
|
|
83
|
-
field, rule: `gt ${rule.gt}`,
|
|
84
|
-
passed, actual, expected: rule.gt,
|
|
85
|
-
});
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
if (rule.lt !== undefined) {
|
|
89
|
-
const passed = typeof actual === "number" && actual < rule.lt;
|
|
90
|
-
results.push({
|
|
91
|
-
field, rule: `lt ${rule.lt}`,
|
|
92
|
-
passed, actual, expected: rule.lt,
|
|
93
|
-
});
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (rule.not_equals !== undefined) {
|
|
97
|
-
results.push({
|
|
98
|
-
field, rule: `not_equals ${JSON.stringify(rule.not_equals)}`,
|
|
99
|
-
passed: !deepEquals(actual, rule.not_equals), actual, expected: rule.not_equals,
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
if (rule.not_contains !== undefined) {
|
|
104
|
-
const passed = typeof actual === "string" && !actual.includes(rule.not_contains);
|
|
105
|
-
results.push({
|
|
106
|
-
field, rule: `not_contains "${rule.not_contains}"`,
|
|
107
|
-
passed, actual, expected: rule.not_contains,
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
if (rule.gte !== undefined) {
|
|
112
|
-
const passed = typeof actual === "number" && actual >= rule.gte;
|
|
113
|
-
results.push({
|
|
114
|
-
field, rule: `gte ${rule.gte}`,
|
|
115
|
-
passed, actual, expected: rule.gte,
|
|
116
|
-
});
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
if (rule.lte !== undefined) {
|
|
120
|
-
const passed = typeof actual === "number" && actual <= rule.lte;
|
|
121
|
-
results.push({
|
|
122
|
-
field, rule: `lte ${rule.lte}`,
|
|
123
|
-
passed, actual, expected: rule.lte,
|
|
124
|
-
});
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (rule.length !== undefined) {
|
|
128
|
-
const hasLength = (Array.isArray(actual) || typeof actual === "string");
|
|
129
|
-
const passed = hasLength && (actual as string | unknown[]).length === rule.length;
|
|
130
|
-
results.push({
|
|
131
|
-
field, rule: `length ${rule.length}`,
|
|
132
|
-
passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length,
|
|
133
|
-
});
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
if (rule.length_gt !== undefined) {
|
|
137
|
-
const hasLength = (Array.isArray(actual) || typeof actual === "string");
|
|
138
|
-
const passed = hasLength && (actual as string | unknown[]).length > rule.length_gt;
|
|
139
|
-
results.push({
|
|
140
|
-
field, rule: `length_gt ${rule.length_gt}`,
|
|
141
|
-
passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_gt,
|
|
142
|
-
});
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
if (rule.length_gte !== undefined) {
|
|
146
|
-
const hasLength = (Array.isArray(actual) || typeof actual === "string");
|
|
147
|
-
const passed = hasLength && (actual as string | unknown[]).length >= rule.length_gte;
|
|
148
|
-
results.push({
|
|
149
|
-
field, rule: `length_gte ${rule.length_gte}`,
|
|
150
|
-
passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_gte,
|
|
151
|
-
});
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (rule.length_lt !== undefined) {
|
|
155
|
-
const hasLength = (Array.isArray(actual) || typeof actual === "string");
|
|
156
|
-
const passed = hasLength && (actual as string | unknown[]).length < rule.length_lt;
|
|
157
|
-
results.push({
|
|
158
|
-
field, rule: `length_lt ${rule.length_lt}`,
|
|
159
|
-
passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_lt,
|
|
160
|
-
});
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
if (rule.length_lte !== undefined) {
|
|
164
|
-
const hasLength = (Array.isArray(actual) || typeof actual === "string");
|
|
165
|
-
const passed = hasLength && (actual as string | unknown[]).length <= rule.length_lte;
|
|
166
|
-
results.push({
|
|
167
|
-
field, rule: `length_lte ${rule.length_lte}`,
|
|
168
|
-
passed, actual: hasLength ? (actual as string | unknown[]).length : describeType(actual), expected: rule.length_lte,
|
|
169
|
-
});
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
if (rule.each !== undefined) {
|
|
173
|
-
if (!Array.isArray(actual)) {
|
|
174
|
-
results.push({ field, rule: "each", passed: false, actual: describeType(actual), expected: "array" });
|
|
175
|
-
} else {
|
|
176
|
-
for (let i = 0; i < actual.length; i++) {
|
|
177
|
-
for (const [subPath, subRule] of Object.entries(rule.each)) {
|
|
178
|
-
const subActual = getByPath(actual[i], subPath);
|
|
179
|
-
const subResults = checkRule(`${path}[${i}].${subPath}`, subRule, subActual);
|
|
180
|
-
results.push(...subResults);
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
if (rule.contains_item !== undefined) {
|
|
187
|
-
if (!Array.isArray(actual)) {
|
|
188
|
-
results.push({ field, rule: "contains_item", passed: false, actual: describeType(actual), expected: "array" });
|
|
189
|
-
} else {
|
|
190
|
-
const found = actual.some((item) => {
|
|
191
|
-
for (const [subPath, subRule] of Object.entries(rule.contains_item!)) {
|
|
192
|
-
const subActual = getByPath(item, subPath);
|
|
193
|
-
const subResults = checkRule("", subRule, subActual);
|
|
194
|
-
if (subResults.some(r => !r.passed)) return false;
|
|
195
|
-
}
|
|
196
|
-
return true;
|
|
197
|
-
});
|
|
198
|
-
results.push({
|
|
199
|
-
field, rule: "contains_item",
|
|
200
|
-
passed: found, actual: `array(${actual.length})`, expected: "at least one matching item",
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
if (rule.set_equals !== undefined) {
|
|
206
|
-
if (!Array.isArray(actual) || !Array.isArray(rule.set_equals)) {
|
|
207
|
-
results.push({
|
|
208
|
-
field, rule: "set_equals",
|
|
209
|
-
passed: false, actual: describeType(actual), expected: "both must be arrays",
|
|
210
|
-
});
|
|
211
|
-
} else {
|
|
212
|
-
const actualSet = new Set(actual.map(v => JSON.stringify(v)));
|
|
213
|
-
const expectedSet = new Set((rule.set_equals as unknown[]).map(v => JSON.stringify(v)));
|
|
214
|
-
const passed = actualSet.size === expectedSet.size && [...actualSet].every(v => expectedSet.has(v));
|
|
215
|
-
results.push({
|
|
216
|
-
field, rule: "set_equals",
|
|
217
|
-
passed, actual, expected: rule.set_equals,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return results;
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
export function checkAssertions(expect: TestStepExpect, response: HttpResponse): AssertionResult[] {
|
|
226
|
-
const results: AssertionResult[] = [];
|
|
227
|
-
|
|
228
|
-
if (expect.status !== undefined) {
|
|
229
|
-
const allowed = Array.isArray(expect.status) ? expect.status : [expect.status];
|
|
230
|
-
results.push({
|
|
231
|
-
field: "status",
|
|
232
|
-
rule: allowed.length === 1 ? `equals ${allowed[0]}` : `one of [${allowed.join(", ")}]`,
|
|
233
|
-
passed: allowed.includes(response.status),
|
|
234
|
-
actual: response.status,
|
|
235
|
-
expected: expect.status,
|
|
236
|
-
kind: "primary",
|
|
237
|
-
});
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (expect.duration !== undefined) {
|
|
241
|
-
results.push({
|
|
242
|
-
field: "duration",
|
|
243
|
-
rule: `lte ${expect.duration}ms`,
|
|
244
|
-
passed: response.duration_ms <= expect.duration,
|
|
245
|
-
actual: response.duration_ms,
|
|
246
|
-
expected: expect.duration,
|
|
247
|
-
kind: "auxiliary",
|
|
248
|
-
});
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
if (expect.headers) {
|
|
252
|
-
for (const [key, rule] of Object.entries(expect.headers)) {
|
|
253
|
-
const actual = response.headers[key.toLowerCase()];
|
|
254
|
-
if (typeof rule === "string") {
|
|
255
|
-
results.push({
|
|
256
|
-
field: `headers.${key}`,
|
|
257
|
-
rule: `equals "${rule}"`,
|
|
258
|
-
passed: actual === rule,
|
|
259
|
-
actual,
|
|
260
|
-
expected: rule,
|
|
261
|
-
kind: "auxiliary",
|
|
262
|
-
});
|
|
263
|
-
} else {
|
|
264
|
-
// AssertionRule in header — supports capture and other checks
|
|
265
|
-
const ruleResults = checkRule(key, rule, actual).map(r => ({
|
|
266
|
-
...r,
|
|
267
|
-
field: r.field.replace(/^body\./, "headers."),
|
|
268
|
-
kind: "auxiliary" as const,
|
|
269
|
-
}));
|
|
270
|
-
results.push(...ruleResults);
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (expect.body && response.body_parsed !== undefined) {
|
|
276
|
-
for (const [path, rule] of Object.entries(expect.body)) {
|
|
277
|
-
let actual: unknown;
|
|
278
|
-
if (path === "_body") {
|
|
279
|
-
actual = response.body_parsed;
|
|
280
|
-
} else if (path.startsWith("_body.")) {
|
|
281
|
-
actual = getByPath(response.body_parsed, path.slice(6));
|
|
282
|
-
} else {
|
|
283
|
-
actual = getByPath(response.body_parsed, path);
|
|
284
|
-
}
|
|
285
|
-
const bodyResults = checkRule(path, rule, actual).map((r) => ({
|
|
286
|
-
...r,
|
|
287
|
-
kind: "primary" as const,
|
|
288
|
-
}));
|
|
289
|
-
results.push(...bodyResults);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
return results;
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
export function extractCaptures(
|
|
297
|
-
bodyRules: Record<string, AssertionRule> | undefined,
|
|
298
|
-
responseBody: unknown,
|
|
299
|
-
headerRules?: Record<string, string | AssertionRule>,
|
|
300
|
-
responseHeaders?: Record<string, string>,
|
|
301
|
-
): Record<string, unknown> {
|
|
302
|
-
const captures: Record<string, unknown> = {};
|
|
303
|
-
|
|
304
|
-
if (bodyRules && responseBody !== undefined) {
|
|
305
|
-
for (const [path, rule] of Object.entries(bodyRules)) {
|
|
306
|
-
if (rule.capture) {
|
|
307
|
-
let value: unknown;
|
|
308
|
-
if (path === "_body") {
|
|
309
|
-
value = responseBody;
|
|
310
|
-
} else if (path.startsWith("_body.")) {
|
|
311
|
-
value = getByPath(responseBody, path.slice(6));
|
|
312
|
-
} else {
|
|
313
|
-
value = getByPath(responseBody, path);
|
|
314
|
-
}
|
|
315
|
-
if (value !== undefined) {
|
|
316
|
-
captures[rule.capture] = value;
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
if (headerRules && responseHeaders) {
|
|
323
|
-
for (const [key, rule] of Object.entries(headerRules)) {
|
|
324
|
-
if (typeof rule !== "string" && rule.capture) {
|
|
325
|
-
const value = responseHeaders[key.toLowerCase()];
|
|
326
|
-
if (value !== undefined) {
|
|
327
|
-
captures[rule.capture] = value;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
return captures;
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
/**
|
|
337
|
-
* Find capture rules whose path didn't resolve in the response, returning
|
|
338
|
-
* `{ var, source, path }` per miss. Surfaced as auxiliary assertion failures
|
|
339
|
-
* so the user sees "captures: {}" with a reason instead of silent emptiness —
|
|
340
|
-
* the empty-captures pitfall is the #1 footgun in CRUD chains because the
|
|
341
|
-
* downstream step is silently skipped or runs with `undefined`. TASK-256.
|
|
342
|
-
*/
|
|
343
|
-
export function findMissedCaptures(
|
|
344
|
-
bodyRules: Record<string, AssertionRule> | undefined,
|
|
345
|
-
responseBody: unknown,
|
|
346
|
-
headerRules?: Record<string, string | AssertionRule>,
|
|
347
|
-
responseHeaders?: Record<string, string>,
|
|
348
|
-
): Array<{ var: string; source: "body" | "header"; path: string }> {
|
|
349
|
-
const misses: Array<{ var: string; source: "body" | "header"; path: string }> = [];
|
|
350
|
-
|
|
351
|
-
if (bodyRules) {
|
|
352
|
-
for (const [path, rule] of Object.entries(bodyRules)) {
|
|
353
|
-
if (!rule.capture) continue;
|
|
354
|
-
if (responseBody === undefined) {
|
|
355
|
-
misses.push({ var: rule.capture, source: "body", path });
|
|
356
|
-
continue;
|
|
357
|
-
}
|
|
358
|
-
let value: unknown;
|
|
359
|
-
if (path === "_body") {
|
|
360
|
-
value = responseBody;
|
|
361
|
-
} else if (path.startsWith("_body.")) {
|
|
362
|
-
value = getByPath(responseBody, path.slice(6));
|
|
363
|
-
} else {
|
|
364
|
-
value = getByPath(responseBody, path);
|
|
365
|
-
}
|
|
366
|
-
if (value === undefined) {
|
|
367
|
-
misses.push({ var: rule.capture, source: "body", path });
|
|
368
|
-
}
|
|
369
|
-
}
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (headerRules) {
|
|
373
|
-
for (const [key, rule] of Object.entries(headerRules)) {
|
|
374
|
-
if (typeof rule === "string" || !rule.capture) continue;
|
|
375
|
-
const value = responseHeaders ? responseHeaders[key.toLowerCase()] : undefined;
|
|
376
|
-
if (value === undefined) {
|
|
377
|
-
misses.push({ var: rule.capture, source: "header", path: key });
|
|
378
|
-
}
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
|
|
382
|
-
return misses;
|
|
383
|
-
}
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* ARV-8 (m-15): bounded async-pool for `zond checks run --workers N`.
|
|
3
|
-
*
|
|
4
|
-
* Cooperative concurrency on a single Bun event loop — no threading,
|
|
5
|
-
* no Workers, just N coroutines that pull from a shared cursor. The
|
|
6
|
-
* design choice is deliberate:
|
|
7
|
-
*
|
|
8
|
-
* * Threads/Workers would need request-context plumbing (auth headers,
|
|
9
|
-
* rate-limiter state, per-run schema validators are not transferable
|
|
10
|
-
* without serialization), and Bun's HTTP client is already
|
|
11
|
-
* non-blocking — so cooperative concurrency is faster *and* simpler.
|
|
12
|
-
* * `Promise.all(items.map(fn))` would saturate at items.length, which
|
|
13
|
-
* defeats the rate-limiter and tends to drown small mock servers.
|
|
14
|
-
*
|
|
15
|
-
* `runPool` preserves input order in the result array — callers (the
|
|
16
|
-
* checks runner, mainly) want to merge per-op findings deterministically
|
|
17
|
-
* regardless of which worker finished first.
|
|
18
|
-
*/
|
|
19
|
-
import os from "node:os";
|
|
20
|
-
|
|
21
|
-
const WORKERS_MIN = 1;
|
|
22
|
-
const WORKERS_MAX = 64;
|
|
23
|
-
/** `--workers auto` ceiling — beyond ~8 the gains on a typical mock
|
|
24
|
-
* server are dominated by network/IO contention, not parallelism. */
|
|
25
|
-
const WORKERS_AUTO_CEILING = 8;
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Run `fn` over `items` with at most `workers` in-flight at once.
|
|
29
|
-
*
|
|
30
|
-
* Results are returned in input order, *not* completion order — keep
|
|
31
|
-
* call-sites that compose findings/snapshots deterministic.
|
|
32
|
-
*
|
|
33
|
-
* Errors propagate: the first rejection cancels remaining workers'
|
|
34
|
-
* dispatch (they finish their in-flight task and exit). Caller should
|
|
35
|
-
* `try/catch` if it wants partial results — none of zond's callers do
|
|
36
|
-
* today (a runner crash is fatal anyway).
|
|
37
|
-
*/
|
|
38
|
-
export async function runPool<T, R>(
|
|
39
|
-
items: readonly T[],
|
|
40
|
-
workers: number,
|
|
41
|
-
fn: (item: T, index: number) => Promise<R>,
|
|
42
|
-
): Promise<R[]> {
|
|
43
|
-
if (items.length === 0) return [];
|
|
44
|
-
// Effective worker count is clamped both ways: never more than items
|
|
45
|
-
// (idle workers waste a closure), never below 1 (else nothing runs).
|
|
46
|
-
const effective = Math.max(1, Math.min(workers, items.length));
|
|
47
|
-
// Sequential fast-path — preserves the *exact* old behaviour
|
|
48
|
-
// (microtask ordering, error timing) for AC #4 backward-compat.
|
|
49
|
-
if (effective === 1) {
|
|
50
|
-
const out: R[] = new Array(items.length);
|
|
51
|
-
for (let i = 0; i < items.length; i++) {
|
|
52
|
-
out[i] = await fn(items[i]!, i);
|
|
53
|
-
}
|
|
54
|
-
return out;
|
|
55
|
-
}
|
|
56
|
-
const out: R[] = new Array(items.length);
|
|
57
|
-
let cursor = 0;
|
|
58
|
-
let aborted: unknown = null;
|
|
59
|
-
async function worker(): Promise<void> {
|
|
60
|
-
while (true) {
|
|
61
|
-
if (aborted !== null) return;
|
|
62
|
-
const i = cursor++;
|
|
63
|
-
if (i >= items.length) return;
|
|
64
|
-
try {
|
|
65
|
-
out[i] = await fn(items[i]!, i);
|
|
66
|
-
} catch (err) {
|
|
67
|
-
// First rejection wins — store it, drain in-flight tasks, then
|
|
68
|
-
// re-throw at the join point.
|
|
69
|
-
if (aborted === null) aborted = err;
|
|
70
|
-
return;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
const swarm: Promise<void>[] = [];
|
|
75
|
-
for (let w = 0; w < effective; w++) swarm.push(worker());
|
|
76
|
-
await Promise.all(swarm);
|
|
77
|
-
if (aborted !== null) throw aborted;
|
|
78
|
-
return out;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Parse a `--workers` flag value:
|
|
83
|
-
*
|
|
84
|
-
* undefined → 1 (backward-compat default — AC #4)
|
|
85
|
-
* "auto" / "AUTO"→ min(cpus, WORKERS_AUTO_CEILING) (AC #5)
|
|
86
|
-
* numeric → clamp [1, 64] (AC #5)
|
|
87
|
-
* anything else → throws (caller maps to a friendly CLI error)
|
|
88
|
-
*/
|
|
89
|
-
export function parseWorkers(value: string | number | undefined): number {
|
|
90
|
-
if (value === undefined || value === null || value === "") return 1;
|
|
91
|
-
if (typeof value === "string") {
|
|
92
|
-
const trimmed = value.trim().toLowerCase();
|
|
93
|
-
if (trimmed === "auto") {
|
|
94
|
-
return Math.max(WORKERS_MIN, Math.min(os.cpus().length, WORKERS_AUTO_CEILING));
|
|
95
|
-
}
|
|
96
|
-
const n = Number.parseInt(trimmed, 10);
|
|
97
|
-
if (!Number.isFinite(n)) throw new Error(`Invalid --workers value: "${value}"`);
|
|
98
|
-
return Math.max(WORKERS_MIN, Math.min(n, WORKERS_MAX));
|
|
99
|
-
}
|
|
100
|
-
if (!Number.isFinite(value)) throw new Error(`Invalid --workers value: ${value}`);
|
|
101
|
-
return Math.max(WORKERS_MIN, Math.min(Math.trunc(value), WORKERS_MAX));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
export const WORKERS_LIMITS = {
|
|
105
|
-
min: WORKERS_MIN,
|
|
106
|
-
max: WORKERS_MAX,
|
|
107
|
-
autoCeiling: WORKERS_AUTO_CEILING,
|
|
108
|
-
} as const;
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Heuristic for "auth-shaped" endpoint paths used by `--safe` mode and
|
|
3
|
-
* the env-symptom diagnostics in `db-analysis.ts`. Anything matching is
|
|
4
|
-
* treated as a login/refresh route — `--safe` whitelists it for live
|
|
5
|
-
* runs, and the diagnostics flag concentrated POST failures here as
|
|
6
|
-
* `auth_required` rather than per-endpoint bugs.
|
|
7
|
-
*/
|
|
8
|
-
export const AUTH_PATH_RE = /\/(auth|login|signin|token|oauth)\b/i;
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TASK-116: detect CI context (commit sha, branch, trigger) from common
|
|
3
|
-
* environment variables. Returns `null` for fields when nothing is present
|
|
4
|
-
* — caller decides whether to default `trigger` to `"manual"`.
|
|
5
|
-
*
|
|
6
|
-
* Supported providers (autodetected — no opt-in required):
|
|
7
|
-
* GitHub Actions GITHUB_ACTIONS=true, GITHUB_SHA, GITHUB_REF_NAME
|
|
8
|
-
* GitLab CI GITLAB_CI=true, CI_COMMIT_SHA, CI_COMMIT_REF_NAME
|
|
9
|
-
* CircleCI CIRCLECI=true, CIRCLE_SHA1, CIRCLE_BRANCH
|
|
10
|
-
* Buildkite BUILDKITE=true, BUILDKITE_COMMIT, BUILDKITE_BRANCH
|
|
11
|
-
* Jenkins JENKINS_URL set, GIT_COMMIT, BRANCH_NAME / GIT_BRANCH
|
|
12
|
-
* Generic CI=true triggers `trigger=ci` even when no provider
|
|
13
|
-
* matches — caller can still pass nullable commit/branch.
|
|
14
|
-
*
|
|
15
|
-
* Manual override is via the explicit `--commit-sha` / `--branch` /
|
|
16
|
-
* `--trigger` flags or the env vars `ZOND_COMMIT_SHA` / `ZOND_BRANCH` /
|
|
17
|
-
* `ZOND_TRIGGER`. These win over autodetection.
|
|
18
|
-
*/
|
|
19
|
-
export interface CiContext {
|
|
20
|
-
trigger: "ci" | "manual";
|
|
21
|
-
commit_sha: string | null;
|
|
22
|
-
branch: string | null;
|
|
23
|
-
/** Provider tag (github-actions / gitlab-ci / circleci / …) for diagnostics. */
|
|
24
|
-
provider: string | null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export function detectCiContext(env: NodeJS.ProcessEnv = process.env): CiContext {
|
|
28
|
-
const overrideCommit = env.ZOND_COMMIT_SHA?.trim() || null;
|
|
29
|
-
const overrideBranch = env.ZOND_BRANCH?.trim() || null;
|
|
30
|
-
const overrideTrigger = env.ZOND_TRIGGER?.trim() || null;
|
|
31
|
-
|
|
32
|
-
let provider: string | null = null;
|
|
33
|
-
let commit: string | null = null;
|
|
34
|
-
let branch: string | null = null;
|
|
35
|
-
|
|
36
|
-
if (env.GITHUB_ACTIONS === "true") {
|
|
37
|
-
provider = "github-actions";
|
|
38
|
-
commit = env.GITHUB_SHA?.trim() || null;
|
|
39
|
-
branch = env.GITHUB_REF_NAME?.trim() || env.GITHUB_HEAD_REF?.trim() || null;
|
|
40
|
-
} else if (env.GITLAB_CI === "true") {
|
|
41
|
-
provider = "gitlab-ci";
|
|
42
|
-
commit = env.CI_COMMIT_SHA?.trim() || null;
|
|
43
|
-
branch = env.CI_COMMIT_REF_NAME?.trim() || null;
|
|
44
|
-
} else if (env.CIRCLECI === "true") {
|
|
45
|
-
provider = "circleci";
|
|
46
|
-
commit = env.CIRCLE_SHA1?.trim() || null;
|
|
47
|
-
branch = env.CIRCLE_BRANCH?.trim() || null;
|
|
48
|
-
} else if (env.BUILDKITE === "true") {
|
|
49
|
-
provider = "buildkite";
|
|
50
|
-
commit = env.BUILDKITE_COMMIT?.trim() || null;
|
|
51
|
-
branch = env.BUILDKITE_BRANCH?.trim() || null;
|
|
52
|
-
} else if (env.JENKINS_URL) {
|
|
53
|
-
provider = "jenkins";
|
|
54
|
-
commit = env.GIT_COMMIT?.trim() || null;
|
|
55
|
-
branch = env.BRANCH_NAME?.trim() || env.GIT_BRANCH?.trim() || null;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const inferredCi = !!provider || env.CI === "true" || env.CI === "1";
|
|
59
|
-
const trigger: "ci" | "manual" =
|
|
60
|
-
overrideTrigger === "ci" || overrideTrigger === "manual"
|
|
61
|
-
? overrideTrigger
|
|
62
|
-
: inferredCi
|
|
63
|
-
? "ci"
|
|
64
|
-
: "manual";
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
trigger,
|
|
68
|
-
commit_sha: overrideCommit ?? commit,
|
|
69
|
-
branch: overrideBranch ?? branch,
|
|
70
|
-
provider: provider ?? (inferredCi ? "generic" : null),
|
|
71
|
-
};
|
|
72
|
-
}
|