@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.
Files changed (256) hide show
  1. package/CHANGELOG.md +758 -3
  2. package/README.md +78 -15
  3. package/package.json +17 -10
  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 +55 -6
  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 +192 -0
  24. package/src/cli/commands/coverage.ts +605 -132
  25. package/src/cli/commands/db.ts +180 -8
  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 -47
  31. package/src/cli/commands/init/agents-md.ts +61 -0
  32. package/src/cli/commands/init/bootstrap.ts +108 -0
  33. package/src/cli/commands/init/index.ts +244 -0
  34. package/src/cli/commands/init/skills.ts +98 -0
  35. package/src/cli/commands/init/templates/agents.md +77 -0
  36. package/src/cli/commands/init/templates/markdown.d.ts +4 -0
  37. package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
  38. package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
  39. package/src/cli/commands/init/templates/skills/zond.md +651 -0
  40. package/src/cli/commands/init/templates/zond-config.yml +14 -0
  41. package/src/cli/commands/prepare-fixtures.ts +135 -0
  42. package/src/cli/commands/probe/mass-assignment.ts +503 -0
  43. package/src/cli/commands/probe/security.ts +454 -0
  44. package/src/cli/commands/probe/static.ts +255 -0
  45. package/src/cli/commands/probe/webhooks.ts +161 -0
  46. package/src/cli/commands/probe.ts +459 -0
  47. package/src/cli/commands/reference.ts +87 -0
  48. package/src/cli/commands/refresh-api.ts +169 -0
  49. package/src/cli/commands/remove-api.ts +150 -0
  50. package/src/cli/commands/report-bundle.ts +318 -0
  51. package/src/cli/commands/report.ts +241 -0
  52. package/src/cli/commands/request.ts +379 -4
  53. package/src/cli/commands/run.ts +911 -33
  54. package/src/cli/commands/session.ts +244 -0
  55. package/src/cli/commands/use.ts +74 -0
  56. package/src/cli/index.ts +36 -607
  57. package/src/cli/json-envelope.ts +112 -3
  58. package/src/cli/json-schemas.ts +263 -0
  59. package/src/cli/program.ts +218 -0
  60. package/src/cli/resolve.ts +105 -0
  61. package/src/cli/status-filter.ts +124 -0
  62. package/src/cli/util/api-context.ts +85 -0
  63. package/src/cli/version.ts +8 -0
  64. package/src/core/anti-fp/bootstrap.ts +34 -0
  65. package/src/core/anti-fp/index.ts +33 -0
  66. package/src/core/anti-fp/registry.ts +44 -0
  67. package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
  68. package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
  69. package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
  70. package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
  71. package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
  72. package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
  73. package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
  74. package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
  75. package/src/core/anti-fp/types.ts +68 -0
  76. package/src/core/checks/checks/_crud-helpers.ts +133 -0
  77. package/src/core/checks/checks/_negative_mutator.ts +133 -0
  78. package/src/core/checks/checks/_readback-helpers.ts +133 -0
  79. package/src/core/checks/checks/content_type_conformance.ts +39 -0
  80. package/src/core/checks/checks/cross_call_references.ts +134 -0
  81. package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
  82. package/src/core/checks/checks/idempotency_replay.ts +246 -0
  83. package/src/core/checks/checks/ignored_auth.ts +211 -0
  84. package/src/core/checks/checks/index.ts +65 -0
  85. package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
  86. package/src/core/checks/checks/missing_required_header.ts +40 -0
  87. package/src/core/checks/checks/negative_data_rejection.ts +45 -0
  88. package/src/core/checks/checks/not_a_server_error.ts +27 -0
  89. package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
  90. package/src/core/checks/checks/pagination_invariants.ts +238 -0
  91. package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
  92. package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
  93. package/src/core/checks/checks/response_headers_conformance.ts +74 -0
  94. package/src/core/checks/checks/response_schema_conformance.ts +30 -0
  95. package/src/core/checks/checks/status_code_conformance.ts +61 -0
  96. package/src/core/checks/checks/unsupported_method.ts +63 -0
  97. package/src/core/checks/checks/use_after_free.ts +78 -0
  98. package/src/core/checks/index.ts +30 -0
  99. package/src/core/checks/mode.ts +79 -0
  100. package/src/core/checks/recommended-action.ts +64 -0
  101. package/src/core/checks/registry.ts +78 -0
  102. package/src/core/checks/runner.ts +874 -0
  103. package/src/core/checks/sarif.ts +230 -0
  104. package/src/core/checks/stateful.ts +121 -0
  105. package/src/core/checks/types.ts +189 -0
  106. package/src/core/classifier/recommended-action.ts +222 -0
  107. package/src/core/context/current.ts +51 -0
  108. package/src/core/context/session.ts +78 -0
  109. package/src/core/coverage/loader.ts +185 -0
  110. package/src/core/coverage/reasons.ts +300 -0
  111. package/src/core/diagnostics/db-analysis.ts +161 -12
  112. package/src/core/diagnostics/failure-class.ts +120 -0
  113. package/src/core/diagnostics/failure-hints.ts +212 -9
  114. package/src/core/diagnostics/spec-pointer.ts +99 -0
  115. package/src/core/diagnostics/suggested-fixes.ts +156 -0
  116. package/src/core/exporter/case-study/index.ts +270 -0
  117. package/src/core/exporter/curl.ts +40 -0
  118. package/src/core/exporter/exporter.ts +48 -0
  119. package/src/core/exporter/html-report/escape.ts +24 -0
  120. package/src/core/exporter/html-report/index.ts +479 -0
  121. package/src/core/exporter/html-report/script.ts +100 -0
  122. package/src/core/exporter/html-report/styles.ts +408 -0
  123. package/src/core/generator/chunker.ts +53 -15
  124. package/src/core/generator/coverage-phase.ts +0 -0
  125. package/src/core/generator/create-body.ts +89 -0
  126. package/src/core/generator/data-factory.ts +490 -33
  127. package/src/core/generator/describe.ts +1 -1
  128. package/src/core/generator/fixtures-builder.ts +325 -0
  129. package/src/core/generator/index.ts +7 -5
  130. package/src/core/generator/openapi-reader.ts +55 -3
  131. package/src/core/generator/path-param-disambig.ts +114 -0
  132. package/src/core/generator/resources-builder.ts +648 -0
  133. package/src/core/generator/schema-utils.ts +11 -3
  134. package/src/core/generator/serializer.ts +114 -15
  135. package/src/core/generator/suite-generator.ts +484 -77
  136. package/src/core/generator/types.ts +8 -0
  137. package/src/core/identity/identity-file.ts +129 -0
  138. package/src/core/lint/affects.ts +28 -0
  139. package/src/core/lint/config.ts +96 -0
  140. package/src/core/lint/format.ts +42 -0
  141. package/src/core/lint/index.ts +94 -0
  142. package/src/core/lint/reporter.ts +128 -0
  143. package/src/core/lint/rules/consistency.ts +158 -0
  144. package/src/core/lint/rules/heuristics.ts +97 -0
  145. package/src/core/lint/rules/strictness.ts +109 -0
  146. package/src/core/lint/types.ts +96 -0
  147. package/src/core/lint/walker.ts +248 -0
  148. package/src/core/meta/meta-store.ts +6 -73
  149. package/src/core/output/README.md +91 -0
  150. package/src/core/output/index.ts +13 -0
  151. package/src/core/output/run.ts +126 -0
  152. package/src/core/output/types.ts +129 -0
  153. package/src/core/parser/env-interpolation.ts +104 -0
  154. package/src/core/parser/filter.ts +57 -0
  155. package/src/core/parser/schema.ts +132 -5
  156. package/src/core/parser/types.ts +29 -2
  157. package/src/core/parser/variables.ts +0 -0
  158. package/src/core/parser/yaml-parser.ts +108 -13
  159. package/src/core/probe/bootstrap.ts +34 -0
  160. package/src/core/probe/dry-run-envelope.ts +57 -0
  161. package/src/core/probe/mass-assignment-probe-class.ts +198 -0
  162. package/src/core/probe/mass-assignment-probe.ts +1122 -0
  163. package/src/core/probe/mass-assignment-template.ts +212 -0
  164. package/src/core/probe/method-probe.ts +164 -0
  165. package/src/core/probe/method-shared.ts +69 -0
  166. package/src/core/probe/negative-probe.ts +691 -0
  167. package/src/core/probe/orphan-tracker.ts +188 -0
  168. package/src/core/probe/path-discovery.ts +440 -0
  169. package/src/core/probe/probe-harness.ts +120 -0
  170. package/src/core/probe/registry.ts +89 -0
  171. package/src/core/probe/runner.ts +136 -0
  172. package/src/core/probe/security-probe-class.ts +201 -0
  173. package/src/core/probe/security-probe.ts +1453 -0
  174. package/src/core/probe/shared.ts +505 -0
  175. package/src/core/probe/static-probe-class.ts +125 -0
  176. package/src/core/probe/types.ts +165 -0
  177. package/src/core/probe/verdict-aggregator.ts +33 -0
  178. package/src/core/probe/webhooks-probe.ts +284 -0
  179. package/src/core/reporter/console.ts +69 -4
  180. package/src/core/reporter/index.ts +2 -3
  181. package/src/core/reporter/json.ts +15 -2
  182. package/src/core/reporter/junit.ts +27 -12
  183. package/src/core/reporter/ndjson.ts +37 -0
  184. package/src/core/reporter/types.ts +3 -0
  185. package/src/core/runner/assertions.ts +62 -2
  186. package/src/core/runner/async-pool.ts +108 -0
  187. package/src/core/runner/auth-path.ts +8 -0
  188. package/src/core/runner/ci-context.ts +72 -0
  189. package/src/core/runner/executor.ts +391 -52
  190. package/src/core/runner/form-encode.ts +51 -0
  191. package/src/core/runner/http-client.ts +115 -7
  192. package/src/core/runner/learn-drift.ts +293 -0
  193. package/src/core/runner/preflight-vars.ts +149 -0
  194. package/src/core/runner/progress-tracker.ts +73 -0
  195. package/src/core/runner/rate-limiter.ts +203 -0
  196. package/src/core/runner/run-kind.ts +39 -0
  197. package/src/core/runner/schema-validator.ts +312 -0
  198. package/src/core/runner/send-request.ts +153 -20
  199. package/src/core/runner/types.ts +38 -0
  200. package/src/core/secrets/registry.ts +164 -0
  201. package/src/core/secrets/secrets-file.ts +115 -0
  202. package/src/core/selectors/operation-filter.ts +144 -0
  203. package/src/core/setup-api.ts +419 -17
  204. package/src/core/severity/category.ts +94 -0
  205. package/src/core/severity/index.ts +121 -0
  206. package/src/core/spec/layers.ts +154 -0
  207. package/src/core/util/format-eta.ts +21 -0
  208. package/src/core/utils.ts +5 -1
  209. package/src/core/workspace/config.ts +129 -0
  210. package/src/core/workspace/manifest.ts +283 -0
  211. package/src/core/workspace/output-rotation.ts +62 -0
  212. package/src/core/workspace/root.ts +94 -0
  213. package/src/core/workspace/triage-path.ts +87 -0
  214. package/src/db/lint-runs.ts +47 -0
  215. package/src/db/migrate.ts +126 -0
  216. package/src/db/migrations/0001_run_kind.sql +25 -0
  217. package/src/db/migrations/sql.d.ts +4 -0
  218. package/src/db/queries/collections.ts +133 -0
  219. package/src/db/queries/coverage.ts +9 -0
  220. package/src/db/queries/dashboard.ts +59 -0
  221. package/src/db/queries/results.ts +128 -0
  222. package/src/db/queries/runs.ts +235 -0
  223. package/src/db/queries/sessions.ts +42 -0
  224. package/src/db/queries/settings.ts +28 -0
  225. package/src/db/queries/types.ts +172 -0
  226. package/src/db/queries.ts +72 -802
  227. package/src/db/schema.ts +179 -48
  228. package/src/cli/commands/export.ts +0 -144
  229. package/src/cli/commands/guide.ts +0 -127
  230. package/src/cli/commands/init.ts +0 -57
  231. package/src/cli/commands/serve.ts +0 -81
  232. package/src/cli/commands/sync.ts +0 -269
  233. package/src/cli/commands/update.ts +0 -189
  234. package/src/cli/commands/validate.ts +0 -34
  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 -21
  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,362 +0,0 @@
1
- /**
2
- * Unified collection state builder for Web UI.
3
- * Aggregates spec endpoints, disk suites, coverage, run results, warnings, env diagnostics.
4
- */
5
-
6
- import type { CollectionRecord, RunRecord, StoredStepResult } from "../../db/queries.ts";
7
- import { listRunsByCollection, getResultsByRunId, getRunById } from "../../db/queries.ts";
8
- import type { EndpointWarning } from "../../core/generator/endpoint-warnings.ts";
9
- import { envHint, statusHint, classifyFailure, computeSharedEnvIssue } from "../../core/diagnostics/failure-hints.ts";
10
- import { join, basename } from "node:path";
11
-
12
- // ── Types ──
13
-
14
- export interface CoveringStep {
15
- suiteName: string;
16
- file: string; // relative filename (e.g. "auth-login.yaml")
17
- stepName: string;
18
- status: "pass" | "fail" | "error" | "skip" | null; // null = not run
19
- responseStatus?: number;
20
- durationMs?: number;
21
- hint?: string; // failure hint
22
- assertions?: { field: string; rule: string; passed: boolean; actual?: unknown; expected?: unknown }[];
23
- }
24
-
25
- export interface EndpointViewState {
26
- method: string;
27
- path: string;
28
- summary?: string;
29
- deprecated: boolean;
30
- hasCoverage: boolean;
31
- runStatus: "passing" | "api_error" | "test_failed" | "not_run" | "no_tests";
32
- warnings: string[];
33
- coveringFiles: string[];
34
- coveringSteps: CoveringStep[];
35
- }
36
-
37
- export interface StepViewState {
38
- name: string;
39
- status: "pass" | "fail" | "error" | "skip";
40
- durationMs?: number;
41
- requestMethod?: string;
42
- requestUrl?: string;
43
- requestBody?: string;
44
- responseStatus?: number;
45
- responseBody?: string;
46
- assertions?: { field: string; rule: string; passed: boolean; actual?: unknown; expected?: unknown }[];
47
- captures?: Record<string, unknown>;
48
- hint?: string;
49
- errorMessage?: string;
50
- }
51
-
52
- export interface SuiteViewState {
53
- name: string;
54
- description?: string;
55
- tags: string[];
56
- stepCount: number;
57
- filePath: string;
58
- status: "passed" | "failed" | "not_run" | "parse_error";
59
- runResult?: { passed: number; failed: number; skipped: number };
60
- parseError?: string;
61
- steps: StepViewState[];
62
- }
63
-
64
- export interface CollectionState {
65
- collection: CollectionRecord;
66
- endpoints: EndpointViewState[];
67
- totalEndpoints: number;
68
- coveragePct: number;
69
- coveredCount: number;
70
- suites: SuiteViewState[];
71
- latestRun: RunRecord | null;
72
- latestRunResults: StoredStepResult[];
73
- envAlert: string | null;
74
- warnings: EndpointWarning[];
75
- // Run stats
76
- runPassed: number;
77
- runFailed: number;
78
- runSkipped: number;
79
- runTotal: number;
80
- runDurationMs: number | null;
81
- }
82
-
83
- // ── Cache ──
84
-
85
- interface CacheEntry {
86
- state: CollectionState;
87
- timestamp: number;
88
- }
89
-
90
- const cache = new Map<number, CacheEntry>();
91
- const CACHE_TTL_MS = 30_000;
92
-
93
- export function invalidateCollectionCache(collectionId: number): void {
94
- cache.delete(collectionId);
95
- }
96
-
97
- // ── Builder ──
98
-
99
- export async function buildCollectionState(collection: CollectionRecord): Promise<CollectionState> {
100
- const cached = cache.get(collection.id);
101
- if (cached && Date.now() - cached.timestamp < CACHE_TTL_MS) {
102
- return cached.state;
103
- }
104
-
105
- // Load spec endpoints
106
- let specEndpoints: import("../../core/generator/types.ts").EndpointInfo[] = [];
107
- let warnings: EndpointWarning[] = [];
108
- if (collection.openapi_spec) {
109
- try {
110
- const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
111
- const { analyzeEndpoints } = await import("../../core/generator/endpoint-warnings.ts");
112
- const doc = await readOpenApiSpec(collection.openapi_spec);
113
- specEndpoints = extractEndpoints(doc);
114
- warnings = analyzeEndpoints(specEndpoints);
115
- } catch { /* spec unavailable */ }
116
- }
117
-
118
- // Scan coverage from disk
119
- const { scanCoveredEndpoints } = await import("../../core/generator/coverage-scanner.ts");
120
- const { specPathToRegex, normalizePath } = await import("../../core/generator/coverage-scanner.ts");
121
- let coveredEndpoints: import("../../core/generator/coverage-scanner.ts").CoveredEndpoint[] = [];
122
- try {
123
- coveredEndpoints = await scanCoveredEndpoints(collection.test_path);
124
- } catch { /* no tests on disk */ }
125
-
126
- // Parse suites from disk
127
- const { parseDirectorySafe } = await import("../../core/parser/yaml-parser.ts");
128
- let diskSuites: import("../../core/parser/yaml-parser.ts").ParseDirectoryResult = { suites: [], errors: [] };
129
- try {
130
- diskSuites = await parseDirectorySafe(collection.test_path);
131
- } catch { /* test dir missing */ }
132
-
133
- // Get latest run
134
- const runs = listRunsByCollection(collection.id, 1, 0);
135
- const latestRun = runs.length > 0 ? (getRunById(runs[0]!.id) ?? null) : null;
136
- const latestRunResults = latestRun ? getResultsByRunId(latestRun.id) : [];
137
-
138
- // Build result maps: suite_name -> step statuses
139
- const suiteResultMap = new Map<string, StoredStepResult[]>();
140
- for (const r of latestRunResults) {
141
- const list = suiteResultMap.get(r.suite_name) ?? [];
142
- list.push(r);
143
- suiteResultMap.set(r.suite_name, list);
144
- }
145
-
146
- // Build endpoint -> run status map
147
- // key: "METHOD /path" from results
148
- const endpointRunStatusMap = new Map<string, "passing" | "api_error" | "test_failed">();
149
- for (const r of latestRunResults) {
150
- if (r.request_method && r.request_url) {
151
- // Extract path from URL
152
- let urlPath: string;
153
- try {
154
- const u = new URL(r.request_url);
155
- urlPath = u.pathname;
156
- } catch {
157
- urlPath = r.request_url;
158
- }
159
- const key = `${r.request_method} ${normalizePath(urlPath)}`;
160
- const current = endpointRunStatusMap.get(key);
161
- if (r.status === "fail" || r.status === "error") {
162
- const ft = classifyFailure(r.status, r.response_status);
163
- endpointRunStatusMap.set(key, ft === "api_error" ? "api_error" : "test_failed");
164
- } else if (r.status === "pass" && current !== "test_failed" && current !== "api_error") {
165
- endpointRunStatusMap.set(key, "passing");
166
- }
167
- }
168
- }
169
-
170
- // Build endpoint view states
171
- const warningsMap = new Map<string, string[]>();
172
- for (const w of warnings) {
173
- warningsMap.set(`${w.method} ${w.path}`, w.warnings);
174
- }
175
-
176
- // Build map: normalized "METHOD /path" -> list of StoredStepResult
177
- const resultsByEndpoint = new Map<string, StoredStepResult[]>();
178
- for (const r of latestRunResults) {
179
- if (r.request_method && r.request_url) {
180
- let urlPath: string;
181
- try { urlPath = new URL(r.request_url).pathname; } catch { urlPath = r.request_url; }
182
- const key = `${r.request_method} ${normalizePath(urlPath)}`;
183
- const list = resultsByEndpoint.get(key) ?? [];
184
- list.push(r);
185
- resultsByEndpoint.set(key, list);
186
- }
187
- }
188
-
189
- // Map suite name -> file basename for display
190
- const suiteNameToFile = new Map<string, string>();
191
- for (const s of diskSuites.suites) {
192
- suiteNameToFile.set(s.name, basename(s.filePath ?? s.name));
193
- }
194
-
195
- // Env file path for hints
196
- const envFilePath = collection.base_dir
197
- ? join(collection.base_dir, ".env.yaml").replace(/\\/g, "/")
198
- : undefined;
199
-
200
- const endpoints: EndpointViewState[] = specEndpoints.map(ep => {
201
- const specRegex = specPathToRegex(ep.path);
202
- const covering = coveredEndpoints.filter(
203
- c => c.method === ep.method && specRegex.test(normalizePath(c.path)),
204
- );
205
- const hasCoverage = covering.length > 0;
206
-
207
- // Determine run status
208
- let runStatus: EndpointViewState["runStatus"] = "no_tests";
209
- if (hasCoverage) {
210
- runStatus = "not_run";
211
- for (const [key, status] of endpointRunStatusMap) {
212
- const [method, path] = [key.split(" ")[0], key.split(" ").slice(1).join(" ")];
213
- if (method === ep.method && specRegex.test(path!)) {
214
- runStatus = status;
215
- break;
216
- }
217
- }
218
- }
219
-
220
- // Build covering steps from run results
221
- const coveringSteps: CoveringStep[] = [];
222
- for (const [key, results] of resultsByEndpoint) {
223
- const [method, path] = [key.split(" ")[0], key.split(" ").slice(1).join(" ")];
224
- if (method === ep.method && specRegex.test(path!)) {
225
- for (const r of results) {
226
- const hint = (r.status === "fail" || r.status === "error")
227
- ? (envHint(r.request_url, r.error_message, envFilePath) ?? statusHint(r.response_status) ?? undefined)
228
- : undefined;
229
- coveringSteps.push({
230
- suiteName: r.suite_name,
231
- file: suiteNameToFile.get(r.suite_name) ?? r.suite_name,
232
- stepName: r.test_name,
233
- status: r.status as CoveringStep["status"],
234
- responseStatus: r.response_status ?? undefined,
235
- durationMs: r.duration_ms ?? undefined,
236
- hint,
237
- assertions: Array.isArray(r.assertions) ? r.assertions : undefined,
238
- });
239
- }
240
- }
241
- }
242
-
243
- return {
244
- method: ep.method,
245
- path: ep.path,
246
- summary: ep.summary,
247
- deprecated: ep.deprecated ?? false,
248
- hasCoverage,
249
- runStatus,
250
- warnings: warningsMap.get(`${ep.method} ${ep.path}`) ?? [],
251
- coveringFiles: covering.map(c => c.file),
252
- coveringSteps,
253
- };
254
- });
255
-
256
- // Coverage stats
257
- const totalEndpoints = endpoints.length;
258
- const coveredCount = endpoints.filter(e => e.hasCoverage).length;
259
- const coveragePct = totalEndpoints > 0 ? Math.round((coveredCount / totalEndpoints) * 100) : 0;
260
-
261
- // Build suite view states
262
- const suites: SuiteViewState[] = diskSuites.suites.map(s => {
263
- const results = suiteResultMap.get(s.name);
264
- let status: SuiteViewState["status"] = "not_run";
265
- let runResult: SuiteViewState["runResult"] | undefined;
266
- const steps: StepViewState[] = [];
267
-
268
- if (results) {
269
- const passed = results.filter(r => r.status === "pass").length;
270
- const failed = results.filter(r => r.status === "fail" || r.status === "error").length;
271
- const skipped = results.filter(r => r.status === "skip").length;
272
- runResult = { passed, failed, skipped };
273
- status = failed > 0 ? "failed" : "passed";
274
-
275
- for (const r of results) {
276
- const hint = (r.status === "fail" || r.status === "error")
277
- ? (envHint(r.request_url, r.error_message, envFilePath) ?? statusHint(r.response_status) ?? undefined)
278
- : undefined;
279
- steps.push({
280
- name: r.test_name,
281
- status: r.status as StepViewState["status"],
282
- durationMs: r.duration_ms ?? undefined,
283
- requestMethod: r.request_method ?? undefined,
284
- requestUrl: r.request_url ?? undefined,
285
- requestBody: r.request_body ?? undefined,
286
- responseStatus: r.response_status ?? undefined,
287
- responseBody: r.response_body ?? undefined,
288
- assertions: Array.isArray(r.assertions) ? r.assertions : undefined,
289
- captures: r.captures && typeof r.captures === "object" ? r.captures as Record<string, unknown> : undefined,
290
- hint,
291
- errorMessage: r.error_message ?? undefined,
292
- });
293
- }
294
- }
295
-
296
- return {
297
- name: s.name,
298
- description: s.description,
299
- tags: s.tags ?? [],
300
- stepCount: s.tests.length,
301
- filePath: s.filePath ?? "",
302
- status,
303
- runResult,
304
- steps,
305
- };
306
- });
307
-
308
- // Add parse errors as suites
309
- for (const err of diskSuites.errors) {
310
- suites.push({
311
- name: err.file,
312
- tags: [],
313
- stepCount: 0,
314
- filePath: err.file,
315
- status: "parse_error",
316
- parseError: err.error,
317
- steps: [],
318
- });
319
- }
320
-
321
- // Env diagnostics
322
- let envAlert: string | null = null;
323
- const failedResults = latestRunResults.filter(r => r.status === "fail" || r.status === "error");
324
- if (failedResults.length > 0) {
325
- const envFilePath = collection.base_dir
326
- ? join(collection.base_dir, ".env.yaml").replace(/\\/g, "/")
327
- : undefined;
328
-
329
- const failuresWithHints = failedResults.map(r => ({
330
- hint: envHint(r.request_url, r.error_message, envFilePath) ?? undefined,
331
- }));
332
- envAlert = computeSharedEnvIssue(failuresWithHints, envFilePath);
333
- }
334
-
335
- // Run stats
336
- const runPassed = latestRun?.passed ?? 0;
337
- const runFailed = latestRun?.failed ?? 0;
338
- const runSkipped = latestRun?.skipped ?? 0;
339
- const runTotal = latestRun?.total ?? 0;
340
- const runDurationMs = latestRun?.duration_ms ?? null;
341
-
342
- const state: CollectionState = {
343
- collection,
344
- endpoints,
345
- totalEndpoints,
346
- coveragePct,
347
- coveredCount,
348
- suites,
349
- latestRun,
350
- latestRunResults,
351
- envAlert,
352
- warnings,
353
- runPassed,
354
- runFailed,
355
- runSkipped,
356
- runTotal,
357
- runDurationMs,
358
- };
359
-
360
- cache.set(collection.id, { state, timestamp: Date.now() });
361
- return state;
362
- }
@@ -1,314 +0,0 @@
1
- import { OpenAPIHono, createRoute } from "@hono/zod-openapi";
2
- import { getRunById, getResultsByRunId } from "../../db/queries.ts";
3
- import { generateJunitXml } from "../../core/reporter/junit.ts";
4
- import { executeRun } from "../../core/runner/execute-run.ts";
5
- import { statusBadge, renderSuiteResults, failedFilterToggle, autoExpandFailedScript } from "../views/results.ts";
6
- import { formatDuration } from "../../core/reporter/console.ts";
7
- import type { TestRunResult, StepResult } from "../../core/runner/types.ts";
8
- import {
9
- ErrorSchema,
10
- RunRequestSchema,
11
- RunResponseSchema,
12
- RunDetailSchema,
13
- RunIdParam,
14
- } from "../schemas.ts";
15
- import { renderProxyResponse, renderProxyError } from "../views/explorer-tab.ts";
16
-
17
- const api = new OpenAPIHono();
18
-
19
- // ──────────────────────────────────────────────
20
- // POST /run — form-data handler for HTMX
21
- // ──────────────────────────────────────────────
22
-
23
- api.post("/run", async (c) => {
24
- try {
25
- const form = await c.req.parseBody();
26
- const testPath = form["path"] as string;
27
- const envName = (form["env"] as string) || undefined;
28
-
29
- if (!testPath) {
30
- return c.json({ error: "Missing 'path' field" }, 400);
31
- }
32
-
33
- const { runId } = await executeRun({ testPath, envName, trigger: "webui" });
34
-
35
- // If targeted at the results panel (dashboard), return inline HTML
36
- const hxTarget = c.req.header("HX-Target");
37
- if (hxTarget === "results-panel") {
38
- const run = getRunById(runId);
39
- if (!run) {
40
- c.header("HX-Redirect", `/runs/${runId}`);
41
- return c.json({ runId });
42
- }
43
- const results = getResultsByRunId(runId);
44
- const passed = run.passed;
45
- const failed = run.failed;
46
- const skipped = run.skipped;
47
- const total = run.total;
48
- const duration = run.duration_ms != null ? formatDuration(run.duration_ms) : "-";
49
-
50
- const header = `
51
- <div style="display:flex;align-items:center;gap:0.75rem;flex-wrap:wrap;margin-bottom:0.5rem;padding-bottom:0.5rem;border-bottom:1px solid var(--border);">
52
- <strong>Run #${run.id}</strong>
53
- <span style="color:var(--text-dim);font-size:0.85rem;">just now</span>
54
- <span style="font-size:0.9rem;">${passed}&#10003; ${failed}&#10007; ${skipped}&#9675;</span>
55
- <span style="color:var(--text-dim);font-size:0.85rem;">${duration}</span>
56
- ${statusBadge(total, passed, failed)}
57
- <span style="flex:1;"></span>
58
- <a href="/api/export/${run.id}/junit" download class="btn btn-sm btn-outline">Export JUnit</a>
59
- <a href="/api/export/${run.id}/json" download class="btn btn-sm btn-outline">Export JSON</a>
60
- ${failedFilterToggle()}
61
- </div>`;
62
-
63
- const suitesHtml = renderSuiteResults(results, runId);
64
- return c.html(header + suitesHtml + autoExpandFailedScript());
65
- }
66
-
67
- // Default: redirect to run detail page
68
- c.header("HX-Redirect", `/runs/${runId}`);
69
- return c.json({ runId });
70
- } catch (err) {
71
- const hxTarget = c.req.header("HX-Target");
72
- if (hxTarget === "results-panel") {
73
- return c.html(`<div style="color:var(--fail);padding:1rem;border:1px solid var(--fail);border-radius:6px;">Error: ${(err as Error).message}</div>`, 500);
74
- }
75
- return c.json({ error: (err as Error).message }, 500);
76
- }
77
- });
78
-
79
- const runRoute = createRoute({
80
- method: "post",
81
- path: "/api/run",
82
- tags: ["Runs"],
83
- summary: "Run tests",
84
- request: {
85
- body: {
86
- content: { "application/json": { schema: RunRequestSchema } },
87
- required: true,
88
- },
89
- },
90
- responses: {
91
- 200: {
92
- content: { "application/json": { schema: RunResponseSchema } },
93
- description: "Run created",
94
- },
95
- 400: {
96
- content: { "application/json": { schema: ErrorSchema } },
97
- description: "Validation error",
98
- },
99
- 500: {
100
- content: { "application/json": { schema: ErrorSchema } },
101
- description: "Server error",
102
- },
103
- },
104
- });
105
-
106
- api.openapi(runRoute, async (c) => {
107
- try {
108
- const { path: testPath, env: envName } = c.req.valid("json");
109
- const { runId } = await executeRun({ testPath, envName, trigger: "webui" });
110
-
111
- c.header("HX-Redirect", `/runs/${runId}`);
112
- return c.json({ runId }, 200);
113
- } catch (err) {
114
- return c.json({ error: (err as Error).message }, 500);
115
- }
116
- });
117
-
118
- // ──────────────────────────────────────────────
119
- // POST /api/proxy — Explorer proxy for HTTP requests
120
- // ──────────────────────────────────────────────
121
-
122
- api.post("/api/proxy", async (c) => {
123
- const form = await c.req.parseBody();
124
- const baseUrl = (form["base_url"] as string) ?? "";
125
- const method = ((form["method"] as string) ?? "GET").toUpperCase();
126
- let path = (form["path"] as string) ?? "/";
127
- const body = (form["body"] as string) || undefined;
128
- const contentType = (form["content_type"] as string) || undefined;
129
-
130
- if (!baseUrl) {
131
- return c.html(renderProxyError("Base URL is required", 0));
132
- }
133
-
134
- // Substitute path parameters
135
- for (const [key, value] of Object.entries(form)) {
136
- if (typeof key === "string" && key.startsWith("param_path_") && value) {
137
- const paramName = key.slice("param_path_".length);
138
- path = path.replace(`{${paramName}}`, encodeURIComponent(value as string));
139
- }
140
- }
141
-
142
- // Build query string
143
- const queryParams = new URLSearchParams();
144
- for (const [key, value] of Object.entries(form)) {
145
- if (typeof key === "string" && key.startsWith("param_query_") && value) {
146
- queryParams.set(key.slice("param_query_".length), value as string);
147
- }
148
- }
149
-
150
- // Build headers
151
- const headers: Record<string, string> = {};
152
- for (const [key, value] of Object.entries(form)) {
153
- if (typeof key === "string" && key.startsWith("param_header_") && value) {
154
- headers[key.slice("param_header_".length)] = value as string;
155
- }
156
- }
157
- // Custom headers
158
- for (let i = 0; i < 50; i++) {
159
- const k = form[`custom_header_key_${i}`] as string | undefined;
160
- const v = form[`custom_header_value_${i}`] as string | undefined;
161
- if (!k && !v) break;
162
- if (k && v) headers[k] = v;
163
- }
164
-
165
- if (contentType && body) {
166
- headers["Content-Type"] = contentType;
167
- }
168
-
169
- // Build URL
170
- let url: URL;
171
- try {
172
- url = new URL(path, baseUrl.endsWith("/") ? baseUrl : baseUrl + "/");
173
- queryParams.forEach((v, k) => url.searchParams.set(k, v));
174
- } catch (err) {
175
- return c.html(renderProxyError(`Invalid URL: ${baseUrl}${path} — ${(err as Error).message}`, 0));
176
- }
177
-
178
- const startTime = performance.now();
179
- try {
180
- const resp = await fetch(url.toString(), {
181
- method,
182
- headers,
183
- body: ["GET", "HEAD"].includes(method) ? undefined : (body || undefined),
184
- });
185
- const elapsed = Math.round(performance.now() - startTime);
186
- const respBody = await resp.text();
187
- const respHeaders: Record<string, string> = {};
188
- resp.headers.forEach((v, k) => { respHeaders[k] = v; });
189
-
190
- return c.html(renderProxyResponse(resp.status, respHeaders, respBody, elapsed));
191
- } catch (err) {
192
- const elapsed = Math.round(performance.now() - startTime);
193
- return c.html(renderProxyError((err as Error).message, elapsed));
194
- }
195
- });
196
-
197
- // ──────────────────────────────────────────────
198
- // Export helpers
199
- // ──────────────────────────────────────────────
200
-
201
- function reconstructResults(runId: number): TestRunResult[] | null {
202
- const run = getRunById(runId);
203
- if (!run) return null;
204
-
205
- const rows = getResultsByRunId(runId);
206
- const suiteMap = new Map<string, StepResult[]>();
207
-
208
- for (const row of rows) {
209
- const steps = suiteMap.get(row.suite_name) ?? [];
210
- steps.push({
211
- name: row.test_name,
212
- status: row.status as StepResult["status"],
213
- duration_ms: row.duration_ms,
214
- request: {
215
- method: row.request_method ?? "GET",
216
- url: row.request_url ?? "",
217
- headers: {},
218
- },
219
- response: row.response_status != null
220
- ? { status: row.response_status, headers: {}, body: "", duration_ms: row.duration_ms }
221
- : undefined,
222
- assertions: row.assertions,
223
- captures: row.captures as Record<string, unknown>,
224
- error: row.error_message ?? undefined,
225
- });
226
- suiteMap.set(row.suite_name, steps);
227
- }
228
-
229
- const results: TestRunResult[] = [];
230
- for (const [suiteName, steps] of suiteMap) {
231
- const total = steps.length;
232
- const passed = steps.filter((s) => s.status === "pass").length;
233
- const failed = steps.filter((s) => s.status === "fail").length;
234
- const skipped = steps.filter((s) => s.status === "skip").length;
235
- results.push({
236
- suite_name: suiteName,
237
- started_at: run.started_at,
238
- finished_at: run.finished_at ?? run.started_at,
239
- total,
240
- passed,
241
- failed,
242
- skipped,
243
- steps,
244
- });
245
- }
246
- return results;
247
- }
248
-
249
- // ──────────────────────────────────────────────
250
- // Export routes (OpenAPI-documented)
251
- // ──────────────────────────────────────────────
252
-
253
- const exportJsonRoute = createRoute({
254
- method: "get",
255
- path: "/api/export/{runId}/json",
256
- tags: ["Export"],
257
- summary: "Export run results as JSON",
258
- request: { params: RunIdParam },
259
- responses: {
260
- 200: {
261
- content: { "application/json": { schema: RunDetailSchema } },
262
- description: "Run results",
263
- },
264
- 400: {
265
- content: { "application/json": { schema: ErrorSchema } },
266
- description: "Invalid run ID",
267
- },
268
- 404: {
269
- content: { "application/json": { schema: ErrorSchema } },
270
- description: "Run not found",
271
- },
272
- },
273
- });
274
-
275
- api.openapi(exportJsonRoute, (c) => {
276
- const { runId } = c.req.valid("param");
277
- const results = reconstructResults(runId);
278
- if (!results) return c.json({ error: "Run not found" }, 404);
279
-
280
- c.header("Content-Disposition", `attachment; filename="run-${runId}-results.json"`);
281
- return c.json(results as any, 200);
282
- });
283
-
284
- const exportJunitRoute = createRoute({
285
- method: "get",
286
- path: "/api/export/{runId}/junit",
287
- tags: ["Export"],
288
- summary: "Export run results as JUnit XML",
289
- request: { params: RunIdParam },
290
- responses: {
291
- 200: { description: "JUnit XML file" },
292
- 400: {
293
- content: { "application/json": { schema: ErrorSchema } },
294
- description: "Invalid run ID",
295
- },
296
- 404: {
297
- content: { "application/json": { schema: ErrorSchema } },
298
- description: "Run not found",
299
- },
300
- },
301
- });
302
-
303
- api.openapi(exportJunitRoute, (c) => {
304
- const { runId } = c.req.valid("param");
305
- const results = reconstructResults(runId);
306
- if (!results) return c.json({ error: "Run not found" }, 404);
307
-
308
- const xml = generateJunitXml(results);
309
- c.header("Content-Disposition", `attachment; filename="run-${runId}-junit.xml"`);
310
- c.header("Content-Type", "application/xml");
311
- return c.body(xml);
312
- });
313
-
314
- export default api;