@kirrosh/zond 0.20.0 → 0.22.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 +110 -3
- package/README.md +26 -15
- package/package.json +10 -6
- package/src/cli/commands/catalog.ts +62 -0
- package/src/cli/commands/ci-init.ts +12 -6
- package/src/cli/commands/completions.ts +176 -0
- package/src/cli/commands/db.ts +2 -1
- package/src/cli/commands/generate.ts +18 -2
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +79 -0
- package/src/cli/commands/init/skills.ts +45 -0
- package/src/cli/commands/init/templates/agents.md +73 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/scenarios.md +97 -0
- package/src/cli/commands/init/templates/skills/zond.md +184 -0
- package/src/cli/commands/init/templates/zond-config.yml +15 -0
- package/src/cli/commands/init.ts +124 -31
- package/src/cli/commands/probe-methods.ts +108 -0
- package/src/cli/commands/probe-validation.ts +124 -0
- package/src/cli/commands/run.ts +99 -10
- package/src/cli/commands/serve.ts +52 -19
- package/src/cli/commands/sync.ts +28 -1
- package/src/cli/commands/update.ts +1 -1
- package/src/cli/commands/use.ts +57 -0
- package/src/cli/index.ts +21 -591
- package/src/cli/program.ts +655 -0
- package/src/cli/version.ts +3 -0
- package/src/core/context/current.ts +35 -0
- package/src/core/diagnostics/db-analysis.ts +11 -2
- package/src/core/diagnostics/render-md.ts +112 -0
- package/src/core/generator/catalog-builder.ts +179 -0
- package/src/core/generator/chunker.ts +14 -2
- package/src/core/generator/data-factory.ts +50 -19
- package/src/core/generator/guide-builder.ts +1 -1
- package/src/core/generator/index.ts +2 -0
- package/src/core/generator/openapi-reader.ts +18 -0
- package/src/core/generator/serializer.ts +11 -2
- package/src/core/generator/suite-generator.ts +106 -7
- package/src/core/meta/types.ts +0 -2
- package/src/core/parser/schema.ts +3 -1
- package/src/core/parser/types.ts +10 -1
- package/src/core/parser/variables.ts +90 -2
- package/src/core/parser/yaml-parser.ts +50 -1
- package/src/core/probe/method-probe.ts +197 -0
- package/src/core/probe/negative-probe.ts +657 -0
- package/src/core/reporter/console.ts +29 -3
- package/src/core/reporter/index.ts +2 -2
- package/src/core/reporter/json.ts +5 -2
- package/src/core/runner/assertions.ts +4 -1
- package/src/core/runner/executor.ts +132 -37
- package/src/core/runner/http-client.ts +40 -5
- package/src/core/runner/rate-limiter.ts +131 -0
- package/src/core/setup-api.ts +4 -1
- package/src/core/workspace/root.ts +94 -0
- package/src/db/schema.ts +4 -1
- package/src/web/routes/api.ts +80 -0
- package/src/web/routes/dashboard.ts +15 -0
- package/src/web/static/style.css +290 -0
- package/src/web/views/explorer-tab.ts +402 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,12 +2,66 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
-
## [
|
|
5
|
+
## [0.22.0] — 2026-04-29
|
|
6
|
+
|
|
7
|
+
### Round-2 papercuts (TASK-68 → TASK-86)
|
|
8
|
+
|
|
9
|
+
- **TASK-68: `zond run --safe` (no path) no longer crashes with `paths[0] must be of type string, got boolean`.**
|
|
10
|
+
Commander's auto-negation `--no-db` defaulted `opts.db` to `true`; the boolean leaked into `path.resolve()` via a lazy
|
|
11
|
+
cast. dbPath is now normalised the same way as elsewhere; the no-path / no-`.zond-current` error is explicit and
|
|
12
|
+
mentions both `zond use <api>` and `--api`.
|
|
13
|
+
|
|
14
|
+
- **TASK-69: `zond db diagnose` no longer hides 5xx failures behind cluster summaries.**
|
|
15
|
+
`groupFailures` previously kept only the first item per group plus 2 examples — for `assertion_failed` clusters that's
|
|
16
|
+
fine, but for `api_error` (5xx) it silently dropped backend-bug evidence. 5xx groups are now always preserved in full
|
|
17
|
+
in `data.failures` and `examples`; assertion/network groups continue to fold.
|
|
18
|
+
|
|
19
|
+
- **TASK-71: YAML parse errors now report `file:line:col` plus a snippet with a column pointer.**
|
|
20
|
+
`Bun.YAML.parse` exposes JS-stack coordinates, not YAML positions — on failure we re-parse with `yaml` (eemeli) just
|
|
21
|
+
for diagnostics and surface `linePos` in the error. Pre-checks for embedded NUL bytes and points at the
|
|
22
|
+
`{{$nullByte}}` generator. Adds `yaml@2.8.3` dependency.
|
|
23
|
+
|
|
24
|
+
- **TASK-77: suite-level `parameterize: { key: [val, …] }` cross-product.**
|
|
25
|
+
Replaces copy-pasting one test across N endpoints. Multiple keys produce the cross-product. Captures and
|
|
26
|
+
tainted/missing-capture state are reset between iterations so values from one binding never leak into the next; step
|
|
27
|
+
names are interpolated through `{{var}}` so reporters and `db diagnose` can tell iterations apart.
|
|
28
|
+
|
|
29
|
+
- **TASK-79: `probe-validation` now pairs every mutating probe with a cleanup-DELETE.**
|
|
30
|
+
When a probe accidentally returns 2xx (the bug class probe-validation hunts for), the new follow-up `DELETE` step
|
|
31
|
+
(`always: true`) consumes a `leaked_id_<i>` capture and removes the resource. When the probe correctly gets 4xx, no id
|
|
32
|
+
is captured and the cleanup is skipped automatically. If the spec defines no DELETE counterpart, the generator emits a
|
|
33
|
+
warning instead. New `--no-cleanup` flag opts out for namespace-isolated test envs.
|
|
34
|
+
|
|
35
|
+
- **TASK-81: `--rate-limit auto` reads `RateLimit-*` response headers and adapts.**
|
|
36
|
+
Implements RFC `draft-ietf-httpapi-ratelimit-headers` plus the GitHub/Stripe `X-RateLimit-*` aliases. When `remaining`
|
|
37
|
+
drops to ≤5, subsequent requests pause until reset (relative-seconds vs Unix-timestamp distinguished by magnitude).
|
|
38
|
+
Static `--rate-limit N` benefits from the same hook — the cap is a floor, headers can push pauses out further.
|
|
39
|
+
|
|
40
|
+
- **TASK-86: `zond generate` honours `format` even when `type` is absent or array (OpenAPI 3.1 nullable).**
|
|
41
|
+
`format: email` on a schema with no `type` (or `type: ["string", "null"]`) used to fall through to the default branch
|
|
42
|
+
and produce `{{$randomString}}`. Format-to-placeholder mapping is now dispatched before the type switch.
|
|
6
43
|
|
|
7
44
|
### Breaking changes
|
|
8
45
|
|
|
9
|
-
- **MCP layer removed**
|
|
10
|
-
|
|
46
|
+
- **MCP layer removed** (see [decision-2](backlog/decisions/decision-2%20-%20Drop-MCP-server-—-keep-CLI-agent-skills-as-the-only-integration-surface.md)) —
|
|
47
|
+
CLI is the only integration surface; agent skills in `skills/*/SKILL.md`
|
|
48
|
+
are read directly. Specifically:
|
|
49
|
+
- `zond mcp start` removed.
|
|
50
|
+
- `zond install --claude/--cursor` removed (was only used to write
|
|
51
|
+
`~/.claude/mcp.json` / `~/.cursor/mcp.json` for the MCP transport).
|
|
52
|
+
- `--integration mcp` flag of `zond init` removed; default integration
|
|
53
|
+
is now `cli` (writes a self-contained `AGENTS.md` with full workflow
|
|
54
|
+
inline). `--integration skip` still works.
|
|
55
|
+
- `@modelcontextprotocol/sdk` runtime dependency dropped.
|
|
56
|
+
- `src/mcp/` deleted entirely (~817 LOC).
|
|
57
|
+
- `src/cli/commands/install.ts` and `src/cli/commands/mcp.ts` deleted.
|
|
58
|
+
- `tests/integration/mcp*.test.ts` removed.
|
|
59
|
+
- All MCP references purged from README, ZOND.md, docs/, skills/,
|
|
60
|
+
AGENTS.md, CLAUDE.md.
|
|
61
|
+
- Migration: existing `~/.claude/mcp.json` / `~/.cursor/mcp.json` keep
|
|
62
|
+
referencing a `zond` server that no longer responds; remove the
|
|
63
|
+
`zond` entry from your client config. New flow — see updated
|
|
64
|
+
`AGENTS.md`: agents call `zond` commands directly.
|
|
11
65
|
|
|
12
66
|
- **`zond migrate` removed** — the migration system was added and then removed in the same branch.
|
|
13
67
|
Format changes in zond are backward-compatible or require a clean `zond generate`.
|
|
@@ -100,6 +154,59 @@ All notable changes to this project will be documented in this file.
|
|
|
100
154
|
of the expected `404` (after a DELETE), the diagnostic now surfaces a "likely soft delete" hint
|
|
101
155
|
with a concrete suggestion to assert the status field value.
|
|
102
156
|
|
|
157
|
+
- **5xx response highlighting** — console reporter now flags failed steps with HTTP 5xx
|
|
158
|
+
responses with a yellow `[5xx <status>]` tag, and the suite/grand-total lines show a
|
|
159
|
+
separate `<N> 5xx` count. The `--json` envelope adds `http_status` and `is_5xx` per
|
|
160
|
+
failure plus a top-level `summary.fiveXx` count, so probe-validation runs surface
|
|
161
|
+
bug candidates at a glance.
|
|
162
|
+
|
|
163
|
+
- **`--report-out <file>`** on `zond run` — writes the JSON or JUnit report directly to a
|
|
164
|
+
file (with `mkdir -p`) instead of to stdout, logging `zond: <FORMAT> report written to
|
|
165
|
+
<path>` on stderr. Decouples the report from any wrapper banner that prefixes stdout
|
|
166
|
+
(notably `bun run zond -- run …`), so downstream JSON parsers don't break.
|
|
167
|
+
|
|
168
|
+
#### Bug-hunting probes
|
|
169
|
+
|
|
170
|
+
- **`zond probe-validation <spec>`** — generates deterministic negative-input probe
|
|
171
|
+
suites that catch the 5xx-on-bad-input class of bugs (the contract: any malformed
|
|
172
|
+
client input must produce a 4xx, never a 5xx). Per endpoint emits probes for: invalid
|
|
173
|
+
path UUIDs, empty body, missing required fields, type confusion, invalid format
|
|
174
|
+
(`email`/`uri`/`date-time`/`uuid`), boundary strings (empty, 10000-char,
|
|
175
|
+
unicode/emoji/RTL), invalid enum values and array-of-string-enum (catches the
|
|
176
|
+
webhooks-events bug shape). `--max-per-endpoint` caps probe count, `--tag` filters
|
|
177
|
+
endpoints. Generated suites embed suite-level `base_url`/auth and are runnable as-is.
|
|
178
|
+
|
|
179
|
+
- **`zond probe-methods <spec>`** — HTTP method completeness sweep. For every path,
|
|
180
|
+
emits one probe per `{GET, POST, PUT, PATCH, DELETE}` method that is *not* declared
|
|
181
|
+
in the spec, expecting a 4xx (`401/403/404/405`). Path placeholders are substituted
|
|
182
|
+
with valid-shape sentinels so the request reaches the router. Catches "PUT on a
|
|
183
|
+
POST-only endpoint returns 500" bugs.
|
|
184
|
+
|
|
185
|
+
- **`probe-validation --list-tags`** — lists all tags from the OpenAPI spec without
|
|
186
|
+
generating anything. `--tag X` is now case-insensitive and trims whitespace; matching
|
|
187
|
+
zero endpoints exits 2 with a clear error and the available-tags list.
|
|
188
|
+
|
|
189
|
+
#### Runner
|
|
190
|
+
|
|
191
|
+
- **`zond run --sequential`** — opt-out of parallel suite execution. Forces
|
|
192
|
+
sequential runs of all suites (useful when a setup token must propagate or when
|
|
193
|
+
rate-limits make parallel suites trigger 429s).
|
|
194
|
+
|
|
195
|
+
- **Auto-load `./.env.yaml`** — `zond run` now also tries `$PWD/.env.yaml` when
|
|
196
|
+
`--env` is not given and neither searchDir nor its parent has one. Logs
|
|
197
|
+
`zond: using ./.env.yaml (cwd fallback)` on stderr. Unblocks running absolute test
|
|
198
|
+
paths from a collection cwd.
|
|
199
|
+
|
|
200
|
+
#### Reporter / DB
|
|
201
|
+
|
|
202
|
+
- **Cascade-skip reason inline** — console reporter now prints
|
|
203
|
+
`(skipped: <error>)` instead of just `(skipped)`, surfacing the underlying
|
|
204
|
+
capture/auth failure on the very same line.
|
|
205
|
+
|
|
206
|
+
- **Run classification** — `zond db runs` now classifies a run with `total > 0`,
|
|
207
|
+
`passed == 0`, and many errors as **FAIL** instead of PASS. Prevents a probe run
|
|
208
|
+
with all 5xx responses from looking green in the runs listing.
|
|
209
|
+
|
|
103
210
|
---
|
|
104
211
|
|
|
105
212
|
### Fixes
|
package/README.md
CHANGED
|
@@ -4,35 +4,37 @@ 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
|
|
13
16
|
```
|
|
14
|
-
|
|
15
|
-
|
|
17
|
+
|
|
18
|
+
Bootstrap a workspace and register your first API:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
zond init --workspace --with-spec ./openapi.json
|
|
16
22
|
```
|
|
17
23
|
|
|
18
|
-
|
|
24
|
+
`zond init` writes a self-contained [`AGENTS.md`](AGENTS.md) — agents read it
|
|
25
|
+
and use the CLI directly (`zond run`, `zond probe-validation`,
|
|
26
|
+
`zond db diagnose`, …). No daemon, no transport, no extra configuration.
|
|
19
27
|
|
|
20
|
-
|
|
28
|
+
Then say to your agent: _"Safely cover the API from openapi.json with tests."_
|
|
21
29
|
|
|
22
30
|
<details>
|
|
23
|
-
<summary>Other installation methods (
|
|
24
|
-
|
|
25
|
-
### CLI / Binary
|
|
31
|
+
<summary>Other installation methods (npx)</summary>
|
|
26
32
|
|
|
27
33
|
```bash
|
|
28
34
|
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
35
|
```
|
|
34
36
|
|
|
35
|
-
See [ZOND.md](ZOND.md) for full CLI reference.
|
|
37
|
+
See [ZOND.md](ZOND.md) for the full CLI reference.
|
|
36
38
|
|
|
37
39
|
</details>
|
|
38
40
|
|
|
@@ -67,11 +69,20 @@ Claude Code can write pytest from scratch — but it takes 30-60 minutes per flo
|
|
|
67
69
|
"Set up CI for API tests"
|
|
68
70
|
```
|
|
69
71
|
|
|
72
|
+
## Shell completions
|
|
73
|
+
|
|
74
|
+
```bash
|
|
75
|
+
zond completions bash > ~/.local/share/bash-completion/completions/zond
|
|
76
|
+
zond completions zsh > ~/.zsh/completions/_zond # then `compinit`
|
|
77
|
+
zond completions fish > ~/.config/fish/completions/zond.fish
|
|
78
|
+
```
|
|
79
|
+
|
|
70
80
|
## Documentation
|
|
71
81
|
|
|
72
82
|
- [ZOND.md](ZOND.md) — full CLI reference
|
|
73
83
|
- [docs/quickstart.md](docs/quickstart.md) — step-by-step quickstart (RU)
|
|
74
84
|
- [docs/ci.md](docs/ci.md) — CI/CD integration
|
|
85
|
+
- [backlog/](backlog/) — project tasks (powered by [Backlog.md](https://backlog.md), see [docs/backlog.md](docs/backlog.md))
|
|
75
86
|
|
|
76
87
|
## License
|
|
77
88
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kirrosh/zond",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.22.0",
|
|
4
4
|
"description": "API testing platform — define tests in YAML, run from CLI or WebUI, generate from OpenAPI specs",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"module": "index.ts",
|
|
@@ -25,17 +25,20 @@
|
|
|
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/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/diagnostics/ tests/cli/
|
|
31
|
+
"test:unit": "bun test tests/db/ tests/parser/ tests/runner/ tests/generator/ tests/core/ tests/core/meta/ tests/diagnostics/ tests/cli/program.test.ts tests/cli/cli-smoke.test.ts tests/cli/completions.test.ts tests/cli/ci-init.test.ts tests/cli/commands.test.ts tests/cli/safe-run.test.ts tests/cli/json-envelope.test.ts tests/cli/describe.test.ts tests/cli/catalog.test.ts tests/cli/db.test.ts tests/cli/request.test.ts tests/cli/init.test.ts tests/cli/init/agents-md.test.ts tests/cli/init/bootstrap.test.ts tests/cli/use.test.ts tests/cli/guide.test.ts tests/cli/update.test.ts tests/cli/serve.test.ts tests/integration/ tests/web/ tests/reporter/",
|
|
30
32
|
"test:mocked": "bun run scripts/run-mocked-tests.ts",
|
|
31
33
|
"check": "tsc --noEmit --project tsconfig.json",
|
|
34
|
+
"lint:dead": "knip --reporter compact",
|
|
32
35
|
"build": "bun build --compile src/cli/index.ts --outfile zond",
|
|
33
|
-
"version:sync": "bun run scripts/sync-version.ts",
|
|
34
|
-
"postversion": "bun run scripts/sync-version.ts && git add .claude-plugin/plugin.json",
|
|
35
36
|
"bench:api": "bun benchmarks/api/server.ts"
|
|
36
37
|
},
|
|
37
38
|
"devDependencies": {
|
|
38
|
-
"@types/bun": "latest"
|
|
39
|
+
"@types/bun": "latest",
|
|
40
|
+
"backlog.md": "^1.44.0",
|
|
41
|
+
"knip": "^6.7.0"
|
|
39
42
|
},
|
|
40
43
|
"engines": {
|
|
41
44
|
"bun": ">=1.1.0"
|
|
@@ -44,11 +47,12 @@
|
|
|
44
47
|
"typescript": "^5"
|
|
45
48
|
},
|
|
46
49
|
"dependencies": {
|
|
47
|
-
"@humanwhocodes/momoa": "^2.0.3",
|
|
48
50
|
"@hono/zod-openapi": "^1.2.2",
|
|
49
51
|
"@readme/openapi-parser": "^5.5.0",
|
|
52
|
+
"commander": "^14.0.0",
|
|
50
53
|
"hono": "^4.12.2",
|
|
51
54
|
"openapi-types": "^12.1.3",
|
|
55
|
+
"yaml": "^2.8.3",
|
|
52
56
|
"zod": "^4.3.6"
|
|
53
57
|
}
|
|
54
58
|
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { join } from "path";
|
|
2
|
+
import { mkdir } from "fs/promises";
|
|
3
|
+
import { readOpenApiSpec, extractEndpoints, extractSecuritySchemes } from "../../core/generator/index.ts";
|
|
4
|
+
import { buildCatalog, serializeCatalog } from "../../core/generator/catalog-builder.ts";
|
|
5
|
+
import { decycleSchema } from "../../core/generator/schema-utils.ts";
|
|
6
|
+
import { hashSpec } from "../../core/meta/meta-store.ts";
|
|
7
|
+
import { printError, printSuccess } from "../output.ts";
|
|
8
|
+
import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
|
|
9
|
+
|
|
10
|
+
export interface CatalogOptions {
|
|
11
|
+
specPath: string;
|
|
12
|
+
output?: string;
|
|
13
|
+
json?: boolean;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export async function catalogCommand(options: CatalogOptions): Promise<number> {
|
|
17
|
+
try {
|
|
18
|
+
const doc = await readOpenApiSpec(options.specPath);
|
|
19
|
+
const endpoints = extractEndpoints(doc);
|
|
20
|
+
const securitySchemes = extractSecuritySchemes(doc);
|
|
21
|
+
const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
|
|
22
|
+
const apiName = (doc as any).info?.title as string | undefined;
|
|
23
|
+
const apiVersion = (doc as any).info?.version as string | undefined;
|
|
24
|
+
const specContent = JSON.stringify(decycleSchema(doc));
|
|
25
|
+
|
|
26
|
+
const catalog = buildCatalog({
|
|
27
|
+
endpoints,
|
|
28
|
+
securitySchemes,
|
|
29
|
+
specSource: options.specPath,
|
|
30
|
+
specHash: hashSpec(specContent),
|
|
31
|
+
apiName,
|
|
32
|
+
apiVersion,
|
|
33
|
+
baseUrl,
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const outputDir = options.output ?? ".";
|
|
37
|
+
await mkdir(outputDir, { recursive: true });
|
|
38
|
+
|
|
39
|
+
const catalogPath = join(outputDir, ".api-catalog.yaml");
|
|
40
|
+
await Bun.write(catalogPath, serializeCatalog(catalog));
|
|
41
|
+
|
|
42
|
+
if (options.json) {
|
|
43
|
+
printJson(jsonOk("catalog", {
|
|
44
|
+
path: catalogPath,
|
|
45
|
+
endpointCount: catalog.endpointCount,
|
|
46
|
+
apiName: catalog.apiName,
|
|
47
|
+
}));
|
|
48
|
+
} else {
|
|
49
|
+
printSuccess(`Generated API catalog: ${catalogPath} (${catalog.endpointCount} endpoints)`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return 0;
|
|
53
|
+
} catch (err) {
|
|
54
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
55
|
+
if (options.json) {
|
|
56
|
+
printJson(jsonError("catalog", [message]));
|
|
57
|
+
} else {
|
|
58
|
+
printError(message);
|
|
59
|
+
}
|
|
60
|
+
return 2;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -42,13 +42,16 @@ jobs:
|
|
|
42
42
|
- name: Run smoke tests (read-only, safe for production)
|
|
43
43
|
run: |
|
|
44
44
|
mkdir -p test-results
|
|
45
|
-
|
|
45
|
+
# --exclude-tag needs-id skips positive smoke that needs real IDs from .env.yaml
|
|
46
|
+
zond run apis/ --tag smoke --exclude-tag needs-id --safe --report junit --no-db > test-results/smoke.xml
|
|
46
47
|
# Use --env-var "API_KEY=\${{ secrets.API_KEY }}" to inject secrets without writing to disk
|
|
47
48
|
continue-on-error: true
|
|
48
49
|
|
|
49
|
-
- name: Run CRUD tests (staging only)
|
|
50
|
+
- name: Run CRUD tests (staging only — ephemeral suites only)
|
|
50
51
|
run: |
|
|
51
|
-
|
|
52
|
+
# --exclude-tag persistent-write keeps only ephemeral CRUD (suites that DELETE what they create).
|
|
53
|
+
# Drop --exclude-tag persistent-write to opt into write suites that leave residual data.
|
|
54
|
+
zond run apis/ --tag crud --exclude-tag persistent-write --env staging --report junit --no-db > test-results/crud.xml
|
|
52
55
|
# Add --env-var "BASE_URL=\${{ secrets.STAGING_URL }}" for staging URL
|
|
53
56
|
continue-on-error: true
|
|
54
57
|
|
|
@@ -86,8 +89,9 @@ api-smoke:
|
|
|
86
89
|
- curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
|
|
87
90
|
script:
|
|
88
91
|
- mkdir -p test-results
|
|
89
|
-
# Use --env-var to inject secrets without writing to disk
|
|
90
|
-
-
|
|
92
|
+
# Use --env-var to inject secrets without writing to disk.
|
|
93
|
+
# --exclude-tag needs-id skips positive smoke that needs real IDs from .env.yaml.
|
|
94
|
+
- zond run apis/ --tag smoke --exclude-tag needs-id --safe --report junit --no-db --env-var "API_KEY=$API_KEY" > test-results/smoke.xml
|
|
91
95
|
allow_failure:
|
|
92
96
|
exit_codes: 1
|
|
93
97
|
artifacts:
|
|
@@ -102,7 +106,9 @@ api-crud:
|
|
|
102
106
|
- curl -fsSL https://raw.githubusercontent.com/kirrosh/zond/master/install.sh | sh
|
|
103
107
|
script:
|
|
104
108
|
- mkdir -p test-results
|
|
105
|
-
-
|
|
109
|
+
# --exclude-tag persistent-write keeps only ephemeral CRUD (suites that DELETE what they create).
|
|
110
|
+
# Drop --exclude-tag persistent-write to opt into write suites that leave residual data.
|
|
111
|
+
- zond run apis/ --tag crud --exclude-tag persistent-write --env staging --report junit --no-db > test-results/crud.xml
|
|
106
112
|
allow_failure:
|
|
107
113
|
exit_codes: 1
|
|
108
114
|
artifacts:
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shell completion script generator.
|
|
3
|
+
*
|
|
4
|
+
* Builds a static completion script from a commander program tree. No external
|
|
5
|
+
* completion library — bash/zsh/fish templates are inlined.
|
|
6
|
+
*
|
|
7
|
+
* Install:
|
|
8
|
+
* bash: zond completions bash > ~/.local/share/bash-completion/completions/zond
|
|
9
|
+
* zsh: zond completions zsh > ~/.zsh/completions/_zond (then `compinit`)
|
|
10
|
+
* fish: zond completions fish > ~/.config/fish/completions/zond.fish
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type { Command } from "commander";
|
|
14
|
+
|
|
15
|
+
export const COMPLETION_SHELLS = ["bash", "zsh", "fish"] as const;
|
|
16
|
+
export type CompletionShell = (typeof COMPLETION_SHELLS)[number];
|
|
17
|
+
|
|
18
|
+
export interface CompletionsOptions {
|
|
19
|
+
shell: CompletionShell;
|
|
20
|
+
program: Command;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface CommandSpec {
|
|
24
|
+
name: string;
|
|
25
|
+
description: string;
|
|
26
|
+
options: string[]; // long flags only, e.g. ["--tag", "--safe"]
|
|
27
|
+
subcommands: CommandSpec[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function extractSpec(cmd: Command): CommandSpec {
|
|
31
|
+
// Option's internal shape exposes `long`; commander v14 keeps this stable.
|
|
32
|
+
const rawOpts = cmd.options as unknown as Array<{ long?: string }>;
|
|
33
|
+
const opts: string[] = rawOpts
|
|
34
|
+
.map((o) => o.long)
|
|
35
|
+
.filter((s): s is string => typeof s === "string");
|
|
36
|
+
|
|
37
|
+
return {
|
|
38
|
+
name: cmd.name(),
|
|
39
|
+
description: cmd.description(),
|
|
40
|
+
options: opts,
|
|
41
|
+
// Drop commander's auto `help` subcommand to keep templates lean.
|
|
42
|
+
subcommands: cmd.commands
|
|
43
|
+
.filter((c) => c.name() !== "help")
|
|
44
|
+
.map(extractSpec),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// ── bash ──
|
|
49
|
+
|
|
50
|
+
function renderBash(root: CommandSpec): string {
|
|
51
|
+
const topLevel = root.subcommands.map((s) => s.name).join(" ");
|
|
52
|
+
|
|
53
|
+
const perCmd = root.subcommands
|
|
54
|
+
.map((sub) => {
|
|
55
|
+
const flags = sub.options.join(" ");
|
|
56
|
+
const subs = sub.subcommands.map((s) => s.name).join(" ");
|
|
57
|
+
return ` ${sub.name})
|
|
58
|
+
COMPREPLY=( $(compgen -W "${[flags, subs].filter(Boolean).join(" ")}" -- "$cur") )
|
|
59
|
+
return 0
|
|
60
|
+
;;`;
|
|
61
|
+
})
|
|
62
|
+
.join("\n");
|
|
63
|
+
|
|
64
|
+
return `# bash completion for zond — generated by 'zond completions bash'
|
|
65
|
+
_zond_completion() {
|
|
66
|
+
local cur prev
|
|
67
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
68
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
69
|
+
|
|
70
|
+
if [ "\$COMP_CWORD" -eq 1 ]; then
|
|
71
|
+
COMPREPLY=( $(compgen -W "${topLevel} --help --version" -- "$cur") )
|
|
72
|
+
return 0
|
|
73
|
+
fi
|
|
74
|
+
|
|
75
|
+
case "\${COMP_WORDS[1]}" in
|
|
76
|
+
${perCmd}
|
|
77
|
+
esac
|
|
78
|
+
}
|
|
79
|
+
complete -F _zond_completion zond
|
|
80
|
+
`;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// ── zsh ──
|
|
84
|
+
|
|
85
|
+
function renderZsh(root: CommandSpec): string {
|
|
86
|
+
const cmdLines = root.subcommands
|
|
87
|
+
.map((s) => ` '${s.name}:${s.description.replace(/'/g, "'\\''")}'`)
|
|
88
|
+
.join(" \\\n");
|
|
89
|
+
|
|
90
|
+
const perCmd = root.subcommands
|
|
91
|
+
.map((sub) => {
|
|
92
|
+
const flagSpecs = sub.options
|
|
93
|
+
.map((f) => ` '${f}[${f.slice(2)}]'`)
|
|
94
|
+
.join(" \\\n");
|
|
95
|
+
const subCmds = sub.subcommands.length > 0
|
|
96
|
+
? `\n _values 'subcommand' \\\n` +
|
|
97
|
+
sub.subcommands.map((s) => ` '${s.name}[${s.description.replace(/'/g, "'\\''")}]'`).join(" \\\n")
|
|
98
|
+
: "";
|
|
99
|
+
return ` ${sub.name})
|
|
100
|
+
_arguments \\
|
|
101
|
+
${flagSpecs || " ':path:_files'"}${subCmds}
|
|
102
|
+
;;`;
|
|
103
|
+
})
|
|
104
|
+
.join("\n");
|
|
105
|
+
|
|
106
|
+
return `#compdef zond
|
|
107
|
+
# zsh completion for zond — generated by 'zond completions zsh'
|
|
108
|
+
|
|
109
|
+
_zond() {
|
|
110
|
+
local context state state_descr line
|
|
111
|
+
local -A opt_args
|
|
112
|
+
|
|
113
|
+
_arguments -C \\
|
|
114
|
+
'1: :->command' \\
|
|
115
|
+
'*::arg:->args'
|
|
116
|
+
|
|
117
|
+
case $state in
|
|
118
|
+
command)
|
|
119
|
+
_values 'zond command' \\
|
|
120
|
+
${cmdLines}
|
|
121
|
+
;;
|
|
122
|
+
args)
|
|
123
|
+
case $line[1] in
|
|
124
|
+
${perCmd}
|
|
125
|
+
esac
|
|
126
|
+
;;
|
|
127
|
+
esac
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_zond "$@"
|
|
131
|
+
`;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── fish ──
|
|
135
|
+
|
|
136
|
+
function renderFish(root: CommandSpec): string {
|
|
137
|
+
const lines: string[] = [
|
|
138
|
+
"# fish completion for zond — generated by 'zond completions fish'",
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
for (const sub of root.subcommands) {
|
|
142
|
+
const desc = sub.description.replace(/'/g, "\\'");
|
|
143
|
+
lines.push(`complete -c zond -n '__fish_use_subcommand' -a '${sub.name}' -d '${desc}'`);
|
|
144
|
+
}
|
|
145
|
+
lines.push(`complete -c zond -n '__fish_use_subcommand' -l help -d 'Show help'`);
|
|
146
|
+
lines.push(`complete -c zond -n '__fish_use_subcommand' -l version -d 'Show version'`);
|
|
147
|
+
|
|
148
|
+
for (const sub of root.subcommands) {
|
|
149
|
+
const condition = `__fish_seen_subcommand_from ${sub.name}`;
|
|
150
|
+
for (const opt of sub.options) {
|
|
151
|
+
lines.push(`complete -c zond -n '${condition}' -l ${opt.slice(2)}`);
|
|
152
|
+
}
|
|
153
|
+
for (const nested of sub.subcommands) {
|
|
154
|
+
const desc = nested.description.replace(/'/g, "\\'");
|
|
155
|
+
lines.push(`complete -c zond -n '${condition}' -a '${nested.name}' -d '${desc}'`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
return lines.join("\n") + "\n";
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ── Public entry ──
|
|
163
|
+
|
|
164
|
+
export function completionsCommand(options: CompletionsOptions): number {
|
|
165
|
+
const spec = extractSpec(options.program);
|
|
166
|
+
|
|
167
|
+
let script: string;
|
|
168
|
+
switch (options.shell) {
|
|
169
|
+
case "bash": script = renderBash(spec); break;
|
|
170
|
+
case "zsh": script = renderZsh(spec); break;
|
|
171
|
+
case "fish": script = renderFish(spec); break;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
process.stdout.write(script);
|
|
175
|
+
return 0;
|
|
176
|
+
}
|
package/src/cli/commands/db.ts
CHANGED
|
@@ -46,7 +46,8 @@ export async function dbCommand(options: DbOptions): Promise<number> {
|
|
|
46
46
|
} else {
|
|
47
47
|
for (const r of runs) {
|
|
48
48
|
const run = r as any;
|
|
49
|
-
const
|
|
49
|
+
const isFail = run.failed > 0 || (run.total > 0 && run.passed === 0);
|
|
50
|
+
const status = isFail ? "FAIL" : "PASS";
|
|
50
51
|
console.log(`#${run.id} ${status} ${run.passed}/${run.total} passed (${run.started_at})`);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
@@ -7,6 +7,8 @@ import {
|
|
|
7
7
|
scanCoveredEndpoints,
|
|
8
8
|
filterUncoveredEndpoints,
|
|
9
9
|
serializeSuite,
|
|
10
|
+
buildCatalog,
|
|
11
|
+
serializeCatalog,
|
|
10
12
|
} from "../../core/generator/index.ts";
|
|
11
13
|
import { generateSuites, findUnresolvedVars } from "../../core/generator/suite-generator.ts";
|
|
12
14
|
import { filterByTag } from "../../core/generator/chunker.ts";
|
|
@@ -30,9 +32,12 @@ export interface GenerateOptions {
|
|
|
30
32
|
export async function generateCommand(options: GenerateOptions): Promise<number> {
|
|
31
33
|
try {
|
|
32
34
|
const doc = await readOpenApiSpec(options.specPath);
|
|
33
|
-
|
|
35
|
+
const allEndpoints = extractEndpoints(doc);
|
|
36
|
+
let endpoints = allEndpoints;
|
|
34
37
|
const securitySchemes = extractSecuritySchemes(doc);
|
|
35
38
|
const baseUrl = ((doc as any).servers?.[0]?.url) as string | undefined;
|
|
39
|
+
const apiName = (doc as any).info?.title as string | undefined;
|
|
40
|
+
const apiVersion = (doc as any).info?.version as string | undefined;
|
|
36
41
|
const warnings: string[] = [];
|
|
37
42
|
|
|
38
43
|
// Filter to uncovered only
|
|
@@ -87,11 +92,22 @@ export async function generateCommand(options: GenerateOptions): Promise<number>
|
|
|
87
92
|
await writeMeta(options.output, {
|
|
88
93
|
zondVersion: ZOND_VERSION,
|
|
89
94
|
lastSyncedAt: new Date().toISOString(),
|
|
90
|
-
specUrl: options.specPath,
|
|
91
95
|
specHash: hashSpec(specContent),
|
|
92
96
|
files: { ...(existingMeta?.files ?? {}), ...metaFiles },
|
|
93
97
|
});
|
|
94
98
|
|
|
99
|
+
// Generate .api-catalog.yaml (always uses full unfiltered endpoint list)
|
|
100
|
+
const catalog = buildCatalog({
|
|
101
|
+
endpoints: allEndpoints,
|
|
102
|
+
securitySchemes,
|
|
103
|
+
specSource: options.specPath,
|
|
104
|
+
specHash: hashSpec(specContent),
|
|
105
|
+
apiName,
|
|
106
|
+
apiVersion,
|
|
107
|
+
baseUrl,
|
|
108
|
+
});
|
|
109
|
+
await Bun.write(join(options.output, ".api-catalog.yaml"), serializeCatalog(catalog));
|
|
110
|
+
|
|
95
111
|
// Sync DB collection spec reference if one is registered for this output directory
|
|
96
112
|
try {
|
|
97
113
|
getDb();
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
|
|
4
|
+
import agentsTemplate from "./templates/agents.md" with { type: "text" };
|
|
5
|
+
|
|
6
|
+
export const START_MARKER = "<!-- zond:start -->";
|
|
7
|
+
export const END_MARKER = "<!-- zond:end -->";
|
|
8
|
+
|
|
9
|
+
export interface AgentsBlockResult {
|
|
10
|
+
path: string;
|
|
11
|
+
action: "created" | "updated" | "noop";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function blockBody(): string {
|
|
15
|
+
return agentsTemplate.trim();
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function wrap(body: string): string {
|
|
19
|
+
return `${START_MARKER}\n${body}\n${END_MARKER}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const BLOCK_RE = new RegExp(
|
|
23
|
+
`${escapeRe(START_MARKER)}[\\s\\S]*?${escapeRe(END_MARKER)}`,
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
function escapeRe(s: string): string {
|
|
27
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Idempotently inserts (or updates) the zond instruction block in `<cwd>/AGENTS.md`.
|
|
32
|
+
*
|
|
33
|
+
* - Missing file → create with just the block.
|
|
34
|
+
* - File without markers → append block at the end (preceded by `\n\n---\n\n`).
|
|
35
|
+
* - File with existing markers → replace the body between them.
|
|
36
|
+
* - File whose existing block already matches → noop.
|
|
37
|
+
*/
|
|
38
|
+
export function upsertAgentsBlock(cwd: string): AgentsBlockResult {
|
|
39
|
+
const path = join(cwd, "AGENTS.md");
|
|
40
|
+
const next = wrap(blockBody());
|
|
41
|
+
|
|
42
|
+
if (!existsSync(path)) {
|
|
43
|
+
writeFileSync(path, next + "\n", "utf-8");
|
|
44
|
+
return { path, action: "created" };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const current = readFileSync(path, "utf-8");
|
|
48
|
+
|
|
49
|
+
if (BLOCK_RE.test(current)) {
|
|
50
|
+
const updated = current.replace(BLOCK_RE, next);
|
|
51
|
+
if (updated === current) return { path, action: "noop" };
|
|
52
|
+
writeFileSync(path, updated, "utf-8");
|
|
53
|
+
return { path, action: "updated" };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Append with separator
|
|
57
|
+
const sep = current.endsWith("\n") ? "\n" : "\n\n";
|
|
58
|
+
const updated = current + sep + "---\n\n" + next + "\n";
|
|
59
|
+
writeFileSync(path, updated, "utf-8");
|
|
60
|
+
return { path, action: "updated" };
|
|
61
|
+
}
|