@kirrosh/zond 0.22.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.
Files changed (256) hide show
  1. package/CHANGELOG.md +648 -0
  2. package/README.md +58 -6
  3. package/package.json +9 -6
  4. package/src/cli/argv.ts +122 -0
  5. package/src/cli/commands/add-api.ts +134 -0
  6. package/src/cli/commands/api/annotate/idempotency.ts +59 -0
  7. package/src/cli/commands/api/annotate/index.ts +525 -0
  8. package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
  9. package/src/cli/commands/api/annotate/overlay.ts +206 -0
  10. package/src/cli/commands/api/annotate/pagination.ts +60 -0
  11. package/src/cli/commands/api/annotate/prompts.ts +183 -0
  12. package/src/cli/commands/api/annotate/readback.ts +58 -0
  13. package/src/cli/commands/api/annotate/resources.ts +91 -0
  14. package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
  15. package/src/cli/commands/audit.ts +480 -0
  16. package/src/cli/commands/bootstrap.ts +710 -0
  17. package/src/cli/commands/catalog.ts +35 -0
  18. package/src/cli/commands/check.ts +348 -0
  19. package/src/cli/commands/checks.ts +756 -0
  20. package/src/cli/commands/ci-init.ts +43 -0
  21. package/src/cli/commands/clean.ts +212 -0
  22. package/src/cli/commands/cleanup.ts +262 -0
  23. package/src/cli/commands/completions.ts +16 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +178 -7
  26. package/src/cli/commands/describe.ts +37 -2
  27. package/src/cli/commands/discover.ts +1236 -0
  28. package/src/cli/commands/doctor.ts +607 -0
  29. package/src/cli/commands/fixtures.ts +402 -0
  30. package/src/cli/commands/generate.ts +420 -46
  31. package/src/cli/commands/init/bootstrap.ts +30 -1
  32. package/src/cli/commands/{init.ts → init/index.ts} +99 -5
  33. package/src/cli/commands/init/skills.ts +56 -3
  34. package/src/cli/commands/init/templates/agents.md +65 -61
  35. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  36. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  37. package/src/cli/commands/init/templates/skills/zond.md +592 -125
  38. package/src/cli/commands/init/templates/zond-config.yml +8 -9
  39. package/src/cli/commands/prepare-fixtures.ts +135 -0
  40. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  41. package/src/cli/commands/probe/security.ts +454 -0
  42. package/src/cli/commands/probe/static.ts +255 -0
  43. package/src/cli/commands/probe/webhooks.ts +161 -0
  44. package/src/cli/commands/probe.ts +459 -0
  45. package/src/cli/commands/reference.ts +87 -0
  46. package/src/cli/commands/refresh-api.ts +169 -0
  47. package/src/cli/commands/remove-api.ts +150 -0
  48. package/src/cli/commands/report-bundle.ts +318 -0
  49. package/src/cli/commands/report.ts +241 -0
  50. package/src/cli/commands/request.ts +379 -4
  51. package/src/cli/commands/run.ts +842 -53
  52. package/src/cli/commands/session.ts +244 -0
  53. package/src/cli/commands/use.ts +18 -1
  54. package/src/cli/index.ts +20 -3
  55. package/src/cli/json-envelope.ts +112 -3
  56. package/src/cli/json-schemas.ts +263 -0
  57. package/src/cli/program.ts +198 -635
  58. package/src/cli/resolve.ts +105 -0
  59. package/src/cli/status-filter.ts +124 -0
  60. package/src/cli/util/api-context.ts +85 -0
  61. package/src/cli/version.ts +5 -0
  62. package/src/core/anti-fp/bootstrap.ts +34 -0
  63. package/src/core/anti-fp/index.ts +33 -0
  64. package/src/core/anti-fp/registry.ts +44 -0
  65. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  66. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  67. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  68. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  69. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  70. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  71. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  72. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  73. package/src/core/anti-fp/types.ts +68 -0
  74. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  75. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  76. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  77. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  78. package/src/core/checks/checks/cross_call_references.ts +134 -0
  79. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  80. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  81. package/src/core/checks/checks/ignored_auth.ts +211 -0
  82. package/src/core/checks/checks/index.ts +65 -0
  83. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  84. package/src/core/checks/checks/missing_required_header.ts +40 -0
  85. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  86. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  87. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  88. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  89. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  90. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  91. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  92. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  93. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  94. package/src/core/checks/checks/unsupported_method.ts +63 -0
  95. package/src/core/checks/checks/use_after_free.ts +78 -0
  96. package/src/core/checks/index.ts +30 -0
  97. package/src/core/checks/mode.ts +79 -0
  98. package/src/core/checks/recommended-action.ts +64 -0
  99. package/src/core/checks/registry.ts +78 -0
  100. package/src/core/checks/runner.ts +874 -0
  101. package/src/core/checks/sarif.ts +230 -0
  102. package/src/core/checks/stateful.ts +121 -0
  103. package/src/core/checks/types.ts +189 -0
  104. package/src/core/classifier/recommended-action.ts +222 -0
  105. package/src/core/context/current.ts +22 -6
  106. package/src/core/context/session.ts +78 -0
  107. package/src/core/coverage/loader.ts +185 -0
  108. package/src/core/coverage/reasons.ts +300 -0
  109. package/src/core/diagnostics/db-analysis.ts +151 -11
  110. package/src/core/diagnostics/failure-class.ts +120 -0
  111. package/src/core/diagnostics/failure-hints.ts +212 -9
  112. package/src/core/diagnostics/spec-pointer.ts +99 -0
  113. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  114. package/src/core/exporter/case-study/index.ts +270 -0
  115. package/src/core/exporter/curl.ts +40 -0
  116. package/src/core/exporter/exporter.ts +48 -0
  117. package/src/core/exporter/html-report/escape.ts +24 -0
  118. package/src/core/exporter/html-report/index.ts +479 -0
  119. package/src/core/exporter/html-report/script.ts +100 -0
  120. package/src/core/exporter/html-report/styles.ts +408 -0
  121. package/src/core/generator/chunker.ts +42 -16
  122. package/src/core/generator/coverage-phase.ts +0 -0
  123. package/src/core/generator/create-body.ts +89 -0
  124. package/src/core/generator/data-factory.ts +445 -19
  125. package/src/core/generator/describe.ts +1 -1
  126. package/src/core/generator/fixtures-builder.ts +325 -0
  127. package/src/core/generator/index.ts +7 -5
  128. package/src/core/generator/openapi-reader.ts +37 -3
  129. package/src/core/generator/path-param-disambig.ts +114 -0
  130. package/src/core/generator/resources-builder.ts +648 -0
  131. package/src/core/generator/schema-utils.ts +11 -3
  132. package/src/core/generator/serializer.ts +103 -13
  133. package/src/core/generator/suite-generator.ts +419 -111
  134. package/src/core/generator/types.ts +8 -0
  135. package/src/core/identity/identity-file.ts +129 -0
  136. package/src/core/lint/affects.ts +28 -0
  137. package/src/core/lint/config.ts +96 -0
  138. package/src/core/lint/format.ts +42 -0
  139. package/src/core/lint/index.ts +94 -0
  140. package/src/core/lint/reporter.ts +128 -0
  141. package/src/core/lint/rules/consistency.ts +158 -0
  142. package/src/core/lint/rules/heuristics.ts +97 -0
  143. package/src/core/lint/rules/strictness.ts +109 -0
  144. package/src/core/lint/types.ts +96 -0
  145. package/src/core/lint/walker.ts +248 -0
  146. package/src/core/meta/meta-store.ts +6 -73
  147. package/src/core/output/README.md +91 -0
  148. package/src/core/output/index.ts +13 -0
  149. package/src/core/output/run.ts +126 -0
  150. package/src/core/output/types.ts +129 -0
  151. package/src/core/parser/env-interpolation.ts +104 -0
  152. package/src/core/parser/filter.ts +57 -0
  153. package/src/core/parser/schema.ts +129 -4
  154. package/src/core/parser/types.ts +19 -1
  155. package/src/core/parser/variables.ts +0 -0
  156. package/src/core/parser/yaml-parser.ts +58 -12
  157. package/src/core/probe/bootstrap.ts +34 -0
  158. package/src/core/probe/dry-run-envelope.ts +57 -0
  159. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  160. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  161. package/src/core/probe/mass-assignment-template.ts +212 -0
  162. package/src/core/probe/method-probe.ts +43 -76
  163. package/src/core/probe/method-shared.ts +69 -0
  164. package/src/core/probe/negative-probe.ts +183 -149
  165. package/src/core/probe/orphan-tracker.ts +188 -0
  166. package/src/core/probe/path-discovery.ts +440 -0
  167. package/src/core/probe/probe-harness.ts +120 -0
  168. package/src/core/probe/registry.ts +89 -0
  169. package/src/core/probe/runner.ts +136 -0
  170. package/src/core/probe/security-probe-class.ts +201 -0
  171. package/src/core/probe/security-probe.ts +1453 -0
  172. package/src/core/probe/shared.ts +505 -0
  173. package/src/core/probe/static-probe-class.ts +125 -0
  174. package/src/core/probe/types.ts +165 -0
  175. package/src/core/probe/verdict-aggregator.ts +33 -0
  176. package/src/core/probe/webhooks-probe.ts +284 -0
  177. package/src/core/reporter/console.ts +41 -2
  178. package/src/core/reporter/index.ts +2 -3
  179. package/src/core/reporter/json.ts +11 -1
  180. package/src/core/reporter/junit.ts +27 -12
  181. package/src/core/reporter/ndjson.ts +37 -0
  182. package/src/core/reporter/types.ts +3 -0
  183. package/src/core/runner/assertions.ts +58 -1
  184. package/src/core/runner/async-pool.ts +108 -0
  185. package/src/core/runner/auth-path.ts +8 -0
  186. package/src/core/runner/ci-context.ts +72 -0
  187. package/src/core/runner/executor.ts +264 -20
  188. package/src/core/runner/form-encode.ts +51 -0
  189. package/src/core/runner/http-client.ts +75 -2
  190. package/src/core/runner/learn-drift.ts +293 -0
  191. package/src/core/runner/preflight-vars.ts +149 -0
  192. package/src/core/runner/progress-tracker.ts +73 -0
  193. package/src/core/runner/rate-limiter.ts +89 -17
  194. package/src/core/runner/run-kind.ts +39 -0
  195. package/src/core/runner/schema-validator.ts +312 -0
  196. package/src/core/runner/send-request.ts +153 -20
  197. package/src/core/runner/types.ts +38 -0
  198. package/src/core/secrets/registry.ts +164 -0
  199. package/src/core/secrets/secrets-file.ts +115 -0
  200. package/src/core/selectors/operation-filter.ts +144 -0
  201. package/src/core/setup-api.ts +415 -16
  202. package/src/core/severity/category.ts +94 -0
  203. package/src/core/severity/index.ts +121 -0
  204. package/src/core/spec/layers.ts +154 -0
  205. package/src/core/util/format-eta.ts +21 -0
  206. package/src/core/utils.ts +5 -1
  207. package/src/core/workspace/config.ts +129 -0
  208. package/src/core/workspace/manifest.ts +283 -0
  209. package/src/core/workspace/output-rotation.ts +62 -0
  210. package/src/core/workspace/triage-path.ts +87 -0
  211. package/src/db/lint-runs.ts +47 -0
  212. package/src/db/migrate.ts +126 -0
  213. package/src/db/migrations/0001_run_kind.sql +25 -0
  214. package/src/db/migrations/sql.d.ts +4 -0
  215. package/src/db/queries/collections.ts +133 -0
  216. package/src/db/queries/coverage.ts +9 -0
  217. package/src/db/queries/dashboard.ts +59 -0
  218. package/src/db/queries/results.ts +128 -0
  219. package/src/db/queries/runs.ts +235 -0
  220. package/src/db/queries/sessions.ts +42 -0
  221. package/src/db/queries/settings.ts +28 -0
  222. package/src/db/queries/types.ts +172 -0
  223. package/src/db/queries.ts +72 -802
  224. package/src/db/schema.ts +178 -50
  225. package/src/cli/commands/export.ts +0 -144
  226. package/src/cli/commands/guide.ts +0 -127
  227. package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
  228. package/src/cli/commands/probe-methods.ts +0 -108
  229. package/src/cli/commands/probe-validation.ts +0 -124
  230. package/src/cli/commands/serve.ts +0 -114
  231. package/src/cli/commands/sync.ts +0 -268
  232. package/src/cli/commands/update.ts +0 -189
  233. package/src/cli/commands/validate.ts +0 -34
  234. package/src/core/diagnostics/render-md.ts +0 -112
  235. package/src/core/exporter/postman.ts +0 -963
  236. package/src/core/generator/guide-builder.ts +0 -253
  237. package/src/core/meta/types.ts +0 -19
  238. package/src/core/parser/index.ts +0 -21
  239. package/src/core/runner/execute-run.ts +0 -132
  240. package/src/core/runner/index.ts +0 -12
  241. package/src/core/sync/spec-differ.ts +0 -38
  242. package/src/web/data/collection-state.ts +0 -362
  243. package/src/web/routes/api.ts +0 -314
  244. package/src/web/routes/dashboard.ts +0 -350
  245. package/src/web/routes/runs.ts +0 -64
  246. package/src/web/schemas.ts +0 -121
  247. package/src/web/server.ts +0 -134
  248. package/src/web/static/htmx.min.cjs +0 -1
  249. package/src/web/static/style.css +0 -1148
  250. package/src/web/views/endpoints-tab.ts +0 -174
  251. package/src/web/views/explorer-tab.ts +0 -402
  252. package/src/web/views/health-strip.ts +0 -92
  253. package/src/web/views/layout.ts +0 -48
  254. package/src/web/views/results.ts +0 -210
  255. package/src/web/views/runs-tab.ts +0 -126
  256. package/src/web/views/suites-tab.ts +0 -181
@@ -1,48 +0,0 @@
1
- let _devMode = false;
2
-
3
- export function setDevMode(enabled: boolean): void {
4
- _devMode = enabled;
5
- }
6
-
7
- export function layout(title: string, content: string, navExtra = ""): string {
8
- const devScript = _devMode
9
- ? `<script>new EventSource('/dev/reload').onmessage = (e) => { if (e.data === 'reload') location.reload() }</script>`
10
- : "";
11
- return `<!DOCTYPE html>
12
- <html lang="en">
13
- <head>
14
- <meta charset="UTF-8">
15
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
16
- <title>${escapeHtml(title)} — zond</title>
17
- <link rel="preconnect" href="https://fonts.googleapis.com">
18
- <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19
- <link href="https://fonts.googleapis.com/css2?family=DM+Sans:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
20
- <link rel="stylesheet" href="/static/style.css?v=${Date.now()}">
21
- <script src="/static/htmx.min.js"></script>
22
- <script>htmx.config.refreshOnHistoryMiss = true;</script>
23
- </head>
24
- <body>
25
- <nav class="navbar">
26
- <a href="/" class="nav-brand" style="text-decoration:none;color:inherit;"><span class="logo-dot"></span>zond</a>
27
- ${navExtra}
28
- </nav>
29
- <main class="main-container">
30
- ${content}
31
- </main>
32
- <footer class="footer"><div class="main-container">zond</div></footer>
33
- ${devScript}
34
- </body>
35
- </html>`;
36
- }
37
-
38
- export function fragment(content: string): string {
39
- return content;
40
- }
41
-
42
- export function escapeHtml(str: string): string {
43
- return str
44
- .replace(/&/g, "&amp;")
45
- .replace(/</g, "&lt;")
46
- .replace(/>/g, "&gt;")
47
- .replace(/"/g, "&quot;");
48
- }
@@ -1,210 +0,0 @@
1
- import { escapeHtml } from "./layout.ts";
2
- import { formatDuration } from "../../core/reporter/console.ts";
3
- import type { StoredStepResult } from "../../db/queries.ts";
4
-
5
- export function statusBadge(total: number, passed: number, failed: number): string {
6
- if (total === 0) return `<span class="badge badge-skip">empty</span>`;
7
- if (failed > 0) return `<span class="badge badge-fail">fail</span>`;
8
- return `<span class="badge badge-pass">pass</span>`;
9
- }
10
-
11
- export function stepStatusBadge(status: string): string {
12
- switch (status) {
13
- case "pass":
14
- return `<span class="badge badge-pass">&#10003;</span>`;
15
- case "fail":
16
- return `<span class="badge badge-fail">&#10007;</span>`;
17
- case "skip":
18
- return `<span class="badge badge-skip">&#9675;</span>`;
19
- case "error":
20
- return `<span class="badge badge-error">&#10007;</span>`;
21
- default:
22
- return `<span class="badge">${escapeHtml(status)}</span>`;
23
- }
24
- }
25
-
26
- export function methodBadge(method: string): string {
27
- const m = method.toLowerCase();
28
- return `<span class="badge-method method-${m}">${method}</span>`;
29
- }
30
-
31
- /**
32
- * Render grouped suite results with step details, captures, and chain visualization.
33
- * Used by both the dashboard panels and the /runs/:id detail page.
34
- */
35
- export function renderSuiteResults(
36
- results: StoredStepResult[],
37
- runId: number,
38
- options?: { idPrefix?: string; suiteMetadata?: Map<string, { description?: string; tags?: string[] }> },
39
- ): string {
40
- const prefix = options?.idPrefix ?? `r${runId}`;
41
-
42
- // Group by suite
43
- const suites = new Map<string, StoredStepResult[]>();
44
- for (const r of results) {
45
- const list = suites.get(r.suite_name) ?? [];
46
- list.push(r);
47
- suites.set(r.suite_name, list);
48
- }
49
-
50
- // Build capture source map
51
- const captureSourceMap = new Map<string, string>();
52
- for (const [, steps] of suites) {
53
- for (const step of steps) {
54
- if (step.captures && typeof step.captures === "object") {
55
- for (const varName of Object.keys(step.captures)) {
56
- captureSourceMap.set(varName, step.test_name);
57
- }
58
- }
59
- }
60
- }
61
-
62
- let suitesHtml = "";
63
- for (const [suiteName, steps] of suites) {
64
- const suiteHasCaptures = steps.some(s =>
65
- s.captures && typeof s.captures === "object" && Object.keys(s.captures).length > 0,
66
- );
67
- const isChainSuite = suiteHasCaptures || suiteName.endsWith("CRUD");
68
-
69
- const stepsHtml = steps
70
- .map((step, i) => {
71
- const detailId = `detail-${prefix}-${i}`;
72
- const hasFailed = step.status === "fail" || step.status === "error";
73
-
74
- let capturesHtml = "";
75
- if (step.captures && typeof step.captures === "object") {
76
- const captureEntries = Object.entries(step.captures);
77
- if (captureEntries.length > 0) {
78
- capturesHtml = captureEntries.map(([k, v]) =>
79
- `<span class="capture-badge">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`,
80
- ).join(" ");
81
- }
82
- }
83
-
84
- let assertionsHtml = "";
85
- if (step.assertions.length > 0) {
86
- const items = step.assertions
87
- .map(
88
- (a) =>
89
- `<li class="${a.passed ? "assertion-pass" : "assertion-fail"}">${escapeHtml(a.field)}: ${escapeHtml(a.rule)} (got ${escapeHtml(String(a.actual))})</li>`,
90
- )
91
- .join("");
92
- assertionsHtml = `<ul class="assertion-list">${items}</ul>`;
93
- }
94
-
95
- let requestHtml = "";
96
- if (step.request_method) {
97
- requestHtml = `<div><strong>Request:</strong> ${escapeHtml(step.request_method)} ${escapeHtml(step.request_url ?? "")}</div>`;
98
- }
99
-
100
- let reqBodyHtml = "";
101
- if (step.request_body) {
102
- reqBodyHtml = `<details class="body-details"><summary>Request Body</summary><pre>${escapeHtml(step.request_body)}</pre></details>`;
103
- }
104
- let resBodyHtml = "";
105
- if (step.response_body) {
106
- resBodyHtml = `<details class="body-details"><summary>Response Body</summary><pre>${escapeHtml(step.response_body)}</pre></details>`;
107
- }
108
-
109
- let errorHtml = "";
110
- if (step.error_message) {
111
- errorHtml = `<div><strong>Error:</strong> ${escapeHtml(step.error_message)}</div>`;
112
- }
113
-
114
- let skipReasonHtml = "";
115
- if (step.status === "skip" && step.error_message) {
116
- const match = step.error_message.match(/Depends on missing capture: (\w+)/);
117
- if (match) {
118
- const depVar = match[1]!;
119
- const sourceStep = captureSourceMap.get(depVar);
120
- skipReasonHtml = sourceStep
121
- ? `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code> (from step "${escapeHtml(sourceStep)}")</div>`
122
- : `<div class="skip-reason">Skipped: depends on <code>${escapeHtml(depVar)}</code></div>`;
123
- }
124
- }
125
-
126
- const hasContent = requestHtml || errorHtml || skipReasonHtml || assertionsHtml || reqBodyHtml || resBodyHtml;
127
- const detailPanel = hasContent
128
- ? `<div class="detail-panel" id="${detailId}" style="display:none">
129
- ${requestHtml}
130
- ${errorHtml}
131
- ${skipReasonHtml}
132
- ${assertionsHtml}
133
- ${reqBodyHtml}
134
- ${resBodyHtml}
135
- </div>`
136
- : "";
137
-
138
- const toggle = hasContent
139
- ? `onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
140
- : "";
141
-
142
- const chainedClass = isChainSuite ? " chained" : "";
143
- const statusClass = (step.status === "fail" || step.status === "error") ? ` step-${step.status}` : "";
144
-
145
- return `
146
- <div class="step-row${chainedClass}${statusClass}" ${toggle}>
147
- <div>${stepStatusBadge(step.status)}</div>
148
- <div class="step-name">${step.request_method && step.request_url ? (() => { let p: string; try { p = new URL(step.request_url).pathname; } catch { p = step.request_url; } const sc = step.response_status ? ` <span class="step-status-code ${step.response_status >= 400 ? "status-error" : "status-ok"}">${step.response_status}</span>` : ""; return `${methodBadge(step.request_method)} <span class="step-path">${escapeHtml(p)}</span>${sc} <span class="step-name-dim">${escapeHtml(step.test_name)}</span>`; })() : escapeHtml(step.test_name)}${capturesHtml ? ` ${capturesHtml}` : ""}</div>
149
- <div class="step-duration">${formatDuration(step.duration_ms)}</div>
150
- </div>
151
- ${detailPanel}`;
152
- })
153
- .join("");
154
-
155
- const chainClass = isChainSuite ? " chain-suite" : "";
156
-
157
- const meta = options?.suiteMetadata?.get(suiteName);
158
- const descriptionHtml = meta?.description
159
- ? `<p class="suite-description">${escapeHtml(meta.description)}</p>`
160
- : "";
161
- const tagsHtml = meta?.tags?.length
162
- ? `<div class="suite-tags">${meta.tags.map(t => `<span class="tag-badge">${escapeHtml(t)}</span>`).join(" ")}</div>`
163
- : "";
164
-
165
- suitesHtml += `
166
- <div class="suite-section${chainClass}">
167
- <h3>${escapeHtml(suiteName)}</h3>
168
- ${descriptionHtml}
169
- ${tagsHtml}
170
- ${isChainSuite ? '<div class="chain-connector">' : ""}
171
- ${stepsHtml}
172
- ${isChainSuite ? "</div>" : ""}
173
- </div>`;
174
- }
175
-
176
- return suitesHtml;
177
- }
178
-
179
- /**
180
- * Render the "show only failed" toggle + auto-expand failed steps script.
181
- */
182
- export function failedFilterToggle(): string {
183
- return `
184
- <label class="failed-filter-toggle" style="display:inline-flex;align-items:center;gap:0.5rem;font-size:0.85rem;cursor:pointer;">
185
- <input type="checkbox" id="failed-only-toggle" onchange="
186
- var on = this.checked;
187
- document.querySelectorAll('.step-row').forEach(function(el) {
188
- if (on && !el.classList.contains('step-fail') && !el.classList.contains('step-error')) {
189
- el.style.display = 'none';
190
- var next = el.nextElementSibling;
191
- if (next && next.classList.contains('detail-panel')) next.style.display = 'none';
192
- } else {
193
- el.style.display = '';
194
- }
195
- });
196
- "> Show only failed
197
- </label>`;
198
- }
199
-
200
- /**
201
- * Script to auto-expand failed step detail panels on page load.
202
- */
203
- export function autoExpandFailedScript(): string {
204
- return `<script>
205
- document.querySelectorAll('.step-row.step-fail, .step-row.step-error').forEach(function(el) {
206
- var next = el.nextElementSibling;
207
- if (next && next.classList.contains('detail-panel')) next.style.display = 'block';
208
- });
209
- </script>`;
210
- }
@@ -1,126 +0,0 @@
1
- /**
2
- * Runs tab: history of test runs with comparison.
3
- */
4
-
5
- import { escapeHtml } from "./layout.ts";
6
- import { statusBadge } from "./results.ts";
7
- import { formatDuration } from "../../core/reporter/console.ts";
8
- import {
9
- listRunsByCollection,
10
- countRunsByCollection,
11
- getResultsByRunId,
12
- getRunById,
13
- } from "../../db/queries.ts";
14
- import type { RunSummary } from "../../db/queries.ts";
15
- import { renderSuiteResults, failedFilterToggle, autoExpandFailedScript } from "./results.ts";
16
-
17
- const PAGE_SIZE = 15;
18
-
19
- export function renderRunsTab(collectionId: number, page = 1): string {
20
- const offset = (page - 1) * PAGE_SIZE;
21
- const runs = listRunsByCollection(collectionId, PAGE_SIZE, offset);
22
- const total = countRunsByCollection(collectionId);
23
- const hasMore = offset + runs.length < total;
24
-
25
- if (runs.length === 0 && page === 1) {
26
- return `<div class="tab-empty">No test runs yet. Click <strong>Run Tests</strong> to get started.</div>`;
27
- }
28
-
29
- const rows = runs.map(r => renderRunRow(r, collectionId)).join("");
30
-
31
- const loadMore = hasMore
32
- ? `<div style="text-align:center;padding:0.75rem;">
33
- <button class="btn btn-sm btn-outline"
34
- hx-get="/panels/runs-tab?collection_id=${collectionId}&page=${page + 1}"
35
- hx-target="#tab-content" hx-swap="innerHTML">Load more...</button>
36
- </div>`
37
- : "";
38
-
39
- return `
40
- <div class="runs-list">
41
- <div class="runs-header">
42
- <span>Run</span><span>Time</span><span>Results</span><span>Duration</span><span>Status</span>
43
- </div>
44
- ${rows}
45
- ${loadMore}
46
- </div>`;
47
- }
48
-
49
- function renderRunRow(run: RunSummary, collectionId: number): string {
50
- const timeAgo = formatTimeAgo(run.started_at);
51
- const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : "-";
52
- const total = run.total || 1;
53
-
54
- // Mini progress bar
55
- const progressBar = run.total > 0
56
- ? `<div class="progress-bar run-progress">
57
- <div class="progress-pass" style="width:${(run.passed / total * 100).toFixed(1)}%"></div>
58
- <div class="progress-fail" style="width:${(run.failed / total * 100).toFixed(1)}%"></div>
59
- </div>`
60
- : "";
61
-
62
- return `
63
- <div class="run-row"
64
- hx-get="/panels/run-detail?run_id=${run.id}&collection_id=${collectionId}"
65
- hx-target="#tab-content" hx-swap="innerHTML">
66
- <span class="run-id">#${run.id}</span>
67
- <span class="run-time">${escapeHtml(timeAgo)}</span>
68
- <span class="run-results">
69
- ${progressBar}
70
- <span class="run-counts">${run.passed}&#10003; ${run.failed}&#10007; ${run.skipped}&#9675;</span>
71
- </span>
72
- <span class="run-duration">${duration}</span>
73
- <span>${statusBadge(run.total, run.passed, run.failed)}</span>
74
- </div>`;
75
- }
76
-
77
- export function renderRunDetail(runId: number, collectionId: number): string {
78
- const run = getRunById(runId);
79
- if (!run) return `<p>Run not found</p>`;
80
-
81
- const results = getResultsByRunId(runId);
82
- if (results.length === 0) return `<p class="tab-empty">No results for run #${runId}.</p>`;
83
-
84
- const timeAgo = formatTimeAgo(run.started_at);
85
- const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : "-";
86
-
87
- const backButton = `<button class="btn btn-sm btn-outline" style="margin-bottom:0.75rem;"
88
- hx-get="/panels/runs-tab?collection_id=${collectionId}"
89
- hx-target="#tab-content" hx-swap="innerHTML">&larr; Back to runs</button>`;
90
-
91
- const header = `
92
- <div class="run-detail-header">
93
- <strong>Run #${run.id}</strong>
94
- <span class="text-dim">${escapeHtml(timeAgo)}</span>
95
- <span>${run.passed}&#10003; ${run.failed}&#10007; ${run.skipped}&#9675;</span>
96
- <span class="text-dim">${duration}</span>
97
- ${statusBadge(run.total, run.passed, run.failed)}
98
- <span style="flex:1;"></span>
99
- <a href="/api/export/${run.id}/junit" download class="btn btn-sm btn-outline">JUnit</a>
100
- <a href="/api/export/${run.id}/json" download class="btn btn-sm btn-outline">JSON</a>
101
- ${failedFilterToggle()}
102
- </div>`;
103
-
104
- const suitesHtml = renderSuiteResults(results, runId);
105
-
106
- return backButton + header + suitesHtml + autoExpandFailedScript();
107
- }
108
-
109
- function formatTimeAgo(isoDate: string): string {
110
- try {
111
- const date = new Date(isoDate);
112
- const now = new Date();
113
- const diffMs = now.getTime() - date.getTime();
114
- const diffSec = Math.floor(diffMs / 1000);
115
- if (diffSec < 60) return "just now";
116
- const diffMin = Math.floor(diffSec / 60);
117
- if (diffMin < 60) return `${diffMin}m ago`;
118
- const diffHr = Math.floor(diffMin / 60);
119
- if (diffHr < 24) return `${diffHr}h ago`;
120
- const diffDay = Math.floor(diffHr / 24);
121
- if (diffDay < 7) return `${diffDay}d ago`;
122
- return date.toLocaleDateString();
123
- } catch {
124
- return isoDate;
125
- }
126
- }
@@ -1,181 +0,0 @@
1
- /**
2
- * Suites tab: all YAML files on disk with run status and step details.
3
- */
4
-
5
- import type { CollectionState, SuiteViewState, StepViewState } from "../data/collection-state.ts";
6
- import { escapeHtml } from "./layout.ts";
7
- import { methodBadge } from "./results.ts";
8
- import { basename } from "node:path";
9
-
10
- export function renderSuitesTab(state: CollectionState): string {
11
- if (state.suites.length === 0) {
12
- return `<div class="tab-empty">No test suites found on disk. Generate tests with <code>zond guide</code> or use the test-generation skill.</div>`;
13
- }
14
-
15
- const rows = state.suites.map((s, i) => renderSuiteRow(s, i)).join("");
16
- return `<div class="suite-list">${rows}</div>`;
17
- }
18
-
19
- function renderSuiteRow(suite: SuiteViewState, index: number): string {
20
- const detailId = `suite-detail-${index}`;
21
-
22
- if (suite.status === "parse_error") {
23
- return `
24
- <div class="suite-row suite-error-row">
25
- <div class="suite-info">
26
- <div class="suite-name">${escapeHtml(basename(suite.filePath || suite.name))}</div>
27
- <div class="suite-desc" style="color:var(--fail);">${escapeHtml(suite.parseError ?? "Parse error")}</div>
28
- </div>
29
- <div class="suite-tags"></div>
30
- <div class="suite-steps-count">-</div>
31
- <div class="suite-result fail">error</div>
32
- </div>`;
33
- }
34
-
35
- const tags = suite.tags.map(t => {
36
- const tagClass = t === "smoke" ? "smoke" : t === "crud" ? "crud" : t === "auth" ? "auth" : t === "destructive" ? "destructive" : "";
37
- return `<span class="tag-pill ${tagClass}">${escapeHtml(t)}</span>`;
38
- }).join("");
39
-
40
- const total = suite.runResult
41
- ? suite.runResult.passed + suite.runResult.failed + suite.runResult.skipped
42
- : 0;
43
-
44
- let resultHtml: string;
45
- if (suite.status === "passed") {
46
- resultHtml = `<div class="suite-result pass">${suite.runResult!.passed}/${total} &#10003;</div>`;
47
- } else if (suite.status === "failed") {
48
- resultHtml = `<div class="suite-result fail">${suite.runResult!.passed}/${total} &#10007;</div>`;
49
- } else {
50
- resultHtml = `<div class="suite-result not-run">not run</div>`;
51
- }
52
-
53
- // Step detail rows
54
- const stepsHtml = suite.steps.length > 0
55
- ? suite.steps.map((step, si) => renderStepRow(step, index, si)).join("")
56
- : `<div style="font-size:0.75rem;color:var(--text-dim);padding:0.5rem;">No run results yet</div>`;
57
-
58
- return `
59
- <div class="suite-row" data-suite-name="${escapeHtml(suite.name)}"
60
- onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'">
61
- <div class="suite-info">
62
- <div class="suite-name">${escapeHtml(suite.name)}</div>
63
- ${suite.description ? `<div class="suite-desc">${escapeHtml(suite.description)}</div>` : ""}
64
- </div>
65
- <div class="suite-tags">${tags}</div>
66
- <div class="suite-steps-count">${suite.stepCount} steps</div>
67
- ${resultHtml}
68
- </div>
69
- <div class="suite-detail" id="${detailId}" style="display:none">
70
- ${stepsHtml}
71
- </div>`;
72
- }
73
-
74
- function renderStepRow(step: StepViewState, suiteIdx: number, stepIdx: number): string {
75
- const icon = step.status === "pass"
76
- ? '<span class="step-icon pass">&#10003;</span>'
77
- : step.status === "fail" || step.status === "error"
78
- ? '<span class="step-icon fail">&#10007;</span>'
79
- : '<span class="step-icon skip">&#9675;</span>';
80
-
81
- const labelStyle = step.status === "fail" || step.status === "error"
82
- ? ' style="color:var(--fail);"'
83
- : step.status === "skip"
84
- ? ' style="color:var(--skip);"'
85
- : "";
86
-
87
- const duration = step.durationMs != null
88
- ? `<span class="step-duration">${step.durationMs}ms</span>`
89
- : `<span class="step-duration">-</span>`;
90
-
91
- // Primary label: prefer METHOD /path [status] over step name
92
- let primaryLabel: string;
93
- let nameLabel = "";
94
- if (step.requestMethod && step.requestUrl) {
95
- let urlPath: string;
96
- try { urlPath = new URL(step.requestUrl).pathname; } catch { urlPath = step.requestUrl; }
97
- const statusTag = step.responseStatus
98
- ? ` <span class="step-status-code ${step.responseStatus >= 400 ? "status-error" : "status-ok"}">${step.responseStatus}</span>`
99
- : "";
100
- primaryLabel = `${methodBadge(step.requestMethod)} <span class="step-path">${escapeHtml(urlPath)}</span>${statusTag}`;
101
- nameLabel = ` <span class="step-name-dim">${escapeHtml(step.name)}</span>`;
102
- } else {
103
- primaryLabel = escapeHtml(step.name);
104
- }
105
-
106
- // Captures
107
- const captureHtml = step.captures && Object.keys(step.captures).length > 0
108
- ? `<span class="step-captures">${Object.entries(step.captures).map(([k, v]) =>
109
- `<span class="capture-pill">${escapeHtml(k)} = ${escapeHtml(String(v))}</span>`
110
- ).join("")}</span>`
111
- : `<span class="step-captures"></span>`;
112
-
113
- const detailId = `s-${suiteIdx}-step-${stepIdx}`;
114
- const hasDetail =
115
- (step.assertions && step.assertions.length > 0) ||
116
- step.hint ||
117
- step.responseBody ||
118
- step.requestBody ||
119
- step.errorMessage ||
120
- (step.requestMethod && step.requestUrl);
121
-
122
- const clickHandler = hasDetail
123
- ? ` onclick="event.stopPropagation();var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'block':'none'"`
124
- : "";
125
-
126
- let detailPanel = "";
127
- if (hasDetail) {
128
- let detailContent = "";
129
-
130
- // Request info
131
- if (step.requestMethod && step.requestUrl) {
132
- detailContent += `<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--text-dim);margin-bottom:0.4rem;">
133
- ${escapeHtml(step.requestMethod)} ${escapeHtml(step.requestUrl)}</div>`;
134
- }
135
-
136
- // Assertions
137
- if (step.assertions && step.assertions.length > 0) {
138
- detailContent += step.assertions.map(a => {
139
- const aIcon = a.passed
140
- ? '<span class="assertion-icon pass">&#10003;</span>'
141
- : '<span class="assertion-icon fail">&#10007;</span>';
142
- const actual = !a.passed && a.actual !== undefined
143
- ? ` <span class="assertion-actual">(got ${escapeHtml(JSON.stringify(a.actual))})</span>` : "";
144
- return `<div class="assertion-row">${aIcon} <span class="assertion-field">${escapeHtml(a.field)}:</span> <span class="assertion-rule">${escapeHtml(a.rule)}</span>${actual}</div>`;
145
- }).join("");
146
- }
147
-
148
- // Error message
149
- if (step.errorMessage) {
150
- detailContent += `<div style="font-family:var(--font-mono);font-size:0.75rem;color:var(--fail);margin-top:0.25rem;">${escapeHtml(step.errorMessage)}</div>`;
151
- }
152
-
153
- // Failure hint
154
- if (step.hint) {
155
- detailContent += `<div class="failure-hint"><span>&#9888;</span> ${escapeHtml(step.hint)}</div>`;
156
- }
157
-
158
- // Request body toggle
159
- if (step.requestBody) {
160
- const truncatedReq = step.requestBody.length > 2000 ? step.requestBody.slice(0, 2000) + "..." : step.requestBody;
161
- detailContent += `<div class="req-res-toggle" onclick="event.stopPropagation();var b=this.nextElementSibling;b.style.display=b.style.display==='none'?'block':'none'">&#9660; Request Body</div>
162
- <div class="req-res-body" style="display:none;"><pre style="font-size:0.7rem;margin:0.25rem 0;">${escapeHtml(truncatedReq)}</pre></div>`;
163
- }
164
-
165
- // Response body toggle
166
- if (step.responseBody) {
167
- const truncated = step.responseBody.length > 2000 ? step.responseBody.slice(0, 2000) + "..." : step.responseBody;
168
- detailContent += `<div class="req-res-toggle" onclick="event.stopPropagation();var b=this.nextElementSibling;b.style.display=b.style.display==='none'?'block':'none'">&#9660; Response Body</div>
169
- <div class="req-res-body" style="display:none;"><pre style="font-size:0.7rem;margin:0.25rem 0;">${escapeHtml(truncated)}</pre></div>`;
170
- }
171
-
172
- detailPanel = `<div class="step-detail-panel" id="${detailId}" style="display:none">${detailContent}</div>`;
173
- }
174
-
175
- return `<div class="step-row"${clickHandler}>
176
- ${icon}
177
- <span class="step-label"${labelStyle}>${primaryLabel}${nameLabel}</span>
178
- ${captureHtml}
179
- ${duration}
180
- </div>${detailPanel}`;
181
- }