@kirrosh/zond 0.13.0 → 0.16.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +1 -1
  3. package/package.json +4 -7
  4. package/src/cli/commands/ci-init.ts +12 -1
  5. package/src/cli/commands/coverage.ts +21 -1
  6. package/src/cli/commands/db.ts +121 -0
  7. package/src/cli/commands/describe.ts +60 -0
  8. package/src/cli/commands/generate.ts +127 -0
  9. package/src/cli/commands/guide.ts +127 -0
  10. package/src/cli/commands/init.ts +50 -77
  11. package/src/cli/commands/request.ts +57 -0
  12. package/src/cli/commands/run.ts +53 -10
  13. package/src/cli/commands/serve.ts +62 -3
  14. package/src/cli/commands/validate.ts +18 -2
  15. package/src/cli/index.ts +213 -215
  16. package/src/cli/json-envelope.ts +19 -0
  17. package/src/core/diagnostics/db-analysis.ts +351 -0
  18. package/src/core/diagnostics/failure-hints.ts +1 -0
  19. package/src/core/generator/data-factory.ts +19 -8
  20. package/src/core/generator/describe.ts +250 -0
  21. package/src/core/generator/guide-builder.ts +20 -0
  22. package/src/core/generator/index.ts +0 -3
  23. package/src/core/generator/suite-generator.ts +133 -20
  24. package/src/core/runner/executor.ts +1 -0
  25. package/src/core/runner/send-request.ts +94 -0
  26. package/src/core/runner/types.ts +1 -0
  27. package/src/db/queries.ts +4 -2
  28. package/src/db/schema.ts +11 -3
  29. package/src/mcp/descriptions.ts +0 -24
  30. package/src/mcp/server.ts +1 -8
  31. package/src/mcp/tools/describe-endpoint.ts +3 -218
  32. package/src/mcp/tools/query-db.ts +6 -222
  33. package/src/mcp/tools/run-tests.ts +1 -0
  34. package/src/mcp/tools/send-request.ts +15 -61
  35. package/src/web/views/suites-tab.ts +1 -1
  36. package/src/cli/commands/add-api.ts +0 -53
  37. package/src/cli/commands/ai-generate.ts +0 -106
  38. package/src/cli/commands/chat.ts +0 -43
  39. package/src/cli/commands/collections.ts +0 -41
  40. package/src/cli/commands/compare.ts +0 -129
  41. package/src/cli/commands/doctor.ts +0 -127
  42. package/src/cli/commands/runs.ts +0 -108
  43. package/src/cli/commands/update.ts +0 -142
  44. package/src/core/agent/agent-loop.ts +0 -116
  45. package/src/core/agent/context-manager.ts +0 -41
  46. package/src/core/agent/system-prompt.ts +0 -27
  47. package/src/core/agent/tools/diagnose-failure.ts +0 -51
  48. package/src/core/agent/tools/index.ts +0 -42
  49. package/src/core/agent/tools/query-results.ts +0 -40
  50. package/src/core/agent/tools/run-tests.ts +0 -38
  51. package/src/core/agent/tools/send-request.ts +0 -44
  52. package/src/core/agent/types.ts +0 -22
  53. package/src/core/generator/ai/ai-generator.ts +0 -61
  54. package/src/core/generator/ai/llm-client.ts +0 -159
  55. package/src/core/generator/ai/output-parser.ts +0 -307
  56. package/src/core/generator/ai/prompt-builder.ts +0 -153
  57. package/src/core/generator/ai/types.ts +0 -56
  58. package/src/mcp/tools/generate-and-save.ts +0 -202
  59. package/src/mcp/tools/save-test-suite.ts +0 -218
  60. package/src/mcp/tools/set-work-dir.ts +0 -35
  61. package/src/tui/chat-ui.ts +0 -150
@@ -1,84 +1,57 @@
1
- import { resolve, dirname } from "path";
2
- import { existsSync, mkdirSync, writeFileSync } from "fs";
3
- import { printSuccess } from "../output.ts";
4
-
5
- export interface InitCommandOptions {
6
- force: boolean;
1
+ import { setupApi } from "../../core/setup-api.ts";
2
+ import { printError, printSuccess } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface InitOptions {
6
+ name?: string;
7
+ spec?: string;
8
+ baseUrl?: string;
9
+ dir?: string;
10
+ force?: boolean;
11
+ insecure?: boolean;
12
+ dbPath?: string;
13
+ json?: boolean;
7
14
  }
8
15
 
9
- const EXAMPLE_TEST = `name: Example Smoke Test
10
- base_url: "{{base_url}}"
11
-
12
- tests:
13
- - name: "List posts"
14
- GET: /posts
15
- expect:
16
- status: 200
17
- body:
18
- id: { type: integer }
19
-
20
- - name: "Get single post"
21
- GET: /posts/1
22
- expect:
23
- status: 200
24
- body:
25
- id: { equals: 1 }
26
- title: { type: string }
27
- `;
28
-
29
- const ENV_DEV = `base_url: https://jsonplaceholder.typicode.com
30
- `;
31
-
32
- const MCP_CONFIG = `{
33
- "mcpServers": {
34
- "zond": {
35
- "command": "zond",
36
- "args": ["mcp"]
37
- }
38
- }
39
- }
40
- `;
41
-
42
- function writeIfMissing(filePath: string, content: string, force: boolean): boolean {
43
- if (!force && existsSync(filePath)) {
44
- console.log(` Skipped ${filePath} (already exists)`);
45
- return false;
46
- }
47
- const dir = dirname(filePath);
48
- if (!existsSync(dir)) {
49
- mkdirSync(dir, { recursive: true });
50
- }
51
- writeFileSync(filePath, content, "utf-8");
52
- console.log(` Created ${filePath}`);
53
- return true;
54
- }
55
-
56
- function isClaudeCodeAvailable(): boolean {
16
+ export async function initCommand(options: InitOptions): Promise<number> {
57
17
  try {
58
- const result = Bun.spawnSync(["claude", "--version"], {
59
- stdout: "ignore",
60
- stderr: "ignore",
18
+ const envVars: Record<string, string> = {};
19
+ if (options.baseUrl) envVars.base_url = options.baseUrl;
20
+
21
+ const result = await setupApi({
22
+ name: options.name,
23
+ spec: options.spec,
24
+ dir: options.dir,
25
+ envVars: Object.keys(envVars).length > 0 ? envVars : undefined,
26
+ dbPath: options.dbPath,
27
+ force: options.force,
28
+ insecure: options.insecure,
61
29
  });
62
- return result.exitCode === 0;
63
- } catch {
64
- return false;
65
- }
66
- }
67
-
68
- export async function initCommand(options: InitCommandOptions): Promise<number> {
69
- const cwd = process.cwd();
70
30
 
71
- console.log("Initializing zond project...\n");
72
-
73
- writeIfMissing(resolve(cwd, "tests/example.yaml"), EXAMPLE_TEST, options.force);
74
- writeIfMissing(resolve(cwd, ".env.dev.yaml"), ENV_DEV, options.force);
75
-
76
- // Create .mcp.json if Claude Code is detected
77
- if (isClaudeCodeAvailable()) {
78
- writeIfMissing(resolve(cwd, ".mcp.json"), MCP_CONFIG, options.force);
79
- printSuccess("Claude Code detected — .mcp.json created");
31
+ if (options.json) {
32
+ printJson(jsonOk("init", {
33
+ collectionId: result.collectionId,
34
+ baseDir: result.baseDir,
35
+ testPath: result.testPath,
36
+ endpoints: result.specEndpoints,
37
+ warnings: result.warnings ?? [],
38
+ }, result.warnings));
39
+ } else {
40
+ printSuccess(`Created API '${options.name ?? "api"}' at ${result.baseDir} (${result.specEndpoints} endpoints)`);
41
+ if (result.warnings) {
42
+ for (const w of result.warnings) {
43
+ process.stderr.write(`Warning: ${w}\n`);
44
+ }
45
+ }
46
+ }
47
+ return 0;
48
+ } catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ if (options.json) {
51
+ printJson(jsonError("init", [message]));
52
+ } else {
53
+ printError(message);
54
+ }
55
+ return 2;
80
56
  }
81
-
82
- console.log("\nReady! Run: zond run tests/");
83
- return 0;
84
57
  }
@@ -0,0 +1,57 @@
1
+ import { sendAdHocRequest } from "../../core/runner/send-request.ts";
2
+ import { printError } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
4
+
5
+ export interface RequestOptions {
6
+ method: string;
7
+ url: string;
8
+ headers?: string[];
9
+ body?: string;
10
+ timeout?: number;
11
+ env?: string;
12
+ api?: string;
13
+ jsonPath?: string;
14
+ dbPath?: string;
15
+ json?: boolean;
16
+ }
17
+
18
+ export async function requestCommand(options: RequestOptions): Promise<number> {
19
+ try {
20
+ const headers: Record<string, string> = {};
21
+ if (options.headers) {
22
+ for (const h of options.headers) {
23
+ const colonIdx = h.indexOf(":");
24
+ if (colonIdx > 0) {
25
+ headers[h.slice(0, colonIdx).trim()] = h.slice(colonIdx + 1).trim();
26
+ }
27
+ }
28
+ }
29
+
30
+ const result = await sendAdHocRequest({
31
+ method: options.method.toUpperCase(),
32
+ url: options.url,
33
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
34
+ body: options.body,
35
+ timeout: options.timeout,
36
+ envName: options.env,
37
+ collectionName: options.api,
38
+ jsonPath: options.jsonPath,
39
+ dbPath: options.dbPath,
40
+ });
41
+
42
+ if (options.json) {
43
+ printJson(jsonOk("request", result));
44
+ } else {
45
+ console.log(JSON.stringify(result, null, 2));
46
+ }
47
+ return 0;
48
+ } catch (err) {
49
+ const message = err instanceof Error ? err.message : String(err);
50
+ if (options.json) {
51
+ printJson(jsonError("request", [message]));
52
+ } else {
53
+ printError(message);
54
+ }
55
+ return 1;
56
+ }
57
+ }
@@ -9,6 +9,7 @@ import type { ReporterName } from "../../core/reporter/types.ts";
9
9
  import type { TestSuite } from "../../core/parser/types.ts";
10
10
  import type { TestRunResult } from "../../core/runner/types.ts";
11
11
  import { printError, printWarning } from "../output.ts";
12
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
12
13
  import { getDb } from "../../db/schema.ts";
13
14
  import { createRun, finalizeRun, saveResults, findCollectionByTestPath } from "../../db/queries.ts";
14
15
 
@@ -25,6 +26,7 @@ export interface RunOptions {
25
26
  tag?: string[];
26
27
  envVars?: string[];
27
28
  dryRun?: boolean;
29
+ json?: boolean;
28
30
  }
29
31
 
30
32
  export async function runCommand(options: RunOptions): Promise<number> {
@@ -51,14 +53,19 @@ export async function runCommand(options: RunOptions): Promise<number> {
51
53
  }
52
54
  }
53
55
 
54
- // 1c. Safe mode: filter to GET-only tests
56
+ // 1c. Safe mode: keep GET, set-only steps, and auth-related requests
55
57
  if (options.safe) {
58
+ const AUTH_PATH_RE = /\/(auth|login|signin|token|oauth)\b/i;
56
59
  for (const suite of suites) {
57
- suite.tests = suite.tests.filter(t => t.method === "GET");
60
+ suite.tests = suite.tests.filter(t => {
61
+ if (t.method === "GET" || !t.method) return true;
62
+ if (AUTH_PATH_RE.test(t.path)) return true;
63
+ return false;
64
+ });
58
65
  }
59
66
  suites = suites.filter(s => s.tests.length > 0);
60
67
  if (suites.length === 0) {
61
- printWarning("No GET tests found. Nothing to run in safe mode.");
68
+ printWarning("No safe tests found. Nothing to run in safe mode.");
62
69
  return 0;
63
70
  }
64
71
  }
@@ -128,29 +135,65 @@ export async function runCommand(options: RunOptions): Promise<number> {
128
135
  results.push(...all);
129
136
  }
130
137
 
131
- // 5. Report
132
- const reporter = getReporter(options.report);
133
- reporter.report(results);
138
+ // 5. Collect warnings
139
+ const warnings: string[] = [];
140
+ const rateLimited = results.flatMap(r => r.steps)
141
+ .filter(s => s.response?.status === 429);
142
+ if (rateLimited.length > 0) {
143
+ warnings.push(`${rateLimited.length} request(s) hit rate limit (429). Consider: consolidating login steps, adding --bail, or using retry_until with delay.`);
144
+ }
145
+
146
+ // 5b. Report
147
+ if (!options.json) {
148
+ const reporter = getReporter(options.report);
149
+ reporter.report(results);
150
+ for (const w of warnings) {
151
+ printWarning(w);
152
+ }
153
+ }
134
154
 
135
155
  // 6. Save to DB
156
+ let savedRunId: number | undefined;
136
157
  if (!options.noDb) {
137
158
  try {
138
159
  getDb(options.dbPath);
139
160
  const collection = findCollectionByTestPath(options.path);
140
- const runId = createRun({
161
+ savedRunId = createRun({
141
162
  started_at: results[0]?.started_at ?? new Date().toISOString(),
142
163
  environment: options.env,
143
164
  collection_id: collection?.id,
144
165
  });
145
- finalizeRun(runId, results);
146
- saveResults(runId, results);
166
+ finalizeRun(savedRunId, results);
167
+ saveResults(savedRunId, results);
147
168
  } catch (err) {
148
169
  printWarning(`Failed to save results to DB: ${(err as Error).message}`);
149
170
  }
150
171
  }
151
172
 
152
173
  // 7. Exit code (always 0 in dry-run mode)
153
- if (dryRun) return 0;
174
+ if (dryRun) {
175
+ if (options.json) {
176
+ printJson(jsonOk("run", { summary: { total: results.length, passed: 0, failed: 0 }, dryRun: true }));
177
+ }
178
+ return 0;
179
+ }
154
180
  const hasFailures = results.some((r) => r.failed > 0 || r.steps.some((s) => s.status === "error"));
181
+
182
+ if (options.json) {
183
+ const total = results.reduce((s, r) => s + r.total, 0);
184
+ const passed = results.reduce((s, r) => s + r.passed, 0);
185
+ const failed = results.reduce((s, r) => s + r.failed, 0);
186
+ const failures = results.flatMap(r =>
187
+ r.steps.filter(s => s.status === "fail" || s.status === "error").map(s => ({
188
+ suite: r.suite_name,
189
+ test: s.name,
190
+ ...(r.suite_file ? { file: r.suite_file } : {}),
191
+ status: s.status,
192
+ error: s.error,
193
+ }))
194
+ );
195
+ printJson(jsonOk("run", { summary: { total, passed, failed }, failures, warnings, runId: savedRunId }));
196
+ }
197
+
155
198
  return hasFailures ? 1 : 0;
156
199
  }
@@ -3,20 +3,79 @@ import { startServer } from "../../web/server.ts";
3
3
  export interface ServeOptions {
4
4
  port?: number;
5
5
  host?: string;
6
- openapiSpec?: string;
7
- testsDir?: string;
8
6
  dbPath?: string;
9
7
  watch?: boolean;
8
+ open?: boolean;
9
+ }
10
+
11
+ /** Kill any existing process listening on the given port (Windows + Unix) */
12
+ async function killPortHolder(port: number): Promise<void> {
13
+ const isWin = process.platform === "win32";
14
+ try {
15
+ if (isWin) {
16
+ // PowerShell: find PID on port, then kill it
17
+ const find = Bun.spawn(["powershell", "-NoProfile", "-Command",
18
+ `(Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue).OwningProcess`], {
19
+ stdout: "pipe", stderr: "ignore",
20
+ });
21
+ const out = await new Response(find.stdout).text();
22
+ const pids = [...new Set(out.trim().split(/\s+/).filter(s => /^\d+$/.test(s) && s !== "0"))];
23
+ for (const pid of pids) {
24
+ Bun.spawn(["powershell", "-NoProfile", "-Command",
25
+ `Stop-Process -Id ${pid} -Force -ErrorAction SilentlyContinue`], {
26
+ stdout: "ignore", stderr: "ignore",
27
+ });
28
+ }
29
+ if (pids.length > 0) {
30
+ // Give OS time to release the port
31
+ await Bun.sleep(500);
32
+ }
33
+ } else {
34
+ // Unix: lsof + kill
35
+ const find = Bun.spawn(["lsof", "-ti", `:${port}`], {
36
+ stdout: "pipe", stderr: "ignore",
37
+ });
38
+ const out = await new Response(find.stdout).text();
39
+ const pids = out.trim().split(/\s+/).filter(s => /^\d+$/.test(s));
40
+ for (const pid of pids) {
41
+ Bun.spawn(["kill", "-9", pid], { stdout: "ignore", stderr: "ignore" });
42
+ }
43
+ if (pids.length > 0) {
44
+ await Bun.sleep(300);
45
+ }
46
+ }
47
+ } catch {
48
+ // Best effort — if we can't kill, startServer will fail with port-in-use
49
+ }
10
50
  }
11
51
 
12
52
  export async function serveCommand(options: ServeOptions): Promise<number> {
53
+ const port = options.port ?? 8080;
54
+
55
+ // Kill previous instance on the same port
56
+ await killPortHolder(port);
57
+
13
58
  await startServer({
14
- port: options.port,
59
+ port,
15
60
  host: options.host,
16
61
  dbPath: options.dbPath,
17
62
  dev: options.watch,
18
63
  });
19
64
 
65
+ // Open browser if requested
66
+ if (options.open) {
67
+ const host = options.host === "0.0.0.0" || !options.host ? "localhost" : options.host;
68
+ const url = `http://${host}:${port}`;
69
+ try {
70
+ const cmd = process.platform === "win32" ? ["cmd", "/c", "start", url]
71
+ : process.platform === "darwin" ? ["open", url]
72
+ : ["xdg-open", url];
73
+ Bun.spawn(cmd, { stdout: "ignore", stderr: "ignore" });
74
+ } catch {
75
+ // Best effort — if browser can't open, server still runs
76
+ }
77
+ }
78
+
20
79
  // Keep running — Bun.serve keeps the process alive
21
80
  return 0;
22
81
  }
@@ -1,18 +1,34 @@
1
1
  import { parse } from "../../core/parser/yaml-parser.ts";
2
2
  import { printError, printSuccess } from "../output.ts";
3
+ import { jsonOk, jsonError, printJson } from "../json-envelope.ts";
3
4
 
4
5
  export interface ValidateOptions {
5
6
  path: string;
7
+ json?: boolean;
6
8
  }
7
9
 
8
10
  export async function validateCommand(options: ValidateOptions): Promise<number> {
9
11
  try {
10
12
  const suites = await parse(options.path);
11
13
  const totalSteps = suites.reduce((sum, s) => sum + s.tests.length, 0);
12
- printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
14
+ if (options.json) {
15
+ printJson(jsonOk("validate", {
16
+ files: suites.length,
17
+ suites: suites.length,
18
+ tests: totalSteps,
19
+ valid: true,
20
+ }));
21
+ } else {
22
+ printSuccess(`OK: ${suites.length} suite(s), ${totalSteps} test(s) validated successfully`);
23
+ }
13
24
  return 0;
14
25
  } catch (err) {
15
- printError(err instanceof Error ? err.message : String(err));
26
+ const message = err instanceof Error ? err.message : String(err);
27
+ if (options.json) {
28
+ printJson(jsonError("validate", [message]));
29
+ } else {
30
+ printError(message);
31
+ }
16
32
  return 2;
17
33
  }
18
34
  }