@kirrosh/zond 0.21.0 → 0.23.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 +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -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 +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -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 +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -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 +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -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 +61 -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 +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -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 +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -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 +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -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 +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- 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 +62 -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 +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -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 +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -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 +128 -0
- package/src/db/queries/runs.ts +235 -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 +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { TestRunResult, StepResult } from "../runner/types.ts";
|
|
2
|
+
import { type Exporter, runExporter } from "../exporter/exporter.ts";
|
|
2
3
|
import type { Reporter, ReporterOptions } from "./types.ts";
|
|
3
4
|
|
|
4
5
|
function escapeXml(str: string): string {
|
|
@@ -55,20 +56,34 @@ function renderTestsuite(result: TestRunResult): string {
|
|
|
55
56
|
return ` <testsuite name="${name}" tests="${tests}" failures="${failures}" errors="${errors}" skipped="${skipped}" time="${time}">\n${testcases}\n </testsuite>`;
|
|
56
57
|
}
|
|
57
58
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const junitExporter: Exporter<TestRunResult[]> = {
|
|
60
|
+
name: "junit",
|
|
61
|
+
mime: "application/xml",
|
|
62
|
+
render(results: TestRunResult[]): string {
|
|
63
|
+
const totalTests = results.reduce((s, r) => s + r.total, 0);
|
|
64
|
+
const totalFailures = results.reduce((s, r) => s + r.failed, 0);
|
|
65
|
+
const totalErrors = results.reduce(
|
|
66
|
+
(s, r) => s + r.steps.filter((s) => s.status === "error").length,
|
|
67
|
+
0,
|
|
68
|
+
);
|
|
69
|
+
const totalTime = formatTime(
|
|
70
|
+
results.reduce((s, r) => s + r.steps.reduce((ss, step) => ss + step.duration_ms, 0), 0),
|
|
71
|
+
);
|
|
63
72
|
|
|
64
|
-
|
|
73
|
+
const suites = results.map(renderTestsuite).join("\n");
|
|
65
74
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
75
|
+
return [
|
|
76
|
+
`<?xml version="1.0" encoding="UTF-8"?>`,
|
|
77
|
+
`<testsuites tests="${totalTests}" failures="${totalFailures}" errors="${totalErrors}" time="${totalTime}">`,
|
|
78
|
+
suites,
|
|
79
|
+
`</testsuites>`,
|
|
80
|
+
].join("\n");
|
|
81
|
+
},
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
/** TASK-186: pure render → sanitizer pipeline; redaction lives in runExporter. */
|
|
85
|
+
export function generateJunitXml(results: TestRunResult[]): string {
|
|
86
|
+
return runExporter(junitExporter, results);
|
|
72
87
|
}
|
|
73
88
|
|
|
74
89
|
export const junitReporter: Reporter = {
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-10 (m-15): NDJSON streaming reporter for `zond checks run`.
|
|
3
|
+
*
|
|
4
|
+
* Each event is a single JSON line on stdout — agents pipe the stream
|
|
5
|
+
* into `jq` / ajv / their own consumer and act on findings *as they
|
|
6
|
+
* happen* instead of waiting for the run to wrap up. The discriminated
|
|
7
|
+
* union of event shapes lives in `src/cli/json-schemas.ts` (zod source
|
|
8
|
+
* of truth) and ships as `docs/json-schema/ndjson-events.schema.json`.
|
|
9
|
+
*
|
|
10
|
+
* The CLI passes `emitToStdout` as the `onEvent` callback to runChecks;
|
|
11
|
+
* tests can pass an in-memory accumulator instead.
|
|
12
|
+
*/
|
|
13
|
+
import type { z } from "zod";
|
|
14
|
+
import type {
|
|
15
|
+
NdjsonCheckStartEventSchema,
|
|
16
|
+
NdjsonCheckResultEventSchema,
|
|
17
|
+
NdjsonFindingEventSchema,
|
|
18
|
+
NdjsonSummaryEventSchema,
|
|
19
|
+
NdjsonEventSchema,
|
|
20
|
+
} from "../../cli/json-schemas.ts";
|
|
21
|
+
|
|
22
|
+
export type NdjsonCheckStartEvent = z.infer<typeof NdjsonCheckStartEventSchema>;
|
|
23
|
+
export type NdjsonCheckResultEvent = z.infer<typeof NdjsonCheckResultEventSchema>;
|
|
24
|
+
export type NdjsonFindingEvent = z.infer<typeof NdjsonFindingEventSchema>;
|
|
25
|
+
export type NdjsonSummaryEvent = z.infer<typeof NdjsonSummaryEventSchema>;
|
|
26
|
+
export type NdjsonEvent = z.infer<typeof NdjsonEventSchema>;
|
|
27
|
+
|
|
28
|
+
/** Write one event to stdout as a single line. AC #5 — when the CLI is
|
|
29
|
+
* in `--ndjson` mode, *every* user-readable message goes to stderr, so
|
|
30
|
+
* stdout stays a clean NDJSON stream that pipes into `jq` / ajv. */
|
|
31
|
+
export function emitToStdout(ev: NdjsonEvent): void {
|
|
32
|
+
process.stdout.write(`${JSON.stringify(ev)}\n`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function nowIso(): string {
|
|
36
|
+
return new Date().toISOString();
|
|
37
|
+
}
|
|
@@ -3,6 +3,9 @@ import type { TestRunResult } from "../runner/types.ts";
|
|
|
3
3
|
export interface ReporterOptions {
|
|
4
4
|
/** Whether to use ANSI colors. Default: auto-detect via isTTY. */
|
|
5
5
|
color?: boolean;
|
|
6
|
+
/** TASK-265: suppress per-suite/per-test detail and emit only the
|
|
7
|
+
* grand-total summary line. Exit code still carries pass/fail. */
|
|
8
|
+
quiet?: boolean;
|
|
6
9
|
}
|
|
7
10
|
|
|
8
11
|
export type ReporterName = "console" | "json" | "junit";
|
|
@@ -10,6 +10,7 @@ function checkType(value: unknown, expectedType: string): boolean {
|
|
|
10
10
|
case "boolean": return typeof value === "boolean";
|
|
11
11
|
case "array": return Array.isArray(value);
|
|
12
12
|
case "object": return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
13
|
+
case "null": return value === null;
|
|
13
14
|
default: return false;
|
|
14
15
|
}
|
|
15
16
|
}
|
|
@@ -37,7 +38,9 @@ function checkRule(path: string, rule: AssertionRule, actual: unknown): Assertio
|
|
|
37
38
|
const field = `body.${path}`;
|
|
38
39
|
|
|
39
40
|
if (rule.exists !== undefined) {
|
|
40
|
-
|
|
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;
|
|
41
44
|
results.push({
|
|
42
45
|
field, rule: `exists ${rule.exists}`,
|
|
43
46
|
passed: doesExist === rule.exists, actual: doesExist, expected: rule.exists,
|
|
@@ -230,6 +233,7 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
|
|
|
230
233
|
passed: allowed.includes(response.status),
|
|
231
234
|
actual: response.status,
|
|
232
235
|
expected: expect.status,
|
|
236
|
+
kind: "primary",
|
|
233
237
|
});
|
|
234
238
|
}
|
|
235
239
|
|
|
@@ -240,6 +244,7 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
|
|
|
240
244
|
passed: response.duration_ms <= expect.duration,
|
|
241
245
|
actual: response.duration_ms,
|
|
242
246
|
expected: expect.duration,
|
|
247
|
+
kind: "auxiliary",
|
|
243
248
|
});
|
|
244
249
|
}
|
|
245
250
|
|
|
@@ -253,12 +258,14 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
|
|
|
253
258
|
passed: actual === rule,
|
|
254
259
|
actual,
|
|
255
260
|
expected: rule,
|
|
261
|
+
kind: "auxiliary",
|
|
256
262
|
});
|
|
257
263
|
} else {
|
|
258
264
|
// AssertionRule in header — supports capture and other checks
|
|
259
265
|
const ruleResults = checkRule(key, rule, actual).map(r => ({
|
|
260
266
|
...r,
|
|
261
267
|
field: r.field.replace(/^body\./, "headers."),
|
|
268
|
+
kind: "auxiliary" as const,
|
|
262
269
|
}));
|
|
263
270
|
results.push(...ruleResults);
|
|
264
271
|
}
|
|
@@ -275,7 +282,11 @@ export function checkAssertions(expect: TestStepExpect, response: HttpResponse):
|
|
|
275
282
|
} else {
|
|
276
283
|
actual = getByPath(response.body_parsed, path);
|
|
277
284
|
}
|
|
278
|
-
|
|
285
|
+
const bodyResults = checkRule(path, rule, actual).map((r) => ({
|
|
286
|
+
...r,
|
|
287
|
+
kind: "primary" as const,
|
|
288
|
+
}));
|
|
289
|
+
results.push(...bodyResults);
|
|
279
290
|
}
|
|
280
291
|
}
|
|
281
292
|
|
|
@@ -321,3 +332,52 @@ export function extractCaptures(
|
|
|
321
332
|
|
|
322
333
|
return captures;
|
|
323
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
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
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;
|
|
@@ -0,0 +1,8 @@
|
|
|
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;
|
|
@@ -0,0 +1,72 @@
|
|
|
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
|
+
}
|