@kirrosh/zond 0.26.1 → 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 +47 -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 -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,359 +0,0 @@
|
|
|
1
|
-
import { z } from "zod";
|
|
2
|
-
import type { TestSuite, TestStep, AssertionRule, TestStepExpect, SuiteConfig, RetryUntil, ForEach, MultipartField, SourceMetadata } from "./types.ts";
|
|
3
|
-
|
|
4
|
-
// ARV-223 (R16/F28): include OPTIONS / HEAD / TRACE so probe-method generated
|
|
5
|
-
// suites (which emit one step per missing-method per path) parse and run.
|
|
6
|
-
// Without this, `zond probe static --emit-tests → zond run` breaks end-to-end
|
|
7
|
-
// on every API where these methods aren't already declared (= almost always).
|
|
8
|
-
const HTTP_METHODS = ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS", "HEAD", "TRACE"] as const;
|
|
9
|
-
|
|
10
|
-
function extractMethodAndPath(raw: unknown): unknown {
|
|
11
|
-
if (typeof raw !== "object" || raw === null) return raw;
|
|
12
|
-
const obj = raw as Record<string, unknown>;
|
|
13
|
-
|
|
14
|
-
let foundMethod: string | undefined;
|
|
15
|
-
for (const method of HTTP_METHODS) {
|
|
16
|
-
if (method in obj) {
|
|
17
|
-
if (foundMethod) {
|
|
18
|
-
throw new Error(`Ambiguous step: found both ${foundMethod} and ${method} keys`);
|
|
19
|
-
}
|
|
20
|
-
foundMethod = method;
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (foundMethod) {
|
|
25
|
-
const path = obj[foundMethod];
|
|
26
|
-
if (typeof path !== "string") {
|
|
27
|
-
throw new Error(`${foundMethod} value must be a string path, got ${typeof path}`);
|
|
28
|
-
}
|
|
29
|
-
const { [foundMethod]: _, ...rest } = obj;
|
|
30
|
-
return { ...rest, method: foundMethod, path };
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// set-only step: no HTTP method required
|
|
34
|
-
if (obj.set && !obj.method) {
|
|
35
|
-
return { ...obj, method: "GET", path: "" };
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
return raw;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const ASSERTION_KEYS = new Set([
|
|
42
|
-
"capture", "type", "equals", "not_equals", "contains", "not_contains",
|
|
43
|
-
"matches", "gt", "lt", "gte", "lte", "exists",
|
|
44
|
-
"length", "length_gt", "length_gte", "length_lt", "length_lte",
|
|
45
|
-
"each", "contains_item", "set_equals",
|
|
46
|
-
]);
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Recursively flattens nested body assertion objects into dot-notation keys.
|
|
50
|
-
* e.g. { category: { name: { equals: "Dogs" } } } → { "category.name": { equals: "Dogs" } }
|
|
51
|
-
* Leaves assertion-level objects untouched (objects where all keys are ASSERTION_KEYS).
|
|
52
|
-
* Also skips the special `_body` key prefix.
|
|
53
|
-
*/
|
|
54
|
-
export function flattenBodyAssertions(body: Record<string, unknown>): Record<string, unknown> {
|
|
55
|
-
const result: Record<string, unknown> = {};
|
|
56
|
-
|
|
57
|
-
function walk(obj: Record<string, unknown>, prefix: string) {
|
|
58
|
-
for (const [key, value] of Object.entries(obj)) {
|
|
59
|
-
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
60
|
-
|
|
61
|
-
if (
|
|
62
|
-
typeof value === "object" && value !== null && !Array.isArray(value) &&
|
|
63
|
-
!fullKey.startsWith("_body")
|
|
64
|
-
) {
|
|
65
|
-
const objKeys = Object.keys(value as Record<string, unknown>);
|
|
66
|
-
const isAssertionRule = objKeys.length > 0 && objKeys.every(k => ASSERTION_KEYS.has(k));
|
|
67
|
-
|
|
68
|
-
if (isAssertionRule) {
|
|
69
|
-
result[fullKey] = value;
|
|
70
|
-
} else {
|
|
71
|
-
walk(value as Record<string, unknown>, fullKey);
|
|
72
|
-
}
|
|
73
|
-
} else {
|
|
74
|
-
result[fullKey] = value;
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
walk(body, "");
|
|
80
|
-
return result;
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
const AssertionRuleSchemaInner: z.ZodType<AssertionRule> = z.preprocess(
|
|
84
|
-
(val) => {
|
|
85
|
-
if (typeof val === "string") return { type: val };
|
|
86
|
-
if (val === null || val === undefined) return { exists: true };
|
|
87
|
-
if (typeof val === "object" && val !== null) {
|
|
88
|
-
const obj = val as Record<string, unknown>;
|
|
89
|
-
// Coerce exists: "true"/"false" → boolean
|
|
90
|
-
if (typeof obj.exists === "string") {
|
|
91
|
-
obj.exists = obj.exists === "true";
|
|
92
|
-
}
|
|
93
|
-
return obj;
|
|
94
|
-
}
|
|
95
|
-
return val;
|
|
96
|
-
},
|
|
97
|
-
z.object({
|
|
98
|
-
capture: z.string().optional(),
|
|
99
|
-
type: z.enum(["string", "integer", "number", "boolean", "array", "object", "null"]).optional(),
|
|
100
|
-
equals: z.unknown().optional(),
|
|
101
|
-
not_equals: z.unknown().optional(),
|
|
102
|
-
contains: z.string().optional(),
|
|
103
|
-
not_contains: z.string().optional(),
|
|
104
|
-
matches: z.string().optional(),
|
|
105
|
-
gt: z.number().optional(),
|
|
106
|
-
lt: z.number().optional(),
|
|
107
|
-
gte: z.number().optional(),
|
|
108
|
-
lte: z.number().optional(),
|
|
109
|
-
exists: z.boolean().optional(),
|
|
110
|
-
length: z.number().int().optional(),
|
|
111
|
-
length_gt: z.number().int().optional(),
|
|
112
|
-
length_gte: z.number().int().optional(),
|
|
113
|
-
length_lt: z.number().int().optional(),
|
|
114
|
-
length_lte: z.number().int().optional(),
|
|
115
|
-
each: z.record(z.string(), z.lazy(() => AssertionRuleSchemaInner)).optional(),
|
|
116
|
-
contains_item: z.record(z.string(), z.lazy(() => AssertionRuleSchemaInner)).optional(),
|
|
117
|
-
set_equals: z.unknown().optional(),
|
|
118
|
-
}),
|
|
119
|
-
) as z.ZodType<AssertionRule>;
|
|
120
|
-
|
|
121
|
-
const AssertionRuleSchema = AssertionRuleSchemaInner;
|
|
122
|
-
|
|
123
|
-
const TestStepExpectSchema: z.ZodType<TestStepExpect> = z.preprocess(
|
|
124
|
-
(val) => {
|
|
125
|
-
if (typeof val !== "object" || val === null) return val;
|
|
126
|
-
const obj = val as Record<string, unknown>;
|
|
127
|
-
// Reject `expect.capture: {...}` — non-canonical syntax some users
|
|
128
|
-
// reach for. zond captures live INSIDE body-rules
|
|
129
|
-
// (`body: { "path.to.field": { capture: var_name } }`); a top-level
|
|
130
|
-
// `capture:` block inside `expect:` is silently dropped, leaving the
|
|
131
|
-
// test green with no captured values. Throw with a clear pointer.
|
|
132
|
-
// (TASK-247)
|
|
133
|
-
if ("capture" in obj && typeof obj.capture === "object" && obj.capture !== null && !Array.isArray(obj.capture)) {
|
|
134
|
-
throw new Error(
|
|
135
|
-
`'expect.capture: {...}' is not a valid step shape. Captures are defined per-field: ` +
|
|
136
|
-
`\`expect.body: { "<path>": { capture: <var_name> } }\`. ` +
|
|
137
|
-
`Top-level 'capture' inside 'expect' is silently ignored — the test would pass with no captured values.`,
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
// expect.status: spot-message for common wrong shapes.
|
|
141
|
-
// Schema accepts `number | number[]`. Users reaching from other tools often
|
|
142
|
-
// write `oneOf: [...]`, `any: [...]`, a string `"200"`, or an array
|
|
143
|
-
// containing strings. The raw zod-issue path (`tests.N.expect.status.0`)
|
|
144
|
-
// is hard to read — surface a single-line hint here. (TASK-249, feedback-13#F1)
|
|
145
|
-
if ("status" in obj && obj.status !== undefined && obj.status !== null) {
|
|
146
|
-
const s = obj.status;
|
|
147
|
-
const STATUS_HINT =
|
|
148
|
-
"expect.status: use a number (200), an array of numbers ([200, 404]), or omit. " +
|
|
149
|
-
"oneOf/any/anyOf are not supported.";
|
|
150
|
-
if (typeof s === "object" && !Array.isArray(s)) {
|
|
151
|
-
const keys = Object.keys(s as Record<string, unknown>);
|
|
152
|
-
const wrong = keys.find((k) => ["oneOf", "anyOf", "any", "in", "one_of"].includes(k));
|
|
153
|
-
if (wrong) {
|
|
154
|
-
throw new Error(`'expect.status' got '${wrong}: [...]' — ${STATUS_HINT}`);
|
|
155
|
-
}
|
|
156
|
-
throw new Error(`'expect.status' got an object — ${STATUS_HINT}`);
|
|
157
|
-
}
|
|
158
|
-
if (typeof s === "string") {
|
|
159
|
-
throw new Error(`'expect.status' got string "${s}" — ${STATUS_HINT}`);
|
|
160
|
-
}
|
|
161
|
-
if (Array.isArray(s) && s.some((v) => typeof v !== "number")) {
|
|
162
|
-
throw new Error(`'expect.status' array must contain only numbers — ${STATUS_HINT}`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
// body: null → remove it
|
|
166
|
-
if (obj.body === null) {
|
|
167
|
-
const { body: _, ...rest } = obj;
|
|
168
|
-
return rest;
|
|
169
|
-
}
|
|
170
|
-
// Flatten nested body assertions into dot-notation
|
|
171
|
-
if (obj.body && typeof obj.body === "object" && !Array.isArray(obj.body)) {
|
|
172
|
-
obj.body = flattenBodyAssertions(obj.body as Record<string, unknown>);
|
|
173
|
-
}
|
|
174
|
-
return obj;
|
|
175
|
-
},
|
|
176
|
-
z.object({
|
|
177
|
-
status: z.union([z.number().int(), z.array(z.number().int())]).optional(),
|
|
178
|
-
body: z.record(z.string(), AssertionRuleSchema).optional(),
|
|
179
|
-
headers: z.record(z.string(), z.union([z.string(), AssertionRuleSchema])).optional(),
|
|
180
|
-
duration: z.number().optional(),
|
|
181
|
-
}),
|
|
182
|
-
) as z.ZodType<TestStepExpect>;
|
|
183
|
-
|
|
184
|
-
const RetryUntilSchema: z.ZodType<RetryUntil> = z.object({
|
|
185
|
-
condition: z.string(),
|
|
186
|
-
max_attempts: z.number().int().positive(),
|
|
187
|
-
delay_ms: z.number().int().nonnegative(),
|
|
188
|
-
});
|
|
189
|
-
|
|
190
|
-
const ForEachSchema: z.ZodType<ForEach> = z.object({
|
|
191
|
-
var: z.string(),
|
|
192
|
-
in: z.unknown(),
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
const MultipartFileFieldSchema = z.object({
|
|
196
|
-
file: z.string(),
|
|
197
|
-
filename: z.string().optional(),
|
|
198
|
-
content_type: z.string().optional(),
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
const MultipartFieldSchema: z.ZodType<MultipartField> = z.union([z.string(), MultipartFileFieldSchema]);
|
|
202
|
-
|
|
203
|
-
// Provenance metadata: passthrough — все поля optional, неизвестные пропускаем без warning
|
|
204
|
-
const SourceMetadataSchema: z.ZodType<SourceMetadata> = z.object({
|
|
205
|
-
type: z.enum(["openapi-generated", "manual", "probe-suite"]).optional(),
|
|
206
|
-
spec: z.string().optional(),
|
|
207
|
-
generator: z.string().optional(),
|
|
208
|
-
generated_at: z.string().optional(),
|
|
209
|
-
endpoint: z.string().optional(),
|
|
210
|
-
response_branch: z.string().optional(),
|
|
211
|
-
schema_pointer: z.string().optional(),
|
|
212
|
-
}).passthrough() as z.ZodType<SourceMetadata>;
|
|
213
|
-
|
|
214
|
-
const KNOWN_STEP_KEYS = new Set([
|
|
215
|
-
"name", "source", "method", "path", "headers",
|
|
216
|
-
"json", "form", "multipart", "query", "expect",
|
|
217
|
-
"skip_if", "retry_until", "for_each", "set", "always",
|
|
218
|
-
// raw HTTP method keys are folded into method/path by extractMethodAndPath
|
|
219
|
-
...HTTP_METHODS,
|
|
220
|
-
]);
|
|
221
|
-
|
|
222
|
-
// Common typo / wrong-name body keys we detect explicitly to emit an
|
|
223
|
-
// actionable error instead of silently dropping. Real APIs reject the empty
|
|
224
|
-
// POST that follows, but the user spends 10+ minutes debugging — this hint
|
|
225
|
-
// turns it into a one-line fix. (TASK-244)
|
|
226
|
-
const BODY_KEY_HINTS: Record<string, string> = {
|
|
227
|
-
body: "json (for application/json), form (urlencoded), or multipart (file upload)",
|
|
228
|
-
data: "json (for application/json) or form (urlencoded)",
|
|
229
|
-
payload: "json",
|
|
230
|
-
// TASK-257: previous hint pointed only at `form:` which is x-www-form-urlencoded
|
|
231
|
-
// and useless for file uploads. Surface `multipart:` explicitly so users with
|
|
232
|
-
// file-upload endpoints (file-upload endpoints, etc.) find it.
|
|
233
|
-
raw: "json for raw JSON, multipart: { field: { file: <path> } } for file upload, or form for urlencoded — raw bodies are not parsed",
|
|
234
|
-
};
|
|
235
|
-
|
|
236
|
-
const TestStepSchema: z.ZodType<TestStep> = z.preprocess(
|
|
237
|
-
(raw) => {
|
|
238
|
-
const obj = extractMethodAndPath(raw);
|
|
239
|
-
if (typeof obj === "object" && obj !== null) {
|
|
240
|
-
const o = obj as Record<string, unknown>;
|
|
241
|
-
|
|
242
|
-
// Reject silently-dropped body-shaped keys with a clear suggestion.
|
|
243
|
-
for (const [bad, hint] of Object.entries(BODY_KEY_HINTS)) {
|
|
244
|
-
if (bad in o) {
|
|
245
|
-
const stepName = typeof o.name === "string" ? ` in step "${o.name}"` : "";
|
|
246
|
-
throw new Error(
|
|
247
|
-
`Unknown step key '${bad}'${stepName}. Did you mean '${hint}'? ` +
|
|
248
|
-
`(zond does not recognize '${bad}:' and would silently drop the body)`,
|
|
249
|
-
);
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Make expect optional for set-only steps
|
|
254
|
-
if (o.set && !o.expect) {
|
|
255
|
-
o.expect = {};
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
return obj;
|
|
259
|
-
},
|
|
260
|
-
z.object({
|
|
261
|
-
name: z.string(),
|
|
262
|
-
source: SourceMetadataSchema.optional(),
|
|
263
|
-
method: z.enum(HTTP_METHODS),
|
|
264
|
-
path: z.string(),
|
|
265
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
266
|
-
json: z.unknown().optional(),
|
|
267
|
-
form: z.record(z.string(), z.string()).optional(),
|
|
268
|
-
multipart: z.record(z.string(), MultipartFieldSchema).optional(),
|
|
269
|
-
query: z.record(z.string(), z.string()).optional(),
|
|
270
|
-
expect: TestStepExpectSchema,
|
|
271
|
-
skip_if: z.string().optional(),
|
|
272
|
-
retry_until: RetryUntilSchema.optional(),
|
|
273
|
-
for_each: ForEachSchema.optional(),
|
|
274
|
-
set: z.record(z.string(), z.unknown()).optional(),
|
|
275
|
-
always: z.boolean().optional(),
|
|
276
|
-
}),
|
|
277
|
-
) as z.ZodType<TestStep>;
|
|
278
|
-
|
|
279
|
-
export const DEFAULT_CONFIG: SuiteConfig = {
|
|
280
|
-
timeout: 30000,
|
|
281
|
-
retries: 0,
|
|
282
|
-
retry_delay: 1000,
|
|
283
|
-
follow_redirects: true,
|
|
284
|
-
verify_ssl: true,
|
|
285
|
-
};
|
|
286
|
-
|
|
287
|
-
const SuiteConfigSchema = z.preprocess(
|
|
288
|
-
(val) => ({ ...DEFAULT_CONFIG, ...(typeof val === "object" && val !== null ? val : {}) }),
|
|
289
|
-
z.object({
|
|
290
|
-
timeout: z.number(),
|
|
291
|
-
retries: z.number(),
|
|
292
|
-
retry_delay: z.number(),
|
|
293
|
-
follow_redirects: z.boolean(),
|
|
294
|
-
verify_ssl: z.boolean(),
|
|
295
|
-
}),
|
|
296
|
-
) as z.ZodType<SuiteConfig>;
|
|
297
|
-
|
|
298
|
-
const TestSuiteSchema = z.preprocess(
|
|
299
|
-
(val) => {
|
|
300
|
-
if (typeof val === "object" && val !== null && !("config" in val)) {
|
|
301
|
-
return { ...val, config: DEFAULT_CONFIG };
|
|
302
|
-
}
|
|
303
|
-
return val;
|
|
304
|
-
},
|
|
305
|
-
z.object({
|
|
306
|
-
name: z.string(),
|
|
307
|
-
description: z.string().optional(),
|
|
308
|
-
setup: z.boolean().optional(),
|
|
309
|
-
tags: z.array(z.string()).optional(),
|
|
310
|
-
source: SourceMetadataSchema.optional(),
|
|
311
|
-
base_url: z.string().optional(),
|
|
312
|
-
headers: z.record(z.string(), z.string()).optional(),
|
|
313
|
-
parameterize: z.record(z.string(), z.array(z.unknown()).min(1)).optional(),
|
|
314
|
-
config: SuiteConfigSchema,
|
|
315
|
-
tests: z.array(TestStepSchema).min(1),
|
|
316
|
-
}),
|
|
317
|
-
);
|
|
318
|
-
|
|
319
|
-
export function validateSuite(raw: unknown): TestSuite {
|
|
320
|
-
return TestSuiteSchema.parse(raw) as TestSuite;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
/** Render a zod path array (`["tests", 0, "expect", "status"]`) as
|
|
324
|
-
* `tests[0].expect.status`. Numeric segments become bracket-indices, string
|
|
325
|
-
* segments dot-join. */
|
|
326
|
-
function pathToHuman(path: ReadonlyArray<string | number>): string {
|
|
327
|
-
let out = "";
|
|
328
|
-
for (const seg of path) {
|
|
329
|
-
if (typeof seg === "number") out += `[${seg}]`;
|
|
330
|
-
else out += out ? `.${seg}` : seg;
|
|
331
|
-
}
|
|
332
|
-
return out || "(root)";
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
/**
|
|
336
|
-
* Format a {@link z.ZodError} as a compact, multi-line, human-readable list:
|
|
337
|
-
*
|
|
338
|
-
* N validation issue(s):
|
|
339
|
-
* <path>: <message>
|
|
340
|
-
* ...
|
|
341
|
-
*
|
|
342
|
-
* The default `ZodError.message` is a JSON dump of the full issue list with
|
|
343
|
-
* internal field names (`_def`, deeply numeric paths, "Invalid input" prefix).
|
|
344
|
-
* The wrapper that callers used to surface ("Validation error in <file>:
|
|
345
|
-
* [{...}]") was unreadable for tester users — they had to mentally parse the
|
|
346
|
-
* stack to find the real path. (TASK-249)
|
|
347
|
-
*/
|
|
348
|
-
export function formatZodError(err: z.ZodError): string {
|
|
349
|
-
const lines = err.issues.map((i) => {
|
|
350
|
-
const path = pathToHuman(i.path as ReadonlyArray<string | number>);
|
|
351
|
-
// zod v4 messages are already readable; strip the redundant "Invalid input: "
|
|
352
|
-
// prefix that adds noise without info.
|
|
353
|
-
const msg = i.message.replace(/^Invalid input:\s*/, "");
|
|
354
|
-
return ` ${path}: ${msg}`;
|
|
355
|
-
});
|
|
356
|
-
const header = `${err.issues.length} validation issue${err.issues.length === 1 ? "" : "s"}:`;
|
|
357
|
-
return `${header}\n${lines.join("\n")}`;
|
|
358
|
-
}
|
|
359
|
-
|
package/src/core/parser/types.ts
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" | "OPTIONS" | "HEAD" | "TRACE";
|
|
2
|
-
|
|
3
|
-
export interface AssertionRule {
|
|
4
|
-
capture?: string;
|
|
5
|
-
type?: "string" | "integer" | "number" | "boolean" | "array" | "object" | "null";
|
|
6
|
-
equals?: unknown;
|
|
7
|
-
not_equals?: unknown;
|
|
8
|
-
contains?: string;
|
|
9
|
-
not_contains?: string;
|
|
10
|
-
matches?: string;
|
|
11
|
-
gt?: number;
|
|
12
|
-
lt?: number;
|
|
13
|
-
gte?: number;
|
|
14
|
-
lte?: number;
|
|
15
|
-
exists?: boolean;
|
|
16
|
-
length?: number;
|
|
17
|
-
length_gt?: number;
|
|
18
|
-
length_gte?: number;
|
|
19
|
-
length_lt?: number;
|
|
20
|
-
length_lte?: number;
|
|
21
|
-
each?: Record<string, AssertionRule>;
|
|
22
|
-
contains_item?: Record<string, AssertionRule>;
|
|
23
|
-
set_equals?: unknown;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export interface TestStepExpect {
|
|
27
|
-
status?: number | number[];
|
|
28
|
-
body?: Record<string, AssertionRule>;
|
|
29
|
-
headers?: Record<string, string | AssertionRule>;
|
|
30
|
-
duration?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface RetryUntil {
|
|
34
|
-
condition: string;
|
|
35
|
-
max_attempts: number;
|
|
36
|
-
delay_ms: number;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
export interface ForEach {
|
|
40
|
-
var: string;
|
|
41
|
-
in: unknown;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* Provenance metadata: «откуда этот test/suite». Optional, не участвует в
|
|
46
|
-
* matching/dedup/validation. Suite-level задаёт общие поля; step-level
|
|
47
|
-
* наследует через shallow merge `{ ...suite.source, ...step.source }`.
|
|
48
|
-
*/
|
|
49
|
-
export interface SourceMetadata {
|
|
50
|
-
type?: "openapi-generated" | "manual" | "probe-suite";
|
|
51
|
-
spec?: string;
|
|
52
|
-
generator?: string;
|
|
53
|
-
generated_at?: string;
|
|
54
|
-
endpoint?: string;
|
|
55
|
-
response_branch?: string;
|
|
56
|
-
schema_pointer?: string;
|
|
57
|
-
[key: string]: unknown;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
export interface MultipartFileField {
|
|
61
|
-
file: string;
|
|
62
|
-
filename?: string;
|
|
63
|
-
content_type?: string;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export type MultipartField = string | MultipartFileField;
|
|
67
|
-
|
|
68
|
-
export interface TestStep {
|
|
69
|
-
name: string;
|
|
70
|
-
source?: SourceMetadata;
|
|
71
|
-
method: HttpMethod;
|
|
72
|
-
path: string;
|
|
73
|
-
headers?: Record<string, string>;
|
|
74
|
-
json?: unknown;
|
|
75
|
-
form?: Record<string, string>;
|
|
76
|
-
multipart?: Record<string, MultipartField>;
|
|
77
|
-
query?: Record<string, string>;
|
|
78
|
-
expect: TestStepExpect;
|
|
79
|
-
skip_if?: string;
|
|
80
|
-
retry_until?: RetryUntil;
|
|
81
|
-
for_each?: ForEach;
|
|
82
|
-
set?: Record<string, unknown>;
|
|
83
|
-
/**
|
|
84
|
-
* Run this step even when prior steps in the suite have failed assertions
|
|
85
|
-
* (so their captures are "tainted"). Designed for cleanup steps. Still
|
|
86
|
-
* skips if a referenced capture is genuinely missing from a response.
|
|
87
|
-
*/
|
|
88
|
-
always?: boolean;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
export interface SuiteConfig {
|
|
92
|
-
timeout: number;
|
|
93
|
-
retries: number;
|
|
94
|
-
retry_delay: number;
|
|
95
|
-
follow_redirects: boolean;
|
|
96
|
-
verify_ssl: boolean;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
export interface TestSuite {
|
|
100
|
-
name: string;
|
|
101
|
-
description?: string;
|
|
102
|
-
/** If true, this suite runs before all regular suites and its captures are shared into their env */
|
|
103
|
-
setup?: boolean;
|
|
104
|
-
tags?: string[];
|
|
105
|
-
source?: SourceMetadata;
|
|
106
|
-
base_url?: string;
|
|
107
|
-
headers?: Record<string, string>;
|
|
108
|
-
/** Cross-product parameterisation: each key contributes one variable
|
|
109
|
-
* binding per array entry. Suite body runs once per combination. */
|
|
110
|
-
parameterize?: Record<string, unknown[]>;
|
|
111
|
-
config: SuiteConfig;
|
|
112
|
-
tests: TestStep[];
|
|
113
|
-
/** Absolute path to the source file, set by yaml-parser */
|
|
114
|
-
filePath?: string;
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
export type Environment = Record<string, string>;
|
|
Binary file
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
import { Glob } from "bun";
|
|
2
|
-
import { resolve } from "node:path";
|
|
3
|
-
import YAML from "yaml";
|
|
4
|
-
import { z } from "zod";
|
|
5
|
-
import { validateSuite, formatZodError } from "./schema.ts";
|
|
6
|
-
import type { TestSuite } from "./types.ts";
|
|
7
|
-
|
|
8
|
-
export interface ParseOptions {
|
|
9
|
-
/** Surface raw `ZodError.message` (the JSON-formatted issue stack) instead
|
|
10
|
-
* of the human-friendly summary. Useful for filing zod bugs / debugging the
|
|
11
|
-
* schema itself; default output is human-readable. (TASK-249) */
|
|
12
|
-
verbose?: boolean;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
/** Convert a 0-based byte offset into a 1-based (line, col) position. */
|
|
16
|
-
function offsetToLineCol(text: string, offset: number): { line: number; col: number } {
|
|
17
|
-
let line = 1;
|
|
18
|
-
let col = 1;
|
|
19
|
-
for (let i = 0; i < offset && i < text.length; i++) {
|
|
20
|
-
if (text.charCodeAt(i) === 0x0a) {
|
|
21
|
-
line++;
|
|
22
|
-
col = 1;
|
|
23
|
-
} else {
|
|
24
|
-
col++;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
return { line, col };
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/**
|
|
31
|
-
* Format a YAML parse error as `file:line:col: <reason>` plus a snippet with
|
|
32
|
-
* a column pointer. Bun.YAML's SyntaxError exposes JS stack coordinates, not
|
|
33
|
-
* YAML positions, so on parse failure we re-parse with eemeli/yaml (which
|
|
34
|
-
* provides accurate `linePos`) just for diagnostics.
|
|
35
|
-
*
|
|
36
|
-
* Exported for tests.
|
|
37
|
-
*/
|
|
38
|
-
export function formatYamlParseError(filePath: string, text: string, primary: Error): Error {
|
|
39
|
-
const doc = YAML.parseDocument(text);
|
|
40
|
-
const e = doc.errors[0];
|
|
41
|
-
if (e?.linePos?.[0]) {
|
|
42
|
-
const { line, col } = e.linePos[0];
|
|
43
|
-
// eemeli's message reads "<reason> at line X, column Y:\n\n<snippet>".
|
|
44
|
-
// Strip the "at line ..." part since we surface line:col in the prefix.
|
|
45
|
-
const cleaned = e.message.replace(/\s+at line \d+, column \d+:/, ":");
|
|
46
|
-
return new Error(`Invalid YAML in ${filePath}:${line}:${col}: ${cleaned}`);
|
|
47
|
-
}
|
|
48
|
-
// eemeli accepted but Bun rejected — fall back to original message.
|
|
49
|
-
return new Error(`Invalid YAML in ${filePath}: ${primary.message}`);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
export async function parseFile(filePath: string, opts: ParseOptions = {}): Promise<TestSuite> {
|
|
53
|
-
let text: string;
|
|
54
|
-
try {
|
|
55
|
-
text = await Bun.file(filePath).text();
|
|
56
|
-
} catch (err) {
|
|
57
|
-
throw new Error(`Failed to read file ${filePath}: ${(err as Error).message}`);
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
// Both Bun.YAML and eemeli/yaml accept NUL bytes silently, but they corrupt
|
|
61
|
-
// downstream consumers (sqlite TEXT, JSON, terminals). Surface explicitly.
|
|
62
|
-
const nulIdx = text.indexOf("\x00");
|
|
63
|
-
if (nulIdx >= 0) {
|
|
64
|
-
const { line, col } = offsetToLineCol(text, nulIdx);
|
|
65
|
-
throw new Error(
|
|
66
|
-
`Invalid YAML in ${filePath}:${line}:${col}: NUL byte (\\x00) in source — ` +
|
|
67
|
-
`if you need a NUL in a request body, use the {{$nullByte}} generator instead of inlining the byte`
|
|
68
|
-
);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let raw: unknown;
|
|
72
|
-
try {
|
|
73
|
-
raw = Bun.YAML.parse(text);
|
|
74
|
-
} catch (err) {
|
|
75
|
-
throw formatYamlParseError(filePath, text, err as Error);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
try {
|
|
79
|
-
const suite = validateSuite(raw);
|
|
80
|
-
suite.filePath = resolve(filePath);
|
|
81
|
-
return suite;
|
|
82
|
-
} catch (err) {
|
|
83
|
-
if (err instanceof z.ZodError && !opts.verbose) {
|
|
84
|
-
throw new Error(`Validation error in ${filePath}:\n${formatZodError(err)}`);
|
|
85
|
-
}
|
|
86
|
-
throw new Error(`Validation error in ${filePath}: ${(err as Error).message}`);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
/**
|
|
91
|
-
* Files that live alongside test suites but aren't suites themselves. The
|
|
92
|
-
* yaml-parser scans recursively from the workspace root, so picking these up
|
|
93
|
-
* would surface spurious "Validation error: missing field name" noise.
|
|
94
|
-
*/
|
|
95
|
-
function isNonSuiteYaml(file: string): boolean {
|
|
96
|
-
if (file.match(/\.env(\..+)?\.ya?ml$/)) return true;
|
|
97
|
-
// Workspace marker — present at the root of every zond workspace.
|
|
98
|
-
if (file === "zond.config.yml" || file === "zond.config.yaml") return true;
|
|
99
|
-
// Per-API artifact files written by `zond add api` / `zond refresh-api`.
|
|
100
|
-
// Match the basename so it works for files at any depth (apis/<name>/...).
|
|
101
|
-
const basename = file.split("/").pop() ?? file;
|
|
102
|
-
if (/^\.api-[a-z0-9-]+\.ya?ml$/i.test(basename)) return true;
|
|
103
|
-
return false;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
export async function parseDirectory(dirPath: string, opts: ParseOptions = {}): Promise<TestSuite[]> {
|
|
107
|
-
const glob = new Glob("**/*.{yaml,yml}");
|
|
108
|
-
const suites: TestSuite[] = [];
|
|
109
|
-
|
|
110
|
-
for await (const file of glob.scan({ cwd: dirPath, absolute: false })) {
|
|
111
|
-
if (isNonSuiteYaml(file)) {
|
|
112
|
-
continue;
|
|
113
|
-
}
|
|
114
|
-
const fullPath = `${dirPath}/${file}`;
|
|
115
|
-
try {
|
|
116
|
-
suites.push(await parseFile(fullPath, opts));
|
|
117
|
-
} catch {
|
|
118
|
-
// Skip files that fail to parse (e.g. invalid AI-generated YAML)
|
|
119
|
-
// so one bad file doesn't block the entire directory
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return suites;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
export interface ParseDirectoryResult {
|
|
127
|
-
suites: TestSuite[];
|
|
128
|
-
errors: { file: string; error: string }[];
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
export async function parseDirectorySafe(dirPath: string, opts: ParseOptions = {}): Promise<ParseDirectoryResult> {
|
|
132
|
-
const glob = new Glob("**/*.{yaml,yml}");
|
|
133
|
-
const suites: TestSuite[] = [];
|
|
134
|
-
const errors: { file: string; error: string }[] = [];
|
|
135
|
-
|
|
136
|
-
for await (const file of glob.scan({ cwd: dirPath, absolute: false })) {
|
|
137
|
-
if (isNonSuiteYaml(file)) {
|
|
138
|
-
continue;
|
|
139
|
-
}
|
|
140
|
-
const fullPath = `${dirPath}/${file}`;
|
|
141
|
-
try {
|
|
142
|
-
suites.push(await parseFile(fullPath, opts));
|
|
143
|
-
} catch (err) {
|
|
144
|
-
errors.push({ file, error: (err as Error).message });
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return { suites, errors };
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
export async function parse(path: string, opts: ParseOptions = {}): Promise<TestSuite[]> {
|
|
152
|
-
const file = Bun.file(path);
|
|
153
|
-
const exists = await file.exists();
|
|
154
|
-
|
|
155
|
-
if (exists) {
|
|
156
|
-
return [await parseFile(path, opts)];
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// Not a file, try as directory
|
|
160
|
-
return parseDirectory(path, opts);
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* Like {@link parse}, but never silently drops files. Returns both successfully
|
|
165
|
-
* parsed suites and per-file parse errors so callers (run, validate, tag-filter)
|
|
166
|
-
* can surface failures instead of pretending the file did not exist.
|
|
167
|
-
*/
|
|
168
|
-
export async function parseSafe(path: string, opts: ParseOptions = {}): Promise<ParseDirectoryResult> {
|
|
169
|
-
const file = Bun.file(path);
|
|
170
|
-
const exists = await file.exists();
|
|
171
|
-
|
|
172
|
-
if (exists) {
|
|
173
|
-
try {
|
|
174
|
-
return { suites: [await parseFile(path, opts)], errors: [] };
|
|
175
|
-
} catch (err) {
|
|
176
|
-
return { suites: [], errors: [{ file: path, error: (err as Error).message }] };
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
return parseDirectorySafe(path, opts);
|
|
181
|
-
}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Probe registry bootstrap (m-17 / ARV-49).
|
|
3
|
-
*
|
|
4
|
-
* Called once from the CLI program init. Imports each Probe class and
|
|
5
|
-
* runs `registerProbe`, which validates the contract from `types.ts`
|
|
6
|
-
* and throws if a slot is missing. Boot-time failure is louder than
|
|
7
|
-
* runtime — adding a new probe class without --dry-run / --report
|
|
8
|
-
* support won't ship; that's the whole point of the m-17 contract.
|
|
9
|
-
*
|
|
10
|
-
* Idempotent: repeated calls are no-ops (matters for unit tests that
|
|
11
|
-
* run the bootstrap multiple times).
|
|
12
|
-
*/
|
|
13
|
-
import { listProbes, registerProbe } from "./registry.ts";
|
|
14
|
-
import { SecurityProbe } from "./security-probe-class.ts";
|
|
15
|
-
import { MassAssignmentProbe } from "./mass-assignment-probe-class.ts";
|
|
16
|
-
import { StaticProbe } from "./static-probe-class.ts";
|
|
17
|
-
|
|
18
|
-
let bootstrapped = false;
|
|
19
|
-
|
|
20
|
-
export function bootstrapProbes(): void {
|
|
21
|
-
if (bootstrapped) return;
|
|
22
|
-
if (listProbes().length === 0) {
|
|
23
|
-
registerProbe(new StaticProbe());
|
|
24
|
-
registerProbe(new MassAssignmentProbe());
|
|
25
|
-
registerProbe(new SecurityProbe());
|
|
26
|
-
}
|
|
27
|
-
bootstrapped = true;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
/** Test helper — resets the singleton so the next `bootstrapProbes()`
|
|
31
|
-
* re-registers from scratch. Pair with `clearProbes()` from registry. */
|
|
32
|
-
export function resetBootstrap(): void {
|
|
33
|
-
bootstrapped = false;
|
|
34
|
-
}
|