@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,652 +0,0 @@
|
|
|
1
|
-
import { resolve, dirname, basename } from "node:path";
|
|
2
|
-
import type { TestSuite, TestStep, Environment, SourceMetadata, AssertionRule } from "../parser/types.ts";
|
|
3
|
-
import { substituteString, substituteStep, substituteDeep, extractVariableReferences } from "../parser/variables.ts";
|
|
4
|
-
import type { TestRunResult, StepResult, HttpRequest, AssertionResult } from "./types.ts";
|
|
5
|
-
import { executeRequest, type FetchOptions } from "./http-client.ts";
|
|
6
|
-
import type { RateLimiter } from "./rate-limiter.ts";
|
|
7
|
-
import { checkAssertions, extractCaptures, findMissedCaptures } from "./assertions.ts";
|
|
8
|
-
import { evaluateExpr } from "./expr-eval.ts";
|
|
9
|
-
import { applyTransform } from "./transforms.ts";
|
|
10
|
-
import type { SchemaValidator } from "./schema-validator.ts";
|
|
11
|
-
import { classifyFailure } from "../diagnostics/failure-class.ts";
|
|
12
|
-
import { buildUrl } from "../util/url.ts";
|
|
13
|
-
|
|
14
|
-
/** Shallow-merge suite-level и step-level provenance. Step перекрывает suite. */
|
|
15
|
-
function mergeProvenance(
|
|
16
|
-
suiteSrc?: SourceMetadata,
|
|
17
|
-
stepSrc?: SourceMetadata,
|
|
18
|
-
): SourceMetadata | null {
|
|
19
|
-
if (!suiteSrc && !stepSrc) return null;
|
|
20
|
-
return { ...(suiteSrc ?? {}), ...(stepSrc ?? {}) };
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/** ARV-157: build the top-level `schema_validation` summary for a step that
|
|
24
|
-
* was run with `--validate-schema`. Mirrors the shape `zond request
|
|
25
|
-
* --validate-schema` already produces (see src/cli/commands/request.ts);
|
|
26
|
-
* consumers can `jq '.steps[] | .schema_validation'` instead of digging
|
|
27
|
-
* into `assertions[] | select(.kind=="schema")`.
|
|
28
|
-
*
|
|
29
|
-
* Returns undefined when the validator wasn't attached or the response had
|
|
30
|
-
* no parseable JSON body — same precondition as `assertions.push(...)` at
|
|
31
|
-
* the call site, so the summary is present iff schema actually ran. */
|
|
32
|
-
function buildSchemaValidationSummary(
|
|
33
|
-
validator: SchemaValidator,
|
|
34
|
-
method: string,
|
|
35
|
-
path: string,
|
|
36
|
-
status: number,
|
|
37
|
-
schemaAssertions: AssertionResult[],
|
|
38
|
-
): StepResult["schema_validation"] {
|
|
39
|
-
const ins = validator.inspect(method, path, status);
|
|
40
|
-
if (!ins.matchedEndpoint) {
|
|
41
|
-
return { result: "no-endpoint", matched_endpoint: null, matched_response_status: null, error_count: 0 };
|
|
42
|
-
}
|
|
43
|
-
if (!ins.hasJsonSchema) {
|
|
44
|
-
return {
|
|
45
|
-
result: "no-schema",
|
|
46
|
-
matched_endpoint: ins.matchedEndpoint,
|
|
47
|
-
matched_response_status: ins.matchedResponseStatus,
|
|
48
|
-
error_count: 0,
|
|
49
|
-
};
|
|
50
|
-
}
|
|
51
|
-
const failed = schemaAssertions.filter((a) => !a.passed).length;
|
|
52
|
-
return {
|
|
53
|
-
result: failed === 0 ? "PASS" : "FAIL",
|
|
54
|
-
matched_endpoint: ins.matchedEndpoint,
|
|
55
|
-
matched_response_status: ins.matchedResponseStatus,
|
|
56
|
-
error_count: failed,
|
|
57
|
-
};
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/** TASK-256: turn each missed-capture (path didn't resolve in response)
|
|
61
|
-
* into an auxiliary failed assertion. The step then fails loudly with
|
|
62
|
-
* "capture <var>: path '<path>' not found in body" instead of producing
|
|
63
|
-
* silent `captures: {}` that the user only notices when the next step in a
|
|
64
|
-
* CRUD chain skips with `Depends on missing capture`. */
|
|
65
|
-
function buildMissedCaptureAssertions(
|
|
66
|
-
misses: ReturnType<typeof findMissedCaptures>,
|
|
67
|
-
): AssertionResult[] {
|
|
68
|
-
return misses.map((m) => ({
|
|
69
|
-
field: `capture ${m.var}`,
|
|
70
|
-
rule: m.source === "body" ? "body-path-exists" : "header-exists",
|
|
71
|
-
passed: false,
|
|
72
|
-
actual: undefined,
|
|
73
|
-
expected: `${m.source} '${m.path}' present`,
|
|
74
|
-
kind: "auxiliary",
|
|
75
|
-
}));
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function collectChainCaptures(tests: TestStep[]): Set<string> {
|
|
79
|
-
const out = new Set<string>();
|
|
80
|
-
const visit = (rules: Record<string, AssertionRule> | undefined) => {
|
|
81
|
-
if (!rules) return;
|
|
82
|
-
for (const r of Object.values(rules)) {
|
|
83
|
-
if (r.capture) out.add(r.capture);
|
|
84
|
-
if (r.each) visit(r.each);
|
|
85
|
-
if (r.contains_item) visit(r.contains_item);
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
for (const step of tests) visit(step.expect?.body);
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
function emptyVarSkipReason(varName: string, chainCaptures: Set<string>): string {
|
|
93
|
-
return chainCaptures.has(varName)
|
|
94
|
-
? `chain capture {{${varName}}} unbound (upstream step did not run or did not capture it)`
|
|
95
|
-
: `required fixture {{${varName}}} is empty`;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
function makeSkippedResult(
|
|
99
|
-
stepName: string,
|
|
100
|
-
reason: string,
|
|
101
|
-
opts?: { cascade?: { missingCapture: string } },
|
|
102
|
-
): StepResult {
|
|
103
|
-
const result: StepResult = {
|
|
104
|
-
name: stepName,
|
|
105
|
-
status: "skip",
|
|
106
|
-
duration_ms: 0,
|
|
107
|
-
request: { method: "", url: "", headers: {} },
|
|
108
|
-
assertions: [],
|
|
109
|
-
captures: {},
|
|
110
|
-
error: reason,
|
|
111
|
-
};
|
|
112
|
-
if (opts?.cascade) {
|
|
113
|
-
result.failure_class = "cascade";
|
|
114
|
-
result.failure_class_reason = `Upstream capture not produced: ${opts.cascade.missingCapture}`;
|
|
115
|
-
}
|
|
116
|
-
return result;
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/** Interpolate {{var}} placeholders inside a test/step name. Falls back to
|
|
120
|
-
* the raw name string if substitution returns a non-string value. */
|
|
121
|
-
function interpolateName(name: string, vars: Record<string, unknown>): string {
|
|
122
|
-
try {
|
|
123
|
-
const out = substituteString(name, vars);
|
|
124
|
-
return typeof out === "string" ? out : String(out);
|
|
125
|
-
} catch {
|
|
126
|
-
return name;
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* Expand a `parameterize: { key: [val, ...] }` map into the cross-product of
|
|
132
|
-
* iteration variable bindings. No `parameterize` (or an empty map) yields a
|
|
133
|
-
* single empty iteration so the existing single-pass behaviour is preserved.
|
|
134
|
-
*
|
|
135
|
-
* Exported for tests.
|
|
136
|
-
*/
|
|
137
|
-
export function expandParameterize(params?: Record<string, unknown[]>): Record<string, unknown>[] {
|
|
138
|
-
if (!params) return [{}];
|
|
139
|
-
const keys = Object.keys(params).filter(k => Array.isArray(params[k]) && (params[k] as unknown[]).length > 0);
|
|
140
|
-
if (keys.length === 0) return [{}];
|
|
141
|
-
let combos: Record<string, unknown>[] = [{}];
|
|
142
|
-
for (const k of keys) {
|
|
143
|
-
const values = params[k] as unknown[];
|
|
144
|
-
const next: Record<string, unknown>[] = [];
|
|
145
|
-
for (const combo of combos) {
|
|
146
|
-
for (const v of values) {
|
|
147
|
-
next.push({ ...combo, [k]: v });
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
combos = next;
|
|
151
|
-
}
|
|
152
|
-
return combos;
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export interface RunSuiteOptions {
|
|
156
|
-
rateLimiter?: RateLimiter;
|
|
157
|
-
/** Optional OpenAPI response-schema validator. When provided, every step's
|
|
158
|
-
* parsed JSON body is validated against the matching schema; failures are
|
|
159
|
-
* appended to the step's `assertions`. */
|
|
160
|
-
schemaValidator?: SchemaValidator;
|
|
161
|
-
/** TASK-144: per-step network-retry budget used by http-client for
|
|
162
|
-
* ECONNRESET / EPIPE / `socket hang up` / `fetch failed` / abort cases.
|
|
163
|
-
* Set by `zond run --retry-on-network <N>`. HTTP statuses are not retried
|
|
164
|
-
* by this path. */
|
|
165
|
-
networkRetries?: number;
|
|
166
|
-
/** ARV-249: shared HTTP-request budget across all parallel suites. When
|
|
167
|
-
* `used >= limit`, remaining steps short-circuit to `skip` with reason
|
|
168
|
-
* `max-requests-cap-reached`. Each `retry_until` attempt counts as one
|
|
169
|
-
* request; dry-run and set-only steps do not consume the budget. */
|
|
170
|
-
requestBudget?: RequestBudget;
|
|
171
|
-
/** ARV-249: invoked after every step completes (pass/fail/skip/error) so
|
|
172
|
-
* the CLI can render a periodic progress line without each suite
|
|
173
|
-
* knowing how many siblings are running in parallel. */
|
|
174
|
-
onStepDone?: (step: StepResult) => void;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/** ARV-249: shared `--max-requests` budget. Mutated in-place by every
|
|
178
|
-
* parallel `runSuite` call — single-threaded JS makes the
|
|
179
|
-
* check-then-increment race-free. `limit === Infinity` means "uncapped"
|
|
180
|
-
* (the default). */
|
|
181
|
-
export interface RequestBudget {
|
|
182
|
-
limit: number;
|
|
183
|
-
used: number;
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
/** Try to reserve one HTTP slot from the shared budget. Returns true if
|
|
187
|
-
* the caller may proceed, false if the cap has been reached. */
|
|
188
|
-
export function reserveRequest(budget: RequestBudget | undefined): boolean {
|
|
189
|
-
if (!budget) return true;
|
|
190
|
-
if (budget.used >= budget.limit) return false;
|
|
191
|
-
budget.used += 1;
|
|
192
|
-
return true;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
export const MAX_REQUESTS_SKIP_REASON = "max-requests-cap-reached";
|
|
196
|
-
|
|
197
|
-
export async function runSuite(
|
|
198
|
-
suite: TestSuite,
|
|
199
|
-
env: Environment = {},
|
|
200
|
-
dryRun = false,
|
|
201
|
-
options: RunSuiteOptions = {},
|
|
202
|
-
): Promise<TestRunResult> {
|
|
203
|
-
const startedAt = new Date().toISOString();
|
|
204
|
-
const steps: StepResult[] = [];
|
|
205
|
-
|
|
206
|
-
/** Push a step result, attaching provenance + failure classification. */
|
|
207
|
-
const pushStep = (result: StepResult, currentStep?: TestStep): void => {
|
|
208
|
-
const merged = mergeProvenance(suite.source, currentStep?.source);
|
|
209
|
-
if (merged !== null) result.provenance = merged;
|
|
210
|
-
const classification = classifyFailure(result);
|
|
211
|
-
if (classification) {
|
|
212
|
-
result.failure_class = classification.failure_class;
|
|
213
|
-
result.failure_class_reason = classification.failure_class_reason;
|
|
214
|
-
}
|
|
215
|
-
steps.push(result);
|
|
216
|
-
if (options.onStepDone) options.onStepDone(result);
|
|
217
|
-
};
|
|
218
|
-
|
|
219
|
-
const fetchOptions: Partial<FetchOptions> = {
|
|
220
|
-
timeout: suite.config.timeout,
|
|
221
|
-
retries: suite.config.retries,
|
|
222
|
-
retry_delay: suite.config.retry_delay,
|
|
223
|
-
follow_redirects: suite.config.follow_redirects,
|
|
224
|
-
rate_limiter: options.rateLimiter,
|
|
225
|
-
...(options.networkRetries !== undefined ? { network_retries: options.networkRetries } : {}),
|
|
226
|
-
};
|
|
227
|
-
|
|
228
|
-
// Names of every variable a step in this suite tries to capture from a
|
|
229
|
-
// response (expect.body.<field>.capture: <name>). When a later step
|
|
230
|
-
// references one of these and the value is empty — under --dry-run, or
|
|
231
|
-
// because the capturing step was skipped — the missing var is a chain
|
|
232
|
-
// capture, NOT a fixture in .env.yaml. Distinguishing them in the skip
|
|
233
|
-
// message stops users from chasing fixture seeding for vars that
|
|
234
|
-
// shouldn't live in .env.yaml at all.
|
|
235
|
-
const chainCaptures = collectChainCaptures(suite.tests);
|
|
236
|
-
|
|
237
|
-
// parameterize cross-product → N iterations of the suite body.
|
|
238
|
-
// Captures and tainted/missing sets are reset per iteration so that
|
|
239
|
-
// values from one binding never leak into the next.
|
|
240
|
-
const iterations = expandParameterize(suite.parameterize);
|
|
241
|
-
|
|
242
|
-
for (const iterVars of iterations) {
|
|
243
|
-
const variables: Record<string, unknown> = { ...env, ...iterVars };
|
|
244
|
-
// Captures whose source step's assertions partially failed, but the value
|
|
245
|
-
// itself was extracted. Cleanup/always steps may still consume them.
|
|
246
|
-
const taintedCaptures = new Set<string>();
|
|
247
|
-
// Captures that were never extracted (response missing the field). Even
|
|
248
|
-
// always-steps can't run if their referenced capture is missing.
|
|
249
|
-
const missingCaptures = new Set<string>();
|
|
250
|
-
|
|
251
|
-
// Expand steps lazily (for_each needs current variables)
|
|
252
|
-
let stepIndex = 0;
|
|
253
|
-
const rawSteps = [...suite.tests];
|
|
254
|
-
|
|
255
|
-
while (stepIndex < rawSteps.length) {
|
|
256
|
-
const step = rawSteps[stepIndex]!;
|
|
257
|
-
stepIndex++;
|
|
258
|
-
|
|
259
|
-
// Expand for_each: insert expanded steps and skip current
|
|
260
|
-
if (step.for_each) {
|
|
261
|
-
const resolvedIn = substituteDeep(step.for_each.in, variables);
|
|
262
|
-
const items = Array.isArray(resolvedIn) ? resolvedIn : [];
|
|
263
|
-
const expanded: TestStep[] = [];
|
|
264
|
-
for (const item of items) {
|
|
265
|
-
const { for_each: _, ...rest } = step;
|
|
266
|
-
expanded.push({ ...rest, name: `${step.name} [${step.for_each.var}=${JSON.stringify(item)}]` } as TestStep);
|
|
267
|
-
// We'll inject the variable right before executing each expanded step
|
|
268
|
-
// Store the var assignment via a set field
|
|
269
|
-
}
|
|
270
|
-
// Insert expanded steps at current position
|
|
271
|
-
rawSteps.splice(stepIndex, 0, ...expanded);
|
|
272
|
-
// Set the for_each variable for each expanded step
|
|
273
|
-
for (let i = 0; i < items.length; i++) {
|
|
274
|
-
const expandedStep = rawSteps[stepIndex + i]!;
|
|
275
|
-
// Temporarily inject into variables when we reach this step
|
|
276
|
-
// We need a way to pass the variable — use a hidden _for_each_vars
|
|
277
|
-
(expandedStep as unknown as Record<string, unknown>).__for_each_var = { key: step.for_each.var, value: items[i] };
|
|
278
|
-
}
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
// Inject for_each variable if present
|
|
283
|
-
const forEachData = (step as unknown as Record<string, unknown>).__for_each_var as { key: string; value: unknown } | undefined;
|
|
284
|
-
if (forEachData) {
|
|
285
|
-
variables[forEachData.key] = forEachData.value;
|
|
286
|
-
delete (step as unknown as Record<string, unknown>).__for_each_var;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Handle set-only steps (no HTTP request)
|
|
290
|
-
if (step.set && step.path === "") {
|
|
291
|
-
for (const [key, rawDirective] of Object.entries(step.set)) {
|
|
292
|
-
const substituted = substituteDeep(rawDirective, variables);
|
|
293
|
-
variables[key] = applyTransform(substituted);
|
|
294
|
-
}
|
|
295
|
-
pushStep({
|
|
296
|
-
name: interpolateName(step.name, variables),
|
|
297
|
-
status: "pass",
|
|
298
|
-
duration_ms: 0,
|
|
299
|
-
request: { method: "", url: "", headers: {} },
|
|
300
|
-
assertions: [],
|
|
301
|
-
captures: {},
|
|
302
|
-
}, step);
|
|
303
|
-
continue;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
// Skip check: if step references a failed capture, skip — unless
|
|
307
|
-
// step is `always: true` AND the capture is just tainted (still extracted).
|
|
308
|
-
const referencedVars = extractVariableReferences(step);
|
|
309
|
-
const missing = referencedVars.find((v) => missingCaptures.has(v));
|
|
310
|
-
if (missing) {
|
|
311
|
-
pushStep(
|
|
312
|
-
makeSkippedResult(
|
|
313
|
-
interpolateName(step.name, variables),
|
|
314
|
-
`Depends on missing capture: ${missing}`,
|
|
315
|
-
{ cascade: { missingCapture: missing } },
|
|
316
|
-
),
|
|
317
|
-
step,
|
|
318
|
-
);
|
|
319
|
-
continue;
|
|
320
|
-
}
|
|
321
|
-
if (!step.always) {
|
|
322
|
-
const tainted = referencedVars.find((v) => taintedCaptures.has(v));
|
|
323
|
-
if (tainted) {
|
|
324
|
-
pushStep(
|
|
325
|
-
makeSkippedResult(
|
|
326
|
-
interpolateName(step.name, variables),
|
|
327
|
-
`Depends on tainted capture: ${tainted} (use always: true on cleanup steps)`,
|
|
328
|
-
{ cascade: { missingCapture: tainted } },
|
|
329
|
-
),
|
|
330
|
-
step,
|
|
331
|
-
);
|
|
332
|
-
continue;
|
|
333
|
-
}
|
|
334
|
-
}
|
|
335
|
-
|
|
336
|
-
// skip_if evaluation
|
|
337
|
-
if (step.skip_if) {
|
|
338
|
-
const exprAfterSubst = String(substituteString(step.skip_if, variables));
|
|
339
|
-
if (evaluateExpr(exprAfterSubst)) {
|
|
340
|
-
const varMatch = step.skip_if.match(/\{\{([^{}]+)\}\}/);
|
|
341
|
-
const skipMsg = varMatch
|
|
342
|
-
? emptyVarSkipReason(varMatch[1]!.trim(), chainCaptures)
|
|
343
|
-
: step.skip_if;
|
|
344
|
-
pushStep(makeSkippedResult(interpolateName(step.name, variables), skipMsg), step);
|
|
345
|
-
continue;
|
|
346
|
-
}
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
// Process set: on HTTP steps — evaluate generators once before building request.
|
|
350
|
-
// Substitution can throw on unknown {{$generator}} — fail this step, not the suite.
|
|
351
|
-
let resolved: TestStep;
|
|
352
|
-
let resolvedBaseUrl: string | undefined;
|
|
353
|
-
let resolvedSuiteHeaders: Record<string, string> | undefined;
|
|
354
|
-
try {
|
|
355
|
-
if (step.set) {
|
|
356
|
-
for (const [key, rawDirective] of Object.entries(step.set)) {
|
|
357
|
-
const substituted = substituteDeep(rawDirective, variables);
|
|
358
|
-
variables[key] = applyTransform(substituted);
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
resolved = substituteStep(step, variables);
|
|
362
|
-
resolvedBaseUrl = suite.base_url ? substituteString(suite.base_url, variables) as string : undefined;
|
|
363
|
-
resolvedSuiteHeaders = suite.headers ? substituteDeep(suite.headers, variables) : undefined;
|
|
364
|
-
} catch (err) {
|
|
365
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
366
|
-
pushStep({
|
|
367
|
-
name: interpolateName(step.name, variables),
|
|
368
|
-
status: "error",
|
|
369
|
-
duration_ms: 0,
|
|
370
|
-
request: { method: step.method, url: step.path, headers: {} },
|
|
371
|
-
assertions: [],
|
|
372
|
-
captures: {},
|
|
373
|
-
error: errorMsg,
|
|
374
|
-
}, step);
|
|
375
|
-
// Substitution never produced a request → capture truly missing.
|
|
376
|
-
if (step.expect.body) {
|
|
377
|
-
for (const rule of Object.values(step.expect.body)) {
|
|
378
|
-
if (rule.capture) missingCaptures.add(rule.capture);
|
|
379
|
-
}
|
|
380
|
-
}
|
|
381
|
-
continue;
|
|
382
|
-
}
|
|
383
|
-
// Skip if any path-variable in the template resolved to empty — an empty
|
|
384
|
-
// path segment produces URLs like /repos//commits/ which always 404/500.
|
|
385
|
-
// The explicit skip_if guard only covers the first param (TASK-237);
|
|
386
|
-
// this catches all others.
|
|
387
|
-
{
|
|
388
|
-
let emptyVar: string | null = null;
|
|
389
|
-
for (const m of step.path.matchAll(/\{\{([^{}]+)\}\}/g)) {
|
|
390
|
-
const varName = m[1]!.trim();
|
|
391
|
-
const val = variables[varName];
|
|
392
|
-
if (val === "" || val === null || val === undefined) { emptyVar = varName; break; }
|
|
393
|
-
}
|
|
394
|
-
if (emptyVar) {
|
|
395
|
-
pushStep(makeSkippedResult(
|
|
396
|
-
interpolateName(step.name, variables),
|
|
397
|
-
emptyVarSkipReason(emptyVar, chainCaptures),
|
|
398
|
-
), step);
|
|
399
|
-
continue;
|
|
400
|
-
}
|
|
401
|
-
}
|
|
402
|
-
const url = buildUrl(resolvedBaseUrl, resolved.path, resolved.query);
|
|
403
|
-
const headers: Record<string, string> = { ...resolvedSuiteHeaders, ...resolved.headers };
|
|
404
|
-
let body: string | undefined;
|
|
405
|
-
let formData: FormData | undefined;
|
|
406
|
-
|
|
407
|
-
if (resolved.json !== undefined) {
|
|
408
|
-
body = JSON.stringify(resolved.json);
|
|
409
|
-
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
410
|
-
headers["Content-Type"] = "application/json";
|
|
411
|
-
}
|
|
412
|
-
} else if (resolved.form) {
|
|
413
|
-
body = new URLSearchParams(resolved.form).toString();
|
|
414
|
-
if (!headers["Content-Type"] && !headers["content-type"]) {
|
|
415
|
-
headers["Content-Type"] = "application/x-www-form-urlencoded";
|
|
416
|
-
}
|
|
417
|
-
} else if (resolved.multipart) {
|
|
418
|
-
const basedir = suite.filePath ? dirname(suite.filePath) : process.cwd();
|
|
419
|
-
formData = new FormData();
|
|
420
|
-
for (const [key, field] of Object.entries(resolved.multipart)) {
|
|
421
|
-
if (typeof field === "string") {
|
|
422
|
-
formData.append(key, field);
|
|
423
|
-
} else {
|
|
424
|
-
const absPath = resolve(basedir, field.file);
|
|
425
|
-
const buf = await Bun.file(absPath).arrayBuffer();
|
|
426
|
-
const mime = field.content_type ?? "application/octet-stream";
|
|
427
|
-
const filename = field.filename ?? basename(absPath);
|
|
428
|
-
formData.append(key, new Blob([buf], { type: mime }), filename);
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
const request: HttpRequest = { method: resolved.method, url, headers, body, formData };
|
|
434
|
-
|
|
435
|
-
// Validate absolute URL before attempting fetch
|
|
436
|
-
if (!url.startsWith("http://") && !url.startsWith("https://")) {
|
|
437
|
-
pushStep({
|
|
438
|
-
name: interpolateName(step.name, variables),
|
|
439
|
-
status: "error",
|
|
440
|
-
duration_ms: 0,
|
|
441
|
-
request,
|
|
442
|
-
assertions: [],
|
|
443
|
-
captures: {},
|
|
444
|
-
error: `base_url is not configured — URL resolved to a relative path: "${url}". Set base_url in .env.yaml`,
|
|
445
|
-
}, step);
|
|
446
|
-
if (step.expect.body) {
|
|
447
|
-
for (const rule of Object.values(step.expect.body)) {
|
|
448
|
-
if (rule.capture) missingCaptures.add(rule.capture);
|
|
449
|
-
}
|
|
450
|
-
}
|
|
451
|
-
continue;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
if (dryRun) {
|
|
455
|
-
const bodyPreview = formData
|
|
456
|
-
? ` [multipart: ${[...formData.keys()].length} field(s)]`
|
|
457
|
-
: body ? ` ${body.slice(0, 200)}` : "";
|
|
458
|
-
pushStep({
|
|
459
|
-
name: interpolateName(step.name, variables),
|
|
460
|
-
status: "pass",
|
|
461
|
-
duration_ms: 0,
|
|
462
|
-
request,
|
|
463
|
-
assertions: [],
|
|
464
|
-
captures: {},
|
|
465
|
-
error: `[DRY RUN] ${resolved.method} ${url}${bodyPreview}`,
|
|
466
|
-
}, step);
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
// retry_until wrapper
|
|
471
|
-
if (step.retry_until) {
|
|
472
|
-
const rt = step.retry_until;
|
|
473
|
-
let lastStepResult: StepResult | undefined;
|
|
474
|
-
for (let attempt = 0; attempt < rt.max_attempts; attempt++) {
|
|
475
|
-
if (!reserveRequest(options.requestBudget)) {
|
|
476
|
-
lastStepResult = makeSkippedResult(
|
|
477
|
-
interpolateName(step.name, variables),
|
|
478
|
-
MAX_REQUESTS_SKIP_REASON,
|
|
479
|
-
);
|
|
480
|
-
break;
|
|
481
|
-
}
|
|
482
|
-
try {
|
|
483
|
-
const response = await executeRequest(request, fetchOptions);
|
|
484
|
-
const captures = extractCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
|
|
485
|
-
const missedCaps = findMissedCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
|
|
486
|
-
const assertions = checkAssertions(resolved.expect, response);
|
|
487
|
-
assertions.push(...buildMissedCaptureAssertions(missedCaps));
|
|
488
|
-
let schemaValidationSummary: StepResult["schema_validation"] | undefined;
|
|
489
|
-
if (options.schemaValidator && response.body_parsed !== undefined) {
|
|
490
|
-
const schemaAssertions = options.schemaValidator.validate(resolved.method, resolved.path, response.status, response.body_parsed);
|
|
491
|
-
assertions.push(...schemaAssertions);
|
|
492
|
-
schemaValidationSummary = buildSchemaValidationSummary(
|
|
493
|
-
options.schemaValidator,
|
|
494
|
-
resolved.method,
|
|
495
|
-
resolved.path,
|
|
496
|
-
response.status,
|
|
497
|
-
schemaAssertions,
|
|
498
|
-
);
|
|
499
|
-
}
|
|
500
|
-
const allPassed = assertions.every((a) => a.passed);
|
|
501
|
-
|
|
502
|
-
lastStepResult = {
|
|
503
|
-
name: interpolateName(step.name, variables),
|
|
504
|
-
status: allPassed ? "pass" : "fail",
|
|
505
|
-
duration_ms: response.duration_ms,
|
|
506
|
-
request,
|
|
507
|
-
response,
|
|
508
|
-
assertions,
|
|
509
|
-
captures,
|
|
510
|
-
...(response.network_retry_count && response.network_retry_count > 0
|
|
511
|
-
? { network_retry: response.network_retry_count }
|
|
512
|
-
: {}),
|
|
513
|
-
...(schemaValidationSummary ? { schema_validation: schemaValidationSummary } : {}),
|
|
514
|
-
};
|
|
515
|
-
|
|
516
|
-
// Evaluate condition with response context
|
|
517
|
-
const condVars: Record<string, unknown> = { ...variables, ...captures, status: response.status };
|
|
518
|
-
if (response.body_parsed && typeof response.body_parsed === "object") {
|
|
519
|
-
for (const [k, v] of Object.entries(response.body_parsed as Record<string, unknown>)) {
|
|
520
|
-
condVars[k] = v;
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
const condStr = String(substituteString(rt.condition, condVars));
|
|
524
|
-
if (evaluateExpr(condStr)) {
|
|
525
|
-
Object.assign(variables, captures);
|
|
526
|
-
break;
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
if (attempt < rt.max_attempts - 1) {
|
|
530
|
-
await new Promise((resolve) => setTimeout(resolve, rt.delay_ms));
|
|
531
|
-
}
|
|
532
|
-
} catch (err) {
|
|
533
|
-
lastStepResult = {
|
|
534
|
-
name: interpolateName(step.name, variables),
|
|
535
|
-
status: "error",
|
|
536
|
-
duration_ms: 0,
|
|
537
|
-
request,
|
|
538
|
-
assertions: [],
|
|
539
|
-
captures: {},
|
|
540
|
-
error: err instanceof Error ? err.message : String(err),
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
}
|
|
544
|
-
if (lastStepResult) pushStep(lastStepResult, step);
|
|
545
|
-
continue;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (!reserveRequest(options.requestBudget)) {
|
|
549
|
-
pushStep(makeSkippedResult(
|
|
550
|
-
interpolateName(step.name, variables),
|
|
551
|
-
MAX_REQUESTS_SKIP_REASON,
|
|
552
|
-
), step);
|
|
553
|
-
continue;
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
try {
|
|
557
|
-
const response = await executeRequest(request, fetchOptions);
|
|
558
|
-
|
|
559
|
-
// Extract captures (body + header)
|
|
560
|
-
const captures = extractCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
|
|
561
|
-
Object.assign(variables, captures);
|
|
562
|
-
|
|
563
|
-
// Track expected captures that weren't obtained — these are missing.
|
|
564
|
-
if (resolved.expect.body) {
|
|
565
|
-
for (const rule of Object.values(resolved.expect.body)) {
|
|
566
|
-
if (rule.capture && !(rule.capture in captures)) {
|
|
567
|
-
missingCaptures.add(rule.capture);
|
|
568
|
-
}
|
|
569
|
-
}
|
|
570
|
-
}
|
|
571
|
-
|
|
572
|
-
// Run assertions
|
|
573
|
-
const missedCaps = findMissedCaptures(resolved.expect.body, response.body_parsed, resolved.expect.headers, response.headers);
|
|
574
|
-
const assertions = checkAssertions(resolved.expect, response);
|
|
575
|
-
assertions.push(...buildMissedCaptureAssertions(missedCaps));
|
|
576
|
-
let schemaValidationSummary: StepResult["schema_validation"] | undefined;
|
|
577
|
-
if (options.schemaValidator && response.body_parsed !== undefined) {
|
|
578
|
-
const schemaAssertions = options.schemaValidator.validate(resolved.method, resolved.path, response.status, response.body_parsed);
|
|
579
|
-
assertions.push(...schemaAssertions);
|
|
580
|
-
schemaValidationSummary = buildSchemaValidationSummary(
|
|
581
|
-
options.schemaValidator,
|
|
582
|
-
resolved.method,
|
|
583
|
-
resolved.path,
|
|
584
|
-
response.status,
|
|
585
|
-
schemaAssertions,
|
|
586
|
-
);
|
|
587
|
-
}
|
|
588
|
-
const allPassed = assertions.every((a) => a.passed);
|
|
589
|
-
|
|
590
|
-
pushStep({
|
|
591
|
-
name: interpolateName(step.name, variables),
|
|
592
|
-
status: allPassed ? "pass" : "fail",
|
|
593
|
-
duration_ms: response.duration_ms,
|
|
594
|
-
request,
|
|
595
|
-
response,
|
|
596
|
-
assertions,
|
|
597
|
-
captures,
|
|
598
|
-
...(response.network_retry_count && response.network_retry_count > 0
|
|
599
|
-
? { network_retry: response.network_retry_count }
|
|
600
|
-
: {}),
|
|
601
|
-
...(schemaValidationSummary ? { schema_validation: schemaValidationSummary } : {}),
|
|
602
|
-
}, step);
|
|
603
|
-
|
|
604
|
-
// If step failed, captures that did extract are tainted (value is real
|
|
605
|
-
// but came from a step whose other assertions failed). Always-steps may
|
|
606
|
-
// still consume them; non-always steps cascade-skip.
|
|
607
|
-
if (!allPassed && resolved.expect.body) {
|
|
608
|
-
for (const rule of Object.values(resolved.expect.body)) {
|
|
609
|
-
if (rule.capture && rule.capture in captures) {
|
|
610
|
-
taintedCaptures.add(rule.capture);
|
|
611
|
-
}
|
|
612
|
-
}
|
|
613
|
-
}
|
|
614
|
-
} catch (err) {
|
|
615
|
-
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
616
|
-
pushStep({
|
|
617
|
-
name: interpolateName(step.name, variables),
|
|
618
|
-
status: "error",
|
|
619
|
-
duration_ms: 0,
|
|
620
|
-
request,
|
|
621
|
-
assertions: [],
|
|
622
|
-
captures: {},
|
|
623
|
-
error: errorMsg,
|
|
624
|
-
}, step);
|
|
625
|
-
|
|
626
|
-
// Network/runtime error → no response → capture truly missing.
|
|
627
|
-
if (step.expect.body) {
|
|
628
|
-
for (const rule of Object.values(step.expect.body)) {
|
|
629
|
-
if (rule.capture) missingCaptures.add(rule.capture);
|
|
630
|
-
}
|
|
631
|
-
}
|
|
632
|
-
}
|
|
633
|
-
}
|
|
634
|
-
} // end of parameterize iteration loop
|
|
635
|
-
|
|
636
|
-
const finishedAt = new Date().toISOString();
|
|
637
|
-
return {
|
|
638
|
-
suite_name: suite.name,
|
|
639
|
-
suite_tags: suite.tags,
|
|
640
|
-
suite_description: suite.description,
|
|
641
|
-
suite_file: suite.filePath,
|
|
642
|
-
started_at: startedAt,
|
|
643
|
-
finished_at: finishedAt,
|
|
644
|
-
total: steps.length,
|
|
645
|
-
passed: steps.filter((s) => s.status === "pass").length,
|
|
646
|
-
failed: steps.filter((s) => s.status === "fail").length,
|
|
647
|
-
skipped: steps.filter((s) => s.status === "skip").length,
|
|
648
|
-
// ARV-318: surface error steps so total = passed+failed+skipped+errored.
|
|
649
|
-
errored: steps.filter((s) => s.status === "error").length,
|
|
650
|
-
steps,
|
|
651
|
-
};
|
|
652
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
const OPERATORS = ["!=", "==", ">=", "<=", ">", "<"] as const;
|
|
2
|
-
|
|
3
|
-
export function evaluateExpr(expr: string): boolean {
|
|
4
|
-
const trimmed = expr.trim();
|
|
5
|
-
if (trimmed === "") return false;
|
|
6
|
-
|
|
7
|
-
for (const op of OPERATORS) {
|
|
8
|
-
const idx = trimmed.indexOf(op);
|
|
9
|
-
if (idx !== -1) {
|
|
10
|
-
const left = trimmed.slice(0, idx).trim();
|
|
11
|
-
const right = trimmed.slice(idx + op.length).trim();
|
|
12
|
-
return compareValues(left, right, op);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
// No operator — truthiness
|
|
17
|
-
return isTruthy(trimmed);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
function compareValues(left: string, right: string, op: string): boolean {
|
|
21
|
-
const lNum = Number(left);
|
|
22
|
-
const rNum = Number(right);
|
|
23
|
-
const numeric = !isNaN(lNum) && !isNaN(rNum) && left !== "" && right !== "";
|
|
24
|
-
|
|
25
|
-
switch (op) {
|
|
26
|
-
case "==": return numeric ? lNum === rNum : left === right;
|
|
27
|
-
case "!=": return numeric ? lNum !== rNum : left !== right;
|
|
28
|
-
case ">": return numeric ? lNum > rNum : left > right;
|
|
29
|
-
case "<": return numeric ? lNum < rNum : left < right;
|
|
30
|
-
case ">=": return numeric ? lNum >= rNum : left >= right;
|
|
31
|
-
case "<=": return numeric ? lNum <= rNum : left <= right;
|
|
32
|
-
default: return false;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function isTruthy(value: string): boolean {
|
|
37
|
-
if (value === "" || value === "0" || value === "false" || value === "null" || value === "undefined") {
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
return true;
|
|
41
|
-
}
|