@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
package/README.md
CHANGED
|
@@ -4,35 +4,69 @@ AI-powered API testing for Claude Code, Cursor, and CI/CD.
|
|
|
4
4
|
|
|
5
5
|
Say "test my API" — get working tests, coverage dashboard, and CI config in minutes.
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: structured tools, safety guardrails, coverage tracking, and run history. You don't need to learn anything new — just describe what you want and the agent handles the rest.
|
|
7
|
+
Zond reads your OpenAPI spec and gives your AI agent everything it needs to test your API: a focused CLI, safety guardrails, coverage tracking, and run history. You don't need to learn anything new — just describe what you want and the agent runs `zond` commands.
|
|
10
8
|
|
|
11
9
|
## Quick Start
|
|
12
10
|
|
|
11
|
+
Install the binary (no Node.js required):
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
|
|
15
|
+
iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Bootstrap a workspace, register your first API, then fill its fixtures:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
zond init # bootstrap workspace (no fixture changes)
|
|
22
|
+
zond add api my-api --spec ./openapi.json # register: copies spec.json + emits manifest
|
|
23
|
+
zond doctor --api my-api --missing-only # gap report: which vars are UNSET
|
|
24
|
+
zond prepare-fixtures --api my-api --apply [--seed] # fill apis/my-api/.env.yaml from live API
|
|
13
25
|
```
|
|
14
|
-
|
|
15
|
-
|
|
26
|
+
|
|
27
|
+
For path-FK ids that auto-discover/--seed can't reach (vendor-dashboard
|
|
28
|
+
ids like `cus_*`, GitHub PR numbers, Sentry issue ids), use the manual
|
|
29
|
+
helpers (ARV-195):
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
zond fixtures add --api my-api customer_id=cus_123 --validate --apply
|
|
33
|
+
pbpaste | zond fixtures import --api my-api --from-curl --apply # paste a curl from devtools
|
|
16
34
|
```
|
|
17
35
|
|
|
18
|
-
|
|
36
|
+
`zond init` writes a self-contained [`AGENTS.md`](AGENTS.md) and Claude Code
|
|
37
|
+
skills — agents read it and use the CLI directly (`zond run`,
|
|
38
|
+
`zond probe static`, `zond db diagnose`, …). No daemon, no transport, no
|
|
39
|
+
extra configuration. `init` is workspace-only — it never touches
|
|
40
|
+
`.env.yaml`; the fixture loop above is the canonical path.
|
|
19
41
|
|
|
20
|
-
|
|
42
|
+
Each registered API gets four files in `apis/<name>/`:
|
|
21
43
|
|
|
22
|
-
|
|
23
|
-
|
|
44
|
+
- `spec.json` — dereferenced OpenAPI snapshot (canonical machine source).
|
|
45
|
+
- `.api-catalog.yaml` — endpoint index for agents (cheap to read).
|
|
46
|
+
- `.api-resources.yaml` — CRUD chains, FK dependencies, ETag/soft-delete flags.
|
|
47
|
+
- `.api-fixtures.yaml` — **manifest** of required `{{vars}}` (read-only, auto-generated).
|
|
48
|
+
|
|
49
|
+
Plus a sibling `.env.yaml` that you (or `zond prepare-fixtures`) fill with
|
|
50
|
+
the **values** for those vars. The manifest/values split is strict — see
|
|
51
|
+
the [workspace contract](AGENTS.md#workspace-contract) for details.
|
|
52
|
+
|
|
53
|
+
Run `zond refresh-api <name> [--spec <new-source>]` to re-snapshot when the
|
|
54
|
+
upstream spec changes.
|
|
55
|
+
|
|
56
|
+
Then say to your agent: _"Safely cover the API from openapi.json with tests."_
|
|
24
57
|
|
|
25
|
-
|
|
58
|
+
Want the whole pipeline at once? `zond audit --api my-api` runs
|
|
59
|
+
prepare-fixtures → generate → probes → run → coverage → HTML report in a
|
|
60
|
+
single shot.
|
|
61
|
+
|
|
62
|
+
<details>
|
|
63
|
+
<summary>Other installation methods (npx)</summary>
|
|
26
64
|
|
|
27
65
|
```bash
|
|
28
66
|
npx -y @kirrosh/zond --version
|
|
29
|
-
|
|
30
|
-
# Standalone binary (no Node.js required)
|
|
31
|
-
curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh # macOS/Linux
|
|
32
|
-
iwr https://raw.githubusercontent.com/kirrosh/zond/master/install.ps1 | iex # Windows
|
|
33
67
|
```
|
|
34
68
|
|
|
35
|
-
See [ZOND.md](ZOND.md) for full CLI reference.
|
|
69
|
+
See [ZOND.md](ZOND.md) for the full CLI reference.
|
|
36
70
|
|
|
37
71
|
</details>
|
|
38
72
|
|
|
@@ -56,6 +90,11 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
|
|
|
56
90
|
| **Spec-Grounded** | Tests are derived from your OpenAPI schema, not invented from scratch. The spec is the source of truth. |
|
|
57
91
|
| **Full Visibility** | Every run is stored in SQLite. Compare runs, track regressions, see exactly what the server returned. |
|
|
58
92
|
| **Coverage Tracking** | See which endpoints are tested, which aren't, and what broke since last run. |
|
|
93
|
+
| **Schema Validation** | `--validate-schema` checks every JSON response against the OpenAPI schema (types, required, enum, format, `$ref`) — catches contract drift the YAML expectations miss. |
|
|
94
|
+
| **Spec Linting** | `zond check spec` static-analyses the OpenAPI document for internal-consistency bugs (e.g. example violates `format: date-time`) and strictness gaps (path-params without `format`, integer params without min/max) — surfaces issues before any HTTP request. |
|
|
95
|
+
| **Depth Checks (m-15)** | `zond checks run` runs a schemathesis-style catalog of conformance + security probes (`status_code_conformance`, `negative_data_rejection`, `ignored_auth`, `use_after_free`, …) — boundary-value coverage, broken-auth detection, soft-deleted resource leaks. Every finding ships with a `recommended_action` enum so the agent triages without parsing messages. |
|
|
96
|
+
| **SARIF for Code Scanning** | `--report sarif` emits SARIF v2.1.0 with stable `partialFingerprints` — drop-in for `github/codeql-action/upload-sarif@v3` so depth-checks findings show up in GitHub's Security tab. |
|
|
97
|
+
| **Concurrent Workers** | `--workers auto` parallelizes runs at the operation level (bounded async-pool, no threading) — runs that took minutes finish in seconds. Pair with `--rate-limit` to stay within an API's RPS budget. |
|
|
59
98
|
| **CI-Ready** | One command generates GitHub Actions or GitLab CI workflow. Tests in YAML, in git, with code review. |
|
|
60
99
|
|
|
61
100
|
## Try It
|
|
@@ -67,11 +106,35 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
|
|
|
67
106
|
"Set up CI for API tests"
|
|
68
107
|
```
|
|
69
108
|
|
|
109
|
+
## Upgrading
|
|
110
|
+
|
|
111
|
+
`zond update` was removed in favour of system package managers:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
# macOS / Linux — re-run the installer
|
|
115
|
+
curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
|
|
116
|
+
|
|
117
|
+
# npm
|
|
118
|
+
npm install -g @kirrosh/zond@latest
|
|
119
|
+
|
|
120
|
+
# bun
|
|
121
|
+
bun install -g @kirrosh/zond@latest
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Shell completions
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
zond completions bash > ~/.local/share/bash-completion/completions/zond
|
|
128
|
+
zond completions zsh > ~/.zsh/completions/_zond # then `compinit`
|
|
129
|
+
zond completions fish > ~/.config/fish/completions/zond.fish
|
|
130
|
+
```
|
|
131
|
+
|
|
70
132
|
## Documentation
|
|
71
133
|
|
|
72
134
|
- [ZOND.md](ZOND.md) — full CLI reference
|
|
73
135
|
- [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart (RU)
|
|
74
136
|
- [docs/ci.md](docs/ci.md) — CI/CD integration
|
|
137
|
+
- [backlog/](backlog/) — project tasks (powered by [Backlog.md](https://backlog.md))
|
|
75
138
|
|
|
76
139
|
## License
|
|
77
140
|
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirrosh/zond",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "API testing platform — define tests in YAML, run from CLI
|
|
3
|
+
"version": "0.23.0",
|
|
4
|
+
"description": "API testing platform — define tests in YAML, run from CLI, generate from OpenAPI specs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "index.ts",
|
|
7
7
|
"type": "module",
|
|
@@ -25,17 +25,23 @@
|
|
|
25
25
|
},
|
|
26
26
|
"scripts": {
|
|
27
27
|
"zond": "bun run src/cli/index.ts",
|
|
28
|
+
"backlog": "bunx backlog",
|
|
29
|
+
"board": "bunx backlog board",
|
|
28
30
|
"test": "bun run test:unit && bun run test:mocked",
|
|
29
|
-
"test:unit": "bun test tests/
|
|
31
|
+
"test:unit": "bun test tests/",
|
|
30
32
|
"test:mocked": "bun run scripts/run-mocked-tests.ts",
|
|
31
33
|
"check": "tsc --noEmit --project tsconfig.json",
|
|
32
|
-
"
|
|
33
|
-
"
|
|
34
|
-
"
|
|
34
|
+
"schemas": "bun run scripts/emit-json-schemas.ts",
|
|
35
|
+
"schemas:check": "bun run scripts/emit-json-schemas.ts --check",
|
|
36
|
+
"lint:dead": "knip --reporter compact",
|
|
37
|
+
"build": "bun build --compile src/cli/index.ts --outfile dist/zond && bun run scripts/codesign-darwin.ts ./dist/zond",
|
|
35
38
|
"bench:api": "bun benchmarks/api/server.ts"
|
|
36
39
|
},
|
|
37
40
|
"devDependencies": {
|
|
38
|
-
"@types/bun": "latest"
|
|
41
|
+
"@types/bun": "latest",
|
|
42
|
+
"ajv-draft-04": "^1.0.0",
|
|
43
|
+
"backlog.md": "^1.44.0",
|
|
44
|
+
"knip": "^6.7.0"
|
|
39
45
|
},
|
|
40
46
|
"engines": {
|
|
41
47
|
"bun": ">=1.1.0"
|
|
@@ -44,11 +50,12 @@
|
|
|
44
50
|
"typescript": "^5"
|
|
45
51
|
},
|
|
46
52
|
"dependencies": {
|
|
47
|
-
"@humanwhocodes/momoa": "^2.0.3",
|
|
48
|
-
"@hono/zod-openapi": "^1.2.2",
|
|
49
53
|
"@readme/openapi-parser": "^5.5.0",
|
|
50
|
-
"
|
|
54
|
+
"ajv": "^8.20.0",
|
|
55
|
+
"ajv-formats": "^3.0.1",
|
|
56
|
+
"commander": "^14.0.0",
|
|
51
57
|
"openapi-types": "^12.1.3",
|
|
58
|
+
"yaml": "^2.8.3",
|
|
52
59
|
"zod": "^4.3.6"
|
|
53
60
|
}
|
|
54
61
|
}
|
package/src/cli/argv.ts
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pre-commander argv handling and shared argument parsers used across the
|
|
3
|
+
* commander tree. Pulled out of program.ts (TASK-190, m-11) so program.ts
|
|
4
|
+
* shrinks toward "just command registration" and these helpers can be
|
|
5
|
+
* unit-tested in isolation.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { InvalidArgumentError } from "commander";
|
|
9
|
+
import type { ReporterName } from "../core/reporter/types.ts";
|
|
10
|
+
|
|
11
|
+
// ── MSYS path preprocessing ──
|
|
12
|
+
//
|
|
13
|
+
// Git Bash on Windows converts API paths like "/users" → "C:/Program Files/Git/users".
|
|
14
|
+
// We reverse that for flags whose values are API paths, not filesystem paths.
|
|
15
|
+
|
|
16
|
+
const MSYS_PREFIX_RE = /^[A-Z]:[\\/](?:Program Files[\\/]Git|msys64|usr)[\\/]/i;
|
|
17
|
+
|
|
18
|
+
const API_PATH_FLAGS = new Set(["--path", "--json-path"]);
|
|
19
|
+
|
|
20
|
+
function stripMsysPath(value: string): string {
|
|
21
|
+
if (!MSYS_PREFIX_RE.test(value)) return value;
|
|
22
|
+
return value.replace(MSYS_PREFIX_RE, "/");
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Pre-process argv before commander sees it: undo Git Bash's MSYS path conversion
|
|
27
|
+
* for `--path` and `--json-path` values (both `--path X` and `--path=X` forms).
|
|
28
|
+
*/
|
|
29
|
+
export function preprocessArgv(argv: string[]): string[] {
|
|
30
|
+
const out = [...argv];
|
|
31
|
+
for (let i = 0; i < out.length; i++) {
|
|
32
|
+
const arg = out[i]!;
|
|
33
|
+
|
|
34
|
+
// --flag=value form
|
|
35
|
+
const eqIdx = arg.indexOf("=");
|
|
36
|
+
if (arg.startsWith("--") && eqIdx !== -1) {
|
|
37
|
+
const flag = arg.slice(0, eqIdx);
|
|
38
|
+
if (API_PATH_FLAGS.has(flag)) {
|
|
39
|
+
out[i] = `${flag}=${stripMsysPath(arg.slice(eqIdx + 1))}`;
|
|
40
|
+
}
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// --flag value form
|
|
45
|
+
if (API_PATH_FLAGS.has(arg)) {
|
|
46
|
+
const next = out[i + 1];
|
|
47
|
+
if (next !== undefined && !next.startsWith("-")) {
|
|
48
|
+
out[i + 1] = stripMsysPath(next);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return out;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// ── Argument parsers ──
|
|
56
|
+
|
|
57
|
+
export function parsePositiveInt(name: string): (raw: string) => number {
|
|
58
|
+
return (raw: string) => {
|
|
59
|
+
const n = Number.parseInt(raw, 10);
|
|
60
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
61
|
+
throw new InvalidArgumentError(`Invalid ${name} value: ${raw}`);
|
|
62
|
+
}
|
|
63
|
+
return n;
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** `--rate-limit` accepts a positive integer (req/sec cap) or the literal
|
|
68
|
+
* string `auto` (no static cap; throttle adaptively from ratelimit-* headers). */
|
|
69
|
+
export function parseRateLimit(raw: string): number | "auto" {
|
|
70
|
+
if (raw.toLowerCase() === "auto") return "auto";
|
|
71
|
+
const n = Number.parseInt(raw, 10);
|
|
72
|
+
if (Number.isNaN(n) || n <= 0) {
|
|
73
|
+
throw new InvalidArgumentError(`Invalid --rate-limit value: ${raw} (expected a positive integer or "auto")`);
|
|
74
|
+
}
|
|
75
|
+
return n;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function parseNonNegativeInt(name: string): (raw: string) => number {
|
|
79
|
+
return (raw: string) => {
|
|
80
|
+
const n = Number.parseInt(raw, 10);
|
|
81
|
+
if (Number.isNaN(n) || n < 0 || String(n) !== raw.trim()) {
|
|
82
|
+
throw new InvalidArgumentError(`Invalid ${name} value: ${raw} (expected a non-negative integer)`);
|
|
83
|
+
}
|
|
84
|
+
return n;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
export function parseInteger(name: string): (raw: string) => number {
|
|
89
|
+
return (raw: string) => {
|
|
90
|
+
const n = Number.parseInt(raw, 10);
|
|
91
|
+
if (Number.isNaN(n)) {
|
|
92
|
+
throw new InvalidArgumentError(`Invalid ${name} value: ${raw}`);
|
|
93
|
+
}
|
|
94
|
+
return n;
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function parsePercentage(raw: string): number {
|
|
99
|
+
const n = Number.parseInt(raw, 10);
|
|
100
|
+
if (Number.isNaN(n) || n < 0 || n > 100) {
|
|
101
|
+
throw new InvalidArgumentError(`Invalid --fail-on-coverage value: ${raw} (must be 0–100)`);
|
|
102
|
+
}
|
|
103
|
+
return n;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export const collect = (val: string, prev: string[]): string[] => [...prev, val];
|
|
107
|
+
|
|
108
|
+
const VALID_REPORTERS = new Set<string>(["console", "json", "junit"]);
|
|
109
|
+
|
|
110
|
+
export function parseReporter(raw: string): ReporterName {
|
|
111
|
+
if (!VALID_REPORTERS.has(raw)) {
|
|
112
|
+
throw new InvalidArgumentError(`Unknown reporter: ${raw}. Available: console, json, junit`);
|
|
113
|
+
}
|
|
114
|
+
return raw as ReporterName;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Helper: split repeatable values like ["a,b", "c"] → ["a", "b", "c"] */
|
|
118
|
+
export function flatSplit(values: string[] | undefined): string[] | undefined {
|
|
119
|
+
if (!values || values.length === 0) return undefined;
|
|
120
|
+
const out = values.flatMap((v) => v.split(",")).filter(Boolean);
|
|
121
|
+
return out.length > 0 ? out : undefined;
|
|
122
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `zond add api <name> --spec <path|url>` — register a new API in the
|
|
3
|
+
* current workspace.
|
|
4
|
+
*
|
|
5
|
+
* The split from `zond init --spec` exists so the two operations
|
|
6
|
+
* (workspace bootstrap vs. API registration) have separate names and
|
|
7
|
+
* separate skill mentions. `init --spec` still works as a deprecated
|
|
8
|
+
* alias.
|
|
9
|
+
*
|
|
10
|
+
* This command refuses to run when no workspace marker is present,
|
|
11
|
+
* pointing the user at `zond init` first. setupApi handles spec
|
|
12
|
+
* snapshot + artifact generation.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { setupApi, type SetupApiResult } from "../../core/setup-api.ts";
|
|
16
|
+
import { findWorkspaceRoot } from "../../core/workspace/root.ts";
|
|
17
|
+
import { jsonOk, jsonError, printJson, zerr } from "../json-envelope.ts";
|
|
18
|
+
import { printError, printSuccess } from "../output.ts";
|
|
19
|
+
|
|
20
|
+
export interface AddApiOptions {
|
|
21
|
+
name: string;
|
|
22
|
+
spec?: string;
|
|
23
|
+
baseUrl?: string;
|
|
24
|
+
dir?: string;
|
|
25
|
+
force?: boolean;
|
|
26
|
+
insecure?: boolean;
|
|
27
|
+
dbPath?: string;
|
|
28
|
+
json?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function addApiCommand(opts: AddApiOptions): Promise<number> {
|
|
32
|
+
const ws = findWorkspaceRoot();
|
|
33
|
+
if (ws.fromFallback) {
|
|
34
|
+
const m = `No workspace detected (no zond.config.yml / .zond / zond.db / apis marker). Run \`zond init\` first to bootstrap a workspace.`;
|
|
35
|
+
if (opts.json) printJson(jsonError("add-api", [m])); else printError(m);
|
|
36
|
+
return 2;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const envVars: Record<string, string> = {};
|
|
40
|
+
if (opts.baseUrl) envVars.base_url = opts.baseUrl;
|
|
41
|
+
|
|
42
|
+
let result: SetupApiResult;
|
|
43
|
+
try {
|
|
44
|
+
result = await setupApi({
|
|
45
|
+
name: opts.name,
|
|
46
|
+
spec: opts.spec,
|
|
47
|
+
dir: opts.dir,
|
|
48
|
+
envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
|
|
49
|
+
dbPath: opts.dbPath,
|
|
50
|
+
force: opts.force,
|
|
51
|
+
insecure: opts.insecure,
|
|
52
|
+
});
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const m = (err as Error).message;
|
|
55
|
+
// Tag known spec-ingest failures with a structured code so downstream
|
|
56
|
+
// tooling (skills, retry logic) can branch on it (ARV-145). Cyclic
|
|
57
|
+
// structures escape decycleSchema only when @readme/openapi-parser
|
|
58
|
+
// builds an unusual graph — surface that as spec_load_failure with a
|
|
59
|
+
// pointer to the underlying serializer message.
|
|
60
|
+
const isCycleError = /cyclic structures|spec_serialize_failed/i.test(m);
|
|
61
|
+
const errInput = isCycleError ? zerr("spec_load_failure", m) : m;
|
|
62
|
+
if (opts.json) printJson(jsonError("add-api", [errInput])); else printError(m);
|
|
63
|
+
return 2;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const mode: "spec" | "run-only" = opts.spec ? "spec" : "run-only";
|
|
67
|
+
const artifacts = mode === "spec"
|
|
68
|
+
? ["spec.json", ".api-catalog.yaml", ".api-resources.yaml", ".api-fixtures.yaml", ".env.yaml"]
|
|
69
|
+
: [".env.yaml"];
|
|
70
|
+
|
|
71
|
+
if (opts.json) {
|
|
72
|
+
printJson(jsonOk("add-api", {
|
|
73
|
+
api: opts.name,
|
|
74
|
+
mode,
|
|
75
|
+
collectionId: result.collectionId,
|
|
76
|
+
baseDir: result.baseDir,
|
|
77
|
+
testPath: result.testPath,
|
|
78
|
+
endpoints: result.specEndpoints,
|
|
79
|
+
artifacts,
|
|
80
|
+
}, result.warnings));
|
|
81
|
+
} else {
|
|
82
|
+
if (mode === "spec") {
|
|
83
|
+
printSuccess(`Registered API '${opts.name}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
|
|
84
|
+
process.stdout.write(` Artifacts: spec.json + .api-catalog.yaml + .api-resources.yaml + .api-fixtures.yaml\n`);
|
|
85
|
+
if (result.authVars && result.authVars.length > 0) {
|
|
86
|
+
const list = result.authVars.map((v) => `\`${v}\``).join(", ");
|
|
87
|
+
process.stdout.write(` Auth required: fill ${list} in ${result.baseDir}/.secrets.yaml (already wired via @secret in .env.yaml).\n`);
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(` Next: run \`zond doctor --api ${opts.name}\` to see required fixtures.\n`);
|
|
90
|
+
} else {
|
|
91
|
+
printSuccess(`Registered API '${opts.name}' at ${result.baseDir} (no spec — run-only mode)`);
|
|
92
|
+
process.stdout.write(` Artifacts: .env.yaml (base_url=${opts.baseUrl})\n`);
|
|
93
|
+
process.stdout.write(` Next: write tests in ${result.testPath}/, run \`zond run --api ${opts.name} <test.yaml>\`.\n`);
|
|
94
|
+
process.stdout.write(` To enable generate/probe/validate-schema, attach a spec: \`zond refresh-api ${opts.name} --spec <path|url>\`.\n`);
|
|
95
|
+
}
|
|
96
|
+
if (result.warnings) for (const w of result.warnings) process.stderr.write(`Warning: ${w}\n`);
|
|
97
|
+
}
|
|
98
|
+
return 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
import type { Command } from "commander";
|
|
102
|
+
import { globalJson } from "../resolve.ts";
|
|
103
|
+
|
|
104
|
+
export function registerAdd(program: Command): void {
|
|
105
|
+
const add = program.command("add").description("Register objects in the workspace");
|
|
106
|
+
add
|
|
107
|
+
.command("api <name>")
|
|
108
|
+
.description("Register an API: from an OpenAPI spec (full toolkit) or just --base-url (run-only mode)")
|
|
109
|
+
.option("--spec <path>", "Path or URL to OpenAPI spec — enables generate/probe/validate-schema")
|
|
110
|
+
.option("--base-url <url>", "Base URL recorded in .env.yaml (required if --spec is omitted)")
|
|
111
|
+
.option("--dir <path>", "Target directory (defaults to apis/<name>/)")
|
|
112
|
+
.option("--force", "Overwrite an existing API with the same name")
|
|
113
|
+
.option("--insecure", "Skip TLS verification when fetching the spec from https")
|
|
114
|
+
.option("--db <path>", "Path to SQLite database file")
|
|
115
|
+
.action(async (name: string, opts, cmd: Command) => {
|
|
116
|
+
const json = globalJson(cmd);
|
|
117
|
+
if (!opts.spec && !opts.baseUrl) {
|
|
118
|
+
const m = "Provide --spec <path|url> for a full registration, or --base-url <url> for run-only mode.";
|
|
119
|
+
if (json) printJson(jsonError("add-api", [m])); else printError(m);
|
|
120
|
+
process.exitCode = 2;
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
process.exitCode = await addApiCommand({
|
|
124
|
+
name,
|
|
125
|
+
spec: opts.spec,
|
|
126
|
+
baseUrl: opts.baseUrl,
|
|
127
|
+
dir: opts.dir,
|
|
128
|
+
force: opts.force === true,
|
|
129
|
+
insecure: opts.insecure === true,
|
|
130
|
+
dbPath: typeof opts.db === "string" ? opts.db : undefined,
|
|
131
|
+
json,
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-187 / idempotency: parser + expected shape.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import type { ResourcePatch } from "./overlay.ts";
|
|
7
|
+
import type { ResourceSlice } from "./prompts.ts";
|
|
8
|
+
|
|
9
|
+
const IdempotencySchema = z.object({
|
|
10
|
+
header: z.string().default("Idempotency-Key"),
|
|
11
|
+
scope: z.enum(["endpoint", "global"]).optional(),
|
|
12
|
+
ignore_response_fields: z.array(z.string()).optional(),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const ResponseSchema = z.object({
|
|
16
|
+
resource: z.string(),
|
|
17
|
+
idempotency: IdempotencySchema.nullable(),
|
|
18
|
+
rationale: z.string().optional(),
|
|
19
|
+
confidence: z.enum(["low", "medium", "high"]).optional(),
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export const EXPECTED_OUTPUT_SHAPE = {
|
|
23
|
+
resource: "string (echo input)",
|
|
24
|
+
idempotency: {
|
|
25
|
+
header: "string (header name, e.g. 'Idempotency-Key')",
|
|
26
|
+
scope: "endpoint | global (optional)",
|
|
27
|
+
ignore_response_fields: "string[] (optional — fields that change between replays, e.g. 'created')",
|
|
28
|
+
},
|
|
29
|
+
rationale: "string (optional)",
|
|
30
|
+
confidence: "low | medium | high",
|
|
31
|
+
null_form: "if create endpoint doesn't support idempotency-replay, return { resource, idempotency: null }",
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export function parseIdempotencyResponse(parsed: unknown, slice: ResourceSlice): { patch: ResourcePatch; audit: Record<string, unknown> } {
|
|
35
|
+
const validated = ResponseSchema.safeParse(parsed);
|
|
36
|
+
if (!validated.success) {
|
|
37
|
+
throw new Error(`idempotency response failed schema for ${slice.resource}: ${validated.error.message}`);
|
|
38
|
+
}
|
|
39
|
+
const v = validated.data;
|
|
40
|
+
if (v.idempotency == null) {
|
|
41
|
+
return {
|
|
42
|
+
patch: { resource: slice.resource },
|
|
43
|
+
audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence, dropped: "no Idempotency-Key support" },
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
patch: {
|
|
48
|
+
resource: slice.resource,
|
|
49
|
+
idempotency: {
|
|
50
|
+
header: v.idempotency.header,
|
|
51
|
+
scope: v.idempotency.scope,
|
|
52
|
+
ignore_response_fields: v.idempotency.ignore_response_fields,
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
audit: { resource: slice.resource, rationale: v.rationale, confidence: v.confidence },
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isApplicable(slice: ResourceSlice): boolean { return Boolean(slice.endpoints.create); }
|