@kirrosh/zond 0.21.0 → 0.23.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +758 -3
- package/README.md +78 -15
- package/package.json +17 -10
- package/src/cli/argv.ts +122 -0
- package/src/cli/commands/add-api.ts +134 -0
- package/src/cli/commands/api/annotate/idempotency.ts +59 -0
- package/src/cli/commands/api/annotate/index.ts +525 -0
- package/src/cli/commands/api/annotate/lifecycle.ts +74 -0
- package/src/cli/commands/api/annotate/overlay.ts +206 -0
- package/src/cli/commands/api/annotate/pagination.ts +60 -0
- package/src/cli/commands/api/annotate/prompts.ts +183 -0
- package/src/cli/commands/api/annotate/readback.ts +58 -0
- package/src/cli/commands/api/annotate/resources.ts +91 -0
- package/src/cli/commands/api/annotate/seed-bodies.ts +61 -0
- package/src/cli/commands/audit.ts +480 -0
- package/src/cli/commands/bootstrap.ts +710 -0
- package/src/cli/commands/catalog.ts +35 -0
- package/src/cli/commands/check.ts +348 -0
- package/src/cli/commands/checks.ts +756 -0
- package/src/cli/commands/ci-init.ts +55 -6
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +192 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +180 -8
- package/src/cli/commands/describe.ts +37 -2
- package/src/cli/commands/discover.ts +1236 -0
- package/src/cli/commands/doctor.ts +607 -0
- package/src/cli/commands/fixtures.ts +402 -0
- package/src/cli/commands/generate.ts +420 -47
- package/src/cli/commands/init/agents-md.ts +61 -0
- package/src/cli/commands/init/bootstrap.ts +108 -0
- package/src/cli/commands/init/index.ts +244 -0
- package/src/cli/commands/init/skills.ts +98 -0
- package/src/cli/commands/init/templates/agents.md +77 -0
- package/src/cli/commands/init/templates/markdown.d.ts +4 -0
- package/src/cli/commands/init/templates/skills/zond-checks.md +397 -0
- package/src/cli/commands/init/templates/skills/zond-triage.md +210 -0
- package/src/cli/commands/init/templates/skills/zond.md +651 -0
- package/src/cli/commands/init/templates/zond-config.yml +14 -0
- package/src/cli/commands/prepare-fixtures.ts +135 -0
- package/src/cli/commands/probe/mass-assignment.ts +503 -0
- package/src/cli/commands/probe/security.ts +454 -0
- package/src/cli/commands/probe/static.ts +255 -0
- package/src/cli/commands/probe/webhooks.ts +161 -0
- package/src/cli/commands/probe.ts +459 -0
- package/src/cli/commands/reference.ts +87 -0
- package/src/cli/commands/refresh-api.ts +169 -0
- package/src/cli/commands/remove-api.ts +150 -0
- package/src/cli/commands/report-bundle.ts +318 -0
- package/src/cli/commands/report.ts +241 -0
- package/src/cli/commands/request.ts +379 -4
- package/src/cli/commands/run.ts +911 -33
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +74 -0
- package/src/cli/index.ts +36 -607
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +218 -0
- package/src/cli/resolve.ts +105 -0
- package/src/cli/status-filter.ts +124 -0
- package/src/cli/util/api-context.ts +85 -0
- package/src/cli/version.ts +8 -0
- package/src/core/anti-fp/bootstrap.ts +34 -0
- package/src/core/anti-fp/index.ts +33 -0
- package/src/core/anti-fp/registry.ts +44 -0
- package/src/core/anti-fp/rules/baseline-echo.ts +74 -0
- package/src/core/anti-fp/rules/schemathesis/body_negation_becomes_valid.ts +52 -0
- package/src/core/anti-fp/rules/schemathesis/coverage_phase_boundary_positive.ts +38 -0
- package/src/core/anti-fp/rules/schemathesis/has_unverifiable_mutations.ts +35 -0
- package/src/core/anti-fp/rules/schemathesis/index.ts +24 -0
- package/src/core/anti-fp/rules/schemathesis/string_type_mutation_becomes_valid.ts +53 -0
- package/src/core/anti-fp/rules/subscription-gated/index.ts +11 -0
- package/src/core/anti-fp/rules/subscription-gated/paid-plan-403.ts +75 -0
- package/src/core/anti-fp/types.ts +68 -0
- package/src/core/checks/checks/_crud-helpers.ts +133 -0
- package/src/core/checks/checks/_negative_mutator.ts +133 -0
- package/src/core/checks/checks/_readback-helpers.ts +133 -0
- package/src/core/checks/checks/content_type_conformance.ts +39 -0
- package/src/core/checks/checks/cross_call_references.ts +134 -0
- package/src/core/checks/checks/ensure_resource_availability.ts +62 -0
- package/src/core/checks/checks/idempotency_replay.ts +246 -0
- package/src/core/checks/checks/ignored_auth.ts +211 -0
- package/src/core/checks/checks/index.ts +65 -0
- package/src/core/checks/checks/lifecycle_transitions.ts +273 -0
- package/src/core/checks/checks/missing_required_header.ts +40 -0
- package/src/core/checks/checks/negative_data_rejection.ts +45 -0
- package/src/core/checks/checks/not_a_server_error.ts +27 -0
- package/src/core/checks/checks/open_cors_on_sensitive.ts +131 -0
- package/src/core/checks/checks/pagination_invariants.ts +238 -0
- package/src/core/checks/checks/positive_data_acceptance.ts +36 -0
- package/src/core/checks/checks/rate_limit_headers_absent.ts +77 -0
- package/src/core/checks/checks/response_headers_conformance.ts +74 -0
- package/src/core/checks/checks/response_schema_conformance.ts +30 -0
- package/src/core/checks/checks/status_code_conformance.ts +61 -0
- package/src/core/checks/checks/unsupported_method.ts +63 -0
- package/src/core/checks/checks/use_after_free.ts +78 -0
- package/src/core/checks/index.ts +30 -0
- package/src/core/checks/mode.ts +79 -0
- package/src/core/checks/recommended-action.ts +64 -0
- package/src/core/checks/registry.ts +78 -0
- package/src/core/checks/runner.ts +874 -0
- package/src/core/checks/sarif.ts +230 -0
- package/src/core/checks/stateful.ts +121 -0
- package/src/core/checks/types.ts +189 -0
- package/src/core/classifier/recommended-action.ts +222 -0
- package/src/core/context/current.ts +51 -0
- package/src/core/context/session.ts +78 -0
- package/src/core/coverage/loader.ts +185 -0
- package/src/core/coverage/reasons.ts +300 -0
- package/src/core/diagnostics/db-analysis.ts +161 -12
- package/src/core/diagnostics/failure-class.ts +120 -0
- package/src/core/diagnostics/failure-hints.ts +212 -9
- package/src/core/diagnostics/spec-pointer.ts +99 -0
- package/src/core/diagnostics/suggested-fixes.ts +156 -0
- package/src/core/exporter/case-study/index.ts +270 -0
- package/src/core/exporter/curl.ts +40 -0
- package/src/core/exporter/exporter.ts +48 -0
- package/src/core/exporter/html-report/escape.ts +24 -0
- package/src/core/exporter/html-report/index.ts +479 -0
- package/src/core/exporter/html-report/script.ts +100 -0
- package/src/core/exporter/html-report/styles.ts +408 -0
- package/src/core/generator/chunker.ts +53 -15
- package/src/core/generator/coverage-phase.ts +0 -0
- package/src/core/generator/create-body.ts +89 -0
- package/src/core/generator/data-factory.ts +490 -33
- package/src/core/generator/describe.ts +1 -1
- package/src/core/generator/fixtures-builder.ts +325 -0
- package/src/core/generator/index.ts +7 -5
- package/src/core/generator/openapi-reader.ts +55 -3
- package/src/core/generator/path-param-disambig.ts +114 -0
- package/src/core/generator/resources-builder.ts +648 -0
- package/src/core/generator/schema-utils.ts +11 -3
- package/src/core/generator/serializer.ts +114 -15
- package/src/core/generator/suite-generator.ts +484 -77
- package/src/core/generator/types.ts +8 -0
- package/src/core/identity/identity-file.ts +129 -0
- package/src/core/lint/affects.ts +28 -0
- package/src/core/lint/config.ts +96 -0
- package/src/core/lint/format.ts +42 -0
- package/src/core/lint/index.ts +94 -0
- package/src/core/lint/reporter.ts +128 -0
- package/src/core/lint/rules/consistency.ts +158 -0
- package/src/core/lint/rules/heuristics.ts +97 -0
- package/src/core/lint/rules/strictness.ts +109 -0
- package/src/core/lint/types.ts +96 -0
- package/src/core/lint/walker.ts +248 -0
- package/src/core/meta/meta-store.ts +6 -73
- package/src/core/output/README.md +91 -0
- package/src/core/output/index.ts +13 -0
- package/src/core/output/run.ts +126 -0
- package/src/core/output/types.ts +129 -0
- package/src/core/parser/env-interpolation.ts +104 -0
- package/src/core/parser/filter.ts +57 -0
- package/src/core/parser/schema.ts +132 -5
- package/src/core/parser/types.ts +29 -2
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +108 -13
- package/src/core/probe/bootstrap.ts +34 -0
- package/src/core/probe/dry-run-envelope.ts +57 -0
- package/src/core/probe/mass-assignment-probe-class.ts +198 -0
- package/src/core/probe/mass-assignment-probe.ts +1122 -0
- package/src/core/probe/mass-assignment-template.ts +212 -0
- package/src/core/probe/method-probe.ts +164 -0
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +691 -0
- package/src/core/probe/orphan-tracker.ts +188 -0
- package/src/core/probe/path-discovery.ts +440 -0
- package/src/core/probe/probe-harness.ts +120 -0
- package/src/core/probe/registry.ts +89 -0
- package/src/core/probe/runner.ts +136 -0
- package/src/core/probe/security-probe-class.ts +201 -0
- package/src/core/probe/security-probe.ts +1453 -0
- package/src/core/probe/shared.ts +505 -0
- package/src/core/probe/static-probe-class.ts +125 -0
- package/src/core/probe/types.ts +165 -0
- package/src/core/probe/verdict-aggregator.ts +33 -0
- package/src/core/probe/webhooks-probe.ts +284 -0
- package/src/core/reporter/console.ts +69 -4
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +15 -2
- package/src/core/reporter/junit.ts +27 -12
- package/src/core/reporter/ndjson.ts +37 -0
- package/src/core/reporter/types.ts +3 -0
- package/src/core/runner/assertions.ts +62 -2
- package/src/core/runner/async-pool.ts +108 -0
- package/src/core/runner/auth-path.ts +8 -0
- package/src/core/runner/ci-context.ts +72 -0
- package/src/core/runner/executor.ts +391 -52
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +115 -7
- package/src/core/runner/learn-drift.ts +293 -0
- package/src/core/runner/preflight-vars.ts +149 -0
- package/src/core/runner/progress-tracker.ts +73 -0
- package/src/core/runner/rate-limiter.ts +203 -0
- package/src/core/runner/run-kind.ts +39 -0
- package/src/core/runner/schema-validator.ts +312 -0
- package/src/core/runner/send-request.ts +153 -20
- package/src/core/runner/types.ts +38 -0
- package/src/core/secrets/registry.ts +164 -0
- package/src/core/secrets/secrets-file.ts +115 -0
- package/src/core/selectors/operation-filter.ts +144 -0
- package/src/core/setup-api.ts +419 -17
- package/src/core/severity/category.ts +94 -0
- package/src/core/severity/index.ts +121 -0
- package/src/core/spec/layers.ts +154 -0
- package/src/core/util/format-eta.ts +21 -0
- package/src/core/utils.ts +5 -1
- package/src/core/workspace/config.ts +129 -0
- package/src/core/workspace/manifest.ts +283 -0
- package/src/core/workspace/output-rotation.ts +62 -0
- package/src/core/workspace/root.ts +94 -0
- package/src/core/workspace/triage-path.ts +87 -0
- package/src/db/lint-runs.ts +47 -0
- package/src/db/migrate.ts +126 -0
- package/src/db/migrations/0001_run_kind.sql +25 -0
- package/src/db/migrations/sql.d.ts +4 -0
- package/src/db/queries/collections.ts +133 -0
- package/src/db/queries/coverage.ts +9 -0
- package/src/db/queries/dashboard.ts +59 -0
- package/src/db/queries/results.ts +128 -0
- package/src/db/queries/runs.ts +235 -0
- package/src/db/queries/sessions.ts +42 -0
- package/src/db/queries/settings.ts +28 -0
- package/src/db/queries/types.ts +172 -0
- package/src/db/queries.ts +72 -802
- package/src/db/schema.ts +179 -48
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init.ts +0 -57
- package/src/cli/commands/serve.ts +0 -81
- package/src/cli/commands/sync.ts +0 -269
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -21
- package/src/core/parser/index.ts +0 -21
- package/src/core/runner/execute-run.ts +0 -132
- package/src/core/runner/index.ts +0 -12
- package/src/core/sync/spec-differ.ts +0 -38
- package/src/web/data/collection-state.ts +0 -362
- package/src/web/routes/api.ts +0 -314
- package/src/web/routes/dashboard.ts +0 -350
- package/src/web/routes/runs.ts +0 -64
- package/src/web/schemas.ts +0 -121
- package/src/web/server.ts +0 -134
- package/src/web/static/htmx.min.cjs +0 -1
- package/src/web/static/style.css +0 -1148
- package/src/web/views/endpoints-tab.ts +0 -174
- package/src/web/views/explorer-tab.ts +0 -402
- package/src/web/views/health-strip.ts +0 -92
- package/src/web/views/layout.ts +0 -48
- package/src/web/views/results.ts +0 -210
- package/src/web/views/runs-tab.ts +0 -126
- package/src/web/views/suites-tab.ts +0 -181
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Endpoints tab: all spec endpoints with coverage status, warnings, filters.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { CollectionState, EndpointViewState, CoveringStep } 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 renderEndpointsTab(state: CollectionState, filters?: { status?: string; method?: string }): string {
|
|
11
|
-
if (state.totalEndpoints === 0) {
|
|
12
|
-
return `<div class="tab-empty">No OpenAPI spec configured. Register a spec with <code>setup_api</code> to see endpoints.</div>`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
let filtered = state.endpoints;
|
|
16
|
-
if (filters?.status) {
|
|
17
|
-
filtered = filtered.filter(ep => {
|
|
18
|
-
if (filters.status === "passing") return ep.runStatus === "passing";
|
|
19
|
-
if (filters.status === "failing") return ep.runStatus === "api_error" || ep.runStatus === "test_failed";
|
|
20
|
-
if (filters.status === "no_tests") return ep.runStatus === "no_tests";
|
|
21
|
-
if (filters.status === "not_run") return ep.runStatus === "not_run";
|
|
22
|
-
return true;
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
if (filters?.method) {
|
|
26
|
-
filtered = filtered.filter(ep => ep.method === filters.method);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const collectionId = state.collection.id;
|
|
30
|
-
|
|
31
|
-
// Filter bar
|
|
32
|
-
const counts = {
|
|
33
|
-
all: state.endpoints.length,
|
|
34
|
-
passing: state.endpoints.filter(e => e.runStatus === "passing").length,
|
|
35
|
-
failing: state.endpoints.filter(e => e.runStatus === "api_error" || e.runStatus === "test_failed").length,
|
|
36
|
-
no_tests: state.endpoints.filter(e => e.runStatus === "no_tests").length,
|
|
37
|
-
not_run: state.endpoints.filter(e => e.runStatus === "not_run").length,
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const filterBar = `
|
|
41
|
-
<div class="filter-bar">
|
|
42
|
-
<button class="filter-chip ${!filters?.status ? 'filter-active' : ''}"
|
|
43
|
-
hx-get="/panels/endpoints?collection_id=${collectionId}"
|
|
44
|
-
hx-target="#tab-content" hx-swap="innerHTML">All (${counts.all})</button>
|
|
45
|
-
<button class="filter-chip filter-chip-pass ${filters?.status === 'passing' ? 'filter-active' : ''}"
|
|
46
|
-
hx-get="/panels/endpoints?collection_id=${collectionId}&status=passing"
|
|
47
|
-
hx-target="#tab-content" hx-swap="innerHTML">Passing (${counts.passing})</button>
|
|
48
|
-
<button class="filter-chip filter-chip-fail ${filters?.status === 'failing' ? 'filter-active' : ''}"
|
|
49
|
-
hx-get="/panels/endpoints?collection_id=${collectionId}&status=failing"
|
|
50
|
-
hx-target="#tab-content" hx-swap="innerHTML">Failing (${counts.failing})</button>
|
|
51
|
-
<button class="filter-chip filter-chip-notrun ${filters?.status === 'not_run' ? 'filter-active' : ''}"
|
|
52
|
-
hx-get="/panels/endpoints?collection_id=${collectionId}&status=not_run"
|
|
53
|
-
hx-target="#tab-content" hx-swap="innerHTML">Not Run (${counts.not_run})</button>
|
|
54
|
-
<button class="filter-chip filter-chip-notest ${filters?.status === 'no_tests' ? 'filter-active' : ''}"
|
|
55
|
-
hx-get="/panels/endpoints?collection_id=${collectionId}&status=no_tests"
|
|
56
|
-
hx-target="#tab-content" hx-swap="innerHTML">No Tests (${counts.no_tests})</button>
|
|
57
|
-
</div>`;
|
|
58
|
-
|
|
59
|
-
const rows = filtered.map((ep, i) => renderEndpointRow(ep, collectionId, i)).join("");
|
|
60
|
-
|
|
61
|
-
return `${filterBar}<div class="endpoint-list">${rows}</div>`;
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
function renderEndpointRow(ep: EndpointViewState, collectionId: number, index: number): string {
|
|
65
|
-
const statusDot = getStatusDot(ep.runStatus);
|
|
66
|
-
const warningBadges = renderWarningBadges(ep.warnings);
|
|
67
|
-
const detailId = `ep-detail-${index}`;
|
|
68
|
-
|
|
69
|
-
return `
|
|
70
|
-
<div class="endpoint-row" onclick="var d=document.getElementById('${detailId}');d.style.display=d.style.display==='none'?'grid':'none'">
|
|
71
|
-
<span class="endpoint-status">${statusDot}</span>
|
|
72
|
-
<span class="endpoint-method">${methodBadge(ep.method)}</span>
|
|
73
|
-
<span class="endpoint-path">${escapeHtml(ep.path)}</span>
|
|
74
|
-
<span class="endpoint-badges">${warningBadges}${ep.summary ? `<span class="endpoint-summary">${escapeHtml(ep.summary)}</span>` : ""}</span>
|
|
75
|
-
</div>
|
|
76
|
-
<div class="endpoint-detail" id="${detailId}" style="display:none">
|
|
77
|
-
${renderEndpointDetail(ep)}
|
|
78
|
-
</div>`;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
function getStatusDot(status: EndpointViewState["runStatus"]): string {
|
|
82
|
-
switch (status) {
|
|
83
|
-
case "passing": return '<span class="status-dot status-pass" title="Tests passing"></span>';
|
|
84
|
-
case "api_error": return '<span class="status-dot status-fail" title="API error (5xx)"></span>';
|
|
85
|
-
case "test_failed": return '<span class="status-dot status-fail" title="Assertion failed"></span>';
|
|
86
|
-
case "not_run": return '<span class="status-dot status-notrun" title="Tests exist, not run"></span>';
|
|
87
|
-
case "no_tests": return '<span class="status-dot status-notest" title="No tests"></span>';
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function renderWarningBadges(warnings: string[]): string {
|
|
92
|
-
return warnings.map(w => {
|
|
93
|
-
if (w === "deprecated") return '<span class="warning-badge warning-deprecated">DEPRECATED</span>';
|
|
94
|
-
if (w === "no_response_schema") return '<span class="warning-badge warning-schema">NO SCHEMA</span>';
|
|
95
|
-
if (w === "no_responses_defined") return '<span class="warning-badge warning-schema">NO RESPONSES</span>';
|
|
96
|
-
if (w.startsWith("required_params_no_examples")) return "";
|
|
97
|
-
return `<span class="warning-badge">${escapeHtml(w)}</span>`;
|
|
98
|
-
}).join(" ");
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
function renderEndpointDetail(ep: EndpointViewState): string {
|
|
102
|
-
if (!ep.hasCoverage) {
|
|
103
|
-
return `<div class="ep-detail-section"><em style="color:var(--text-dim);">No test files cover this endpoint</em></div>`;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// If we have run results, show covering steps
|
|
107
|
-
if (ep.coveringSteps.length > 0) {
|
|
108
|
-
const steps = ep.coveringSteps.map(step => {
|
|
109
|
-
const icon = step.status === "pass"
|
|
110
|
-
? '<span class="step-icon pass">✓</span>'
|
|
111
|
-
: step.status === "fail" || step.status === "error"
|
|
112
|
-
? '<span class="step-icon fail">✗</span>'
|
|
113
|
-
: step.status === "skip"
|
|
114
|
-
? '<span class="step-icon skip">▬</span>'
|
|
115
|
-
: '<span class="step-icon" style="color:var(--text-dim);">○</span>';
|
|
116
|
-
|
|
117
|
-
const statusBadge = step.responseStatus && step.responseStatus >= 400 && (step.status === "fail" || step.status === "error")
|
|
118
|
-
? ` <span class="warning-badge server-error" style="font-size:0.6rem;">${step.responseStatus} ${httpStatusText(step.responseStatus)}</span>`
|
|
119
|
-
: "";
|
|
120
|
-
|
|
121
|
-
const duration = step.durationMs != null ? `<span class="step-duration">${step.durationMs}ms</span>` : "";
|
|
122
|
-
|
|
123
|
-
let assertionsHtml = "";
|
|
124
|
-
if (step.assertions && step.assertions.length > 0) {
|
|
125
|
-
assertionsHtml = `<div style="padding-left:1.5rem;margin-top:0.25rem;">` +
|
|
126
|
-
step.assertions.map(a => {
|
|
127
|
-
const aIcon = a.passed
|
|
128
|
-
? '<span class="assertion-icon pass">✓</span>'
|
|
129
|
-
: '<span class="assertion-icon fail">✗</span>';
|
|
130
|
-
const actual = !a.passed && a.actual !== undefined
|
|
131
|
-
? ` <span class="assertion-actual">(got ${JSON.stringify(a.actual)})</span>` : "";
|
|
132
|
-
return `<div class="assertion-row">${aIcon} <span class="assertion-field">${escapeHtml(a.field)}:</span> <span class="assertion-rule">${escapeHtml(a.rule)}</span>${actual}</div>`;
|
|
133
|
-
}).join("") + `</div>`;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
let hintHtml = "";
|
|
137
|
-
if (step.hint) {
|
|
138
|
-
hintHtml = `<div class="failure-hint"><span>⚠</span> ${escapeHtml(step.hint)}</div>`;
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return `<div class="covering-suite">
|
|
142
|
-
${icon}
|
|
143
|
-
<a class="suite-ref suite-link" href="#" data-suite="${escapeHtml(step.suiteName)}"
|
|
144
|
-
onclick="event.stopPropagation();switchToSuite(this.dataset.suite)">${escapeHtml(step.file)}</a>
|
|
145
|
-
<span class="dim" style="font-size:0.75rem;">→ "${escapeHtml(step.stepName)}"</span>
|
|
146
|
-
<span style="margin-left:auto;display:flex;align-items:center;gap:0.5rem;">${statusBadge}${duration}</span>
|
|
147
|
-
</div>${assertionsHtml}${hintHtml}`;
|
|
148
|
-
}).join("");
|
|
149
|
-
return steps;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
// Fallback: just file names
|
|
153
|
-
const files = ep.coveringFiles.map(f => {
|
|
154
|
-
const fileName = basename(f);
|
|
155
|
-
const suiteName = fileName.replace(/\.(ya?ml)$/i, "");
|
|
156
|
-
return `<div class="covering-suite">
|
|
157
|
-
<span class="step-icon" style="color:var(--text-dim);">○</span>
|
|
158
|
-
<a class="suite-ref suite-link" href="#" data-suite="${escapeHtml(suiteName)}"
|
|
159
|
-
onclick="event.stopPropagation();switchToSuite(this.dataset.suite)">${escapeHtml(fileName)}</a>
|
|
160
|
-
<span class="dim" style="font-size:0.75rem;">not run</span>
|
|
161
|
-
</div>`;
|
|
162
|
-
}).join("");
|
|
163
|
-
return files;
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function httpStatusText(code: number): string {
|
|
167
|
-
const map: Record<number, string> = {
|
|
168
|
-
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found",
|
|
169
|
-
405: "Method Not Allowed", 409: "Conflict", 422: "Unprocessable Entity",
|
|
170
|
-
429: "Too Many Requests", 500: "Internal Server Error", 502: "Bad Gateway",
|
|
171
|
-
503: "Service Unavailable", 504: "Gateway Timeout",
|
|
172
|
-
};
|
|
173
|
-
return map[code] ?? "";
|
|
174
|
-
}
|
|
@@ -1,402 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Explorer tab: Swagger-like interactive API explorer.
|
|
3
|
-
* Renders endpoint forms, executes requests via server proxy, displays responses.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import type { OpenAPIV3 } from "openapi-types";
|
|
7
|
-
import type { CollectionRecord } from "../../db/queries.ts";
|
|
8
|
-
import type { EndpointInfo } from "../../core/generator/types.ts";
|
|
9
|
-
import { escapeHtml } from "./layout.ts";
|
|
10
|
-
import { methodBadge } from "./results.ts";
|
|
11
|
-
|
|
12
|
-
// ── Public API ──
|
|
13
|
-
|
|
14
|
-
export async function renderExplorerTab(collection: CollectionRecord): Promise<string> {
|
|
15
|
-
if (!collection.openapi_spec) {
|
|
16
|
-
return `<div class="tab-empty">No OpenAPI spec configured. Register a spec with <code>setup_api</code> to see the explorer.</div>`;
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
let doc: OpenAPIV3.Document;
|
|
20
|
-
let endpoints: EndpointInfo[];
|
|
21
|
-
try {
|
|
22
|
-
const { readOpenApiSpec, extractEndpoints } = await import("../../core/generator/openapi-reader.ts");
|
|
23
|
-
doc = await readOpenApiSpec(collection.openapi_spec);
|
|
24
|
-
endpoints = extractEndpoints(doc);
|
|
25
|
-
} catch (err) {
|
|
26
|
-
return `<div class="tab-empty">Failed to load OpenAPI spec: ${escapeHtml((err as Error).message)}</div>`;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (endpoints.length === 0) {
|
|
30
|
-
return `<div class="tab-empty">No endpoints found in the OpenAPI spec.</div>`;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// Resolve base URLs from spec servers + env
|
|
34
|
-
const baseUrls: string[] = [];
|
|
35
|
-
if (doc.servers && doc.servers.length > 0) {
|
|
36
|
-
for (const s of doc.servers) {
|
|
37
|
-
if (s.url) baseUrls.push(s.url);
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
let envBaseUrl: string | undefined;
|
|
42
|
-
try {
|
|
43
|
-
const { loadEnvironment } = await import("../../core/parser/variables.ts");
|
|
44
|
-
const env = await loadEnvironment(undefined, collection.base_dir ?? collection.test_path);
|
|
45
|
-
if (env.base_url) {
|
|
46
|
-
envBaseUrl = env.base_url;
|
|
47
|
-
if (!baseUrls.includes(envBaseUrl)) baseUrls.unshift(envBaseUrl);
|
|
48
|
-
}
|
|
49
|
-
} catch { /* no env file */ }
|
|
50
|
-
|
|
51
|
-
// Base URL bar
|
|
52
|
-
const baseUrlBar = renderBaseUrlBar(baseUrls, envBaseUrl);
|
|
53
|
-
|
|
54
|
-
// Group endpoints by first tag
|
|
55
|
-
const groups = new Map<string, EndpointInfo[]>();
|
|
56
|
-
for (const ep of endpoints) {
|
|
57
|
-
const tag = ep.tags.length > 0 ? ep.tags[0]! : "Other";
|
|
58
|
-
const list = groups.get(tag) ?? [];
|
|
59
|
-
list.push(ep);
|
|
60
|
-
groups.set(tag, list);
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
let idx = 0;
|
|
64
|
-
const groupsHtml = [...groups.entries()].map(([tag, eps]) => {
|
|
65
|
-
const rows = eps.map(ep => {
|
|
66
|
-
const html = renderEndpointEntry(ep, idx, collection.id);
|
|
67
|
-
idx++;
|
|
68
|
-
return html;
|
|
69
|
-
}).join("");
|
|
70
|
-
return `<details class="explorer-group" open>
|
|
71
|
-
<summary class="explorer-group-title">${escapeHtml(tag)} <span class="tab-count">${eps.length}</span></summary>
|
|
72
|
-
${rows}
|
|
73
|
-
</details>`;
|
|
74
|
-
}).join("");
|
|
75
|
-
|
|
76
|
-
const script = `<script>
|
|
77
|
-
function explorerToggle(id) {
|
|
78
|
-
var el = document.getElementById(id);
|
|
79
|
-
if (!el) return;
|
|
80
|
-
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
|
81
|
-
}
|
|
82
|
-
function explorerAddHeader(btn) {
|
|
83
|
-
var container = btn.previousElementSibling;
|
|
84
|
-
var count = container.querySelectorAll('.explorer-header-pair').length;
|
|
85
|
-
var row = document.createElement('div');
|
|
86
|
-
row.className = 'explorer-header-pair';
|
|
87
|
-
row.innerHTML = '<input type="text" name="custom_header_key_' + count + '" placeholder="Header name" class="explorer-input explorer-input-sm">' +
|
|
88
|
-
'<input type="text" name="custom_header_value_' + count + '" placeholder="Value" class="explorer-input explorer-input-sm">' +
|
|
89
|
-
'<button type="button" class="explorer-remove-btn" onclick="this.parentElement.remove()">x</button>';
|
|
90
|
-
container.appendChild(row);
|
|
91
|
-
}
|
|
92
|
-
function explorerGetBaseUrl() {
|
|
93
|
-
var sel = document.getElementById('explorer-base-url-select');
|
|
94
|
-
var custom = document.getElementById('explorer-base-url-custom');
|
|
95
|
-
if (sel && sel.value === '__custom__') return custom ? custom.value : '';
|
|
96
|
-
return sel ? sel.value : (custom ? custom.value : '');
|
|
97
|
-
}
|
|
98
|
-
function explorerBeforeRequest(formId) {
|
|
99
|
-
var form = document.getElementById(formId);
|
|
100
|
-
if (!form) return true;
|
|
101
|
-
var input = form.querySelector('input[name="base_url"]');
|
|
102
|
-
if (input) input.value = explorerGetBaseUrl();
|
|
103
|
-
return true;
|
|
104
|
-
}
|
|
105
|
-
document.addEventListener('change', function(e) {
|
|
106
|
-
if (e.target && e.target.id === 'explorer-base-url-select') {
|
|
107
|
-
var custom = document.getElementById('explorer-base-url-custom');
|
|
108
|
-
if (custom) custom.style.display = e.target.value === '__custom__' ? 'block' : 'none';
|
|
109
|
-
}
|
|
110
|
-
});
|
|
111
|
-
</script>`;
|
|
112
|
-
|
|
113
|
-
return `${baseUrlBar}<div class="explorer-list">${groupsHtml}</div>${script}`;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export function renderProxyResponse(status: number, headers: Record<string, string>, body: string, elapsedMs: number): string {
|
|
117
|
-
const statusClass = status < 300 ? "status-2xx" : status < 400 ? "status-3xx" : status < 500 ? "status-4xx" : "status-5xx";
|
|
118
|
-
const statusText = httpStatusText(status);
|
|
119
|
-
const size = body.length < 1024 ? `${body.length} B` : `${(body.length / 1024).toFixed(1)} KB`;
|
|
120
|
-
|
|
121
|
-
// Try to format JSON
|
|
122
|
-
let formattedBody: string;
|
|
123
|
-
const contentType = headers["content-type"] ?? "";
|
|
124
|
-
if (contentType.includes("json") || body.trimStart().startsWith("{") || body.trimStart().startsWith("[")) {
|
|
125
|
-
try {
|
|
126
|
-
const pretty = JSON.stringify(JSON.parse(body), null, 2);
|
|
127
|
-
formattedBody = highlightJson(pretty);
|
|
128
|
-
} catch {
|
|
129
|
-
formattedBody = escapeHtml(body);
|
|
130
|
-
}
|
|
131
|
-
} else {
|
|
132
|
-
formattedBody = escapeHtml(body);
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const headerEntries = Object.entries(headers);
|
|
136
|
-
const headersHtml = headerEntries.length > 0
|
|
137
|
-
? `<details class="response-headers">
|
|
138
|
-
<summary>Headers (${headerEntries.length})</summary>
|
|
139
|
-
<pre class="response-headers-pre">${headerEntries.map(([k, v]) => `<span class="json-key">${escapeHtml(k)}</span>: ${escapeHtml(v)}`).join("\n")}</pre>
|
|
140
|
-
</details>`
|
|
141
|
-
: "";
|
|
142
|
-
|
|
143
|
-
return `<div class="explorer-response">
|
|
144
|
-
<div class="response-meta">
|
|
145
|
-
<span class="response-status ${statusClass}">${status} ${escapeHtml(statusText)}</span>
|
|
146
|
-
<span class="response-time">${elapsedMs}ms</span>
|
|
147
|
-
<span class="response-size">${size}</span>
|
|
148
|
-
</div>
|
|
149
|
-
${headersHtml}
|
|
150
|
-
<div class="response-body"><pre><code>${formattedBody}</code></pre></div>
|
|
151
|
-
</div>`;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
export function renderProxyError(message: string, elapsedMs: number): string {
|
|
155
|
-
return `<div class="explorer-response explorer-response-error">
|
|
156
|
-
<div class="response-meta">
|
|
157
|
-
<span class="response-status status-5xx">Error</span>
|
|
158
|
-
<span class="response-time">${elapsedMs}ms</span>
|
|
159
|
-
</div>
|
|
160
|
-
<div class="response-error-msg">${escapeHtml(message)}</div>
|
|
161
|
-
</div>`;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// ── Private helpers ──
|
|
165
|
-
|
|
166
|
-
function renderBaseUrlBar(baseUrls: string[], envBaseUrl?: string): string {
|
|
167
|
-
if (baseUrls.length === 0) {
|
|
168
|
-
return `<div class="explorer-base-url">
|
|
169
|
-
<label class="explorer-label">Base URL</label>
|
|
170
|
-
<input type="text" id="explorer-base-url-custom" class="explorer-input" placeholder="https://api.example.com" value="">
|
|
171
|
-
</div>`;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
const options = baseUrls.map(url => {
|
|
175
|
-
const label = url === envBaseUrl ? `${url} (env)` : url;
|
|
176
|
-
return `<option value="${escapeHtml(url)}">${escapeHtml(label)}</option>`;
|
|
177
|
-
}).join("");
|
|
178
|
-
|
|
179
|
-
return `<div class="explorer-base-url">
|
|
180
|
-
<label class="explorer-label">Base URL</label>
|
|
181
|
-
<select id="explorer-base-url-select" class="explorer-input">
|
|
182
|
-
${options}
|
|
183
|
-
<option value="__custom__">Custom...</option>
|
|
184
|
-
</select>
|
|
185
|
-
<input type="text" id="explorer-base-url-custom" class="explorer-input" placeholder="https://api.example.com" style="display:none;">
|
|
186
|
-
</div>`;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
function renderEndpointEntry(ep: EndpointInfo, index: number, collectionId: number): string {
|
|
190
|
-
const formId = `explorer-form-${index}`;
|
|
191
|
-
const detailId = `explorer-detail-${index}`;
|
|
192
|
-
const responseId = `explorer-response-${index}`;
|
|
193
|
-
const spinnerId = `explorer-spinner-${index}`;
|
|
194
|
-
const deprecated = ep.deprecated ? ' <span class="warning-badge warning-deprecated">DEPRECATED</span>' : "";
|
|
195
|
-
const securityHint = ep.security.length > 0
|
|
196
|
-
? ` <span class="explorer-auth-hint" title="Requires: ${escapeHtml(ep.security.join(", "))}">Auth</span>`
|
|
197
|
-
: "";
|
|
198
|
-
|
|
199
|
-
// Separate parameters by location
|
|
200
|
-
const pathParams = ep.parameters.filter(p => p.in === "path");
|
|
201
|
-
const queryParams = ep.parameters.filter(p => p.in === "query");
|
|
202
|
-
const headerParams = ep.parameters.filter(p => p.in === "header");
|
|
203
|
-
|
|
204
|
-
// Request body
|
|
205
|
-
const hasBody = ["POST", "PUT", "PATCH"].includes(ep.method);
|
|
206
|
-
const exampleBody = hasBody && ep.requestBodySchema
|
|
207
|
-
? JSON.stringify(generateExample(ep.requestBodySchema), null, 2)
|
|
208
|
-
: "";
|
|
209
|
-
const bodyContentType = ep.requestBodyContentType ?? "application/json";
|
|
210
|
-
|
|
211
|
-
const paramsHtml = renderParamsSection(pathParams, queryParams, headerParams);
|
|
212
|
-
const bodyHtml = hasBody ? renderBodySection(exampleBody, bodyContentType) : "";
|
|
213
|
-
const headersHtml = renderCustomHeadersSection();
|
|
214
|
-
|
|
215
|
-
return `
|
|
216
|
-
<div class="explorer-endpoint" onclick="explorerToggle('${detailId}')">
|
|
217
|
-
${methodBadge(ep.method)}
|
|
218
|
-
<span class="explorer-endpoint-path">${escapeHtml(ep.path)}</span>
|
|
219
|
-
${deprecated}${securityHint}
|
|
220
|
-
${ep.summary ? `<span class="explorer-endpoint-summary">${escapeHtml(ep.summary)}</span>` : ""}
|
|
221
|
-
</div>
|
|
222
|
-
<div class="explorer-detail" id="${detailId}" style="display:none" onclick="event.stopPropagation()">
|
|
223
|
-
<form id="${formId}" hx-post="/api/proxy" hx-target="#${responseId}" hx-swap="innerHTML"
|
|
224
|
-
hx-indicator="#${spinnerId}"
|
|
225
|
-
hx-vals='js:{"base_url": explorerGetBaseUrl()}'
|
|
226
|
-
hx-disabled-elt="find .explorer-send-btn">
|
|
227
|
-
<input type="hidden" name="method" value="${ep.method}">
|
|
228
|
-
<input type="hidden" name="path" value="${escapeHtml(ep.path)}">
|
|
229
|
-
<input type="hidden" name="collection_id" value="${collectionId}">
|
|
230
|
-
${paramsHtml}
|
|
231
|
-
${bodyHtml}
|
|
232
|
-
${headersHtml}
|
|
233
|
-
<div class="explorer-actions">
|
|
234
|
-
<button type="submit" class="btn explorer-send-btn">Send</button>
|
|
235
|
-
<span id="${spinnerId}" class="htmx-indicator explorer-spinner">Sending...</span>
|
|
236
|
-
</div>
|
|
237
|
-
</form>
|
|
238
|
-
<div id="${responseId}" class="explorer-response-container"></div>
|
|
239
|
-
</div>`;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function renderParamsSection(
|
|
243
|
-
pathParams: OpenAPIV3.ParameterObject[],
|
|
244
|
-
queryParams: OpenAPIV3.ParameterObject[],
|
|
245
|
-
headerParams: OpenAPIV3.ParameterObject[],
|
|
246
|
-
): string {
|
|
247
|
-
const all = [
|
|
248
|
-
...pathParams.map(p => ({ ...p, prefix: "param_path_" })),
|
|
249
|
-
...queryParams.map(p => ({ ...p, prefix: "param_query_" })),
|
|
250
|
-
...headerParams.map(p => ({ ...p, prefix: "param_header_" })),
|
|
251
|
-
];
|
|
252
|
-
|
|
253
|
-
if (all.length === 0) return "";
|
|
254
|
-
|
|
255
|
-
const rows = all.map(p => {
|
|
256
|
-
const schema = p.schema as OpenAPIV3.SchemaObject | undefined;
|
|
257
|
-
const type = schema?.type ?? "string";
|
|
258
|
-
const required = p.required ? '<span class="explorer-required">*</span>' : "";
|
|
259
|
-
const locationLabel = p.in === "path" ? "path" : p.in === "query" ? "query" : "header";
|
|
260
|
-
const placeholder = schema?.example != null ? String(schema.example) : (schema?.enum ? schema.enum[0] : "");
|
|
261
|
-
const defaultVal = schema?.default != null ? String(schema.default) : "";
|
|
262
|
-
const description = p.description ? ` title="${escapeHtml(p.description)}"` : "";
|
|
263
|
-
|
|
264
|
-
return `<div class="explorer-param-row"${description}>
|
|
265
|
-
<span class="explorer-param-name">${escapeHtml(p.name)}${required}</span>
|
|
266
|
-
<span class="explorer-param-location">${locationLabel}</span>
|
|
267
|
-
<span class="explorer-param-type">${escapeHtml(type)}${schema?.format ? ` (${escapeHtml(schema.format)})` : ""}</span>
|
|
268
|
-
<input type="text" name="${p.prefix}${escapeHtml(p.name)}" class="explorer-input"
|
|
269
|
-
placeholder="${escapeHtml(placeholder)}" value="${escapeHtml(defaultVal)}"
|
|
270
|
-
${p.required ? "required" : ""}>
|
|
271
|
-
</div>`;
|
|
272
|
-
}).join("");
|
|
273
|
-
|
|
274
|
-
return `<div class="explorer-section">
|
|
275
|
-
<div class="explorer-section-title">Parameters</div>
|
|
276
|
-
${rows}
|
|
277
|
-
</div>`;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
function renderBodySection(exampleBody: string, contentType: string): string {
|
|
281
|
-
return `<div class="explorer-section">
|
|
282
|
-
<div class="explorer-section-title">Request Body
|
|
283
|
-
<select name="content_type" class="explorer-input explorer-input-sm explorer-content-type">
|
|
284
|
-
<option value="application/json"${contentType === "application/json" ? " selected" : ""}>application/json</option>
|
|
285
|
-
<option value="application/x-www-form-urlencoded"${contentType === "application/x-www-form-urlencoded" ? " selected" : ""}>form-urlencoded</option>
|
|
286
|
-
</select>
|
|
287
|
-
</div>
|
|
288
|
-
<textarea name="body" class="explorer-body-editor" rows="8" spellcheck="false">${escapeHtml(exampleBody)}</textarea>
|
|
289
|
-
</div>`;
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
function renderCustomHeadersSection(): string {
|
|
293
|
-
return `<div class="explorer-section">
|
|
294
|
-
<div class="explorer-section-title">Headers</div>
|
|
295
|
-
<div class="explorer-headers-list">
|
|
296
|
-
<div class="explorer-header-pair">
|
|
297
|
-
<input type="text" name="custom_header_key_0" placeholder="Header name" class="explorer-input explorer-input-sm">
|
|
298
|
-
<input type="text" name="custom_header_value_0" placeholder="Value" class="explorer-input explorer-input-sm">
|
|
299
|
-
<button type="button" class="explorer-remove-btn" onclick="this.parentElement.remove()">x</button>
|
|
300
|
-
</div>
|
|
301
|
-
</div>
|
|
302
|
-
<button type="button" class="explorer-add-header-btn" onclick="explorerAddHeader(this)">+ Add header</button>
|
|
303
|
-
</div>`;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
function generateExample(schema: OpenAPIV3.SchemaObject, depth = 0): unknown {
|
|
307
|
-
if (depth > 5) return {};
|
|
308
|
-
|
|
309
|
-
if (schema.example !== undefined) return schema.example;
|
|
310
|
-
|
|
311
|
-
if (schema.enum && schema.enum.length > 0) return schema.enum[0];
|
|
312
|
-
|
|
313
|
-
if (schema.allOf) {
|
|
314
|
-
const merged: Record<string, unknown> = {};
|
|
315
|
-
for (const sub of schema.allOf) {
|
|
316
|
-
const s = sub as OpenAPIV3.SchemaObject;
|
|
317
|
-
const val = generateExample(s, depth + 1);
|
|
318
|
-
if (typeof val === "object" && val !== null && !Array.isArray(val)) {
|
|
319
|
-
Object.assign(merged, val);
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
return merged;
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (schema.oneOf && schema.oneOf.length > 0) {
|
|
326
|
-
return generateExample(schema.oneOf[0] as OpenAPIV3.SchemaObject, depth + 1);
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
if (schema.anyOf && schema.anyOf.length > 0) {
|
|
330
|
-
return generateExample(schema.anyOf[0] as OpenAPIV3.SchemaObject, depth + 1);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
switch (schema.type) {
|
|
334
|
-
case "string":
|
|
335
|
-
if (schema.format === "email") return "user@example.com";
|
|
336
|
-
if (schema.format === "date") return "2026-01-01";
|
|
337
|
-
if (schema.format === "date-time") return "2026-01-01T00:00:00Z";
|
|
338
|
-
if (schema.format === "uri" || schema.format === "url") return "https://example.com";
|
|
339
|
-
if (schema.format === "uuid") return "550e8400-e29b-41d4-a716-446655440000";
|
|
340
|
-
return "string";
|
|
341
|
-
case "integer":
|
|
342
|
-
return schema.minimum != null ? schema.minimum : 0;
|
|
343
|
-
case "number":
|
|
344
|
-
return schema.minimum != null ? schema.minimum : 0.0;
|
|
345
|
-
case "boolean":
|
|
346
|
-
return true;
|
|
347
|
-
case "array": {
|
|
348
|
-
const items = schema.items as OpenAPIV3.SchemaObject | undefined;
|
|
349
|
-
if (items) return [generateExample(items, depth + 1)];
|
|
350
|
-
return [];
|
|
351
|
-
}
|
|
352
|
-
case "object":
|
|
353
|
-
default:
|
|
354
|
-
if (schema.properties) {
|
|
355
|
-
const result: Record<string, unknown> = {};
|
|
356
|
-
for (const [key, propObj] of Object.entries(schema.properties)) {
|
|
357
|
-
result[key] = generateExample(propObj as OpenAPIV3.SchemaObject, depth + 1);
|
|
358
|
-
}
|
|
359
|
-
return result;
|
|
360
|
-
}
|
|
361
|
-
return {};
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
|
|
365
|
-
function highlightJson(json: string): string {
|
|
366
|
-
// Split into tokens and non-token text, escape everything properly
|
|
367
|
-
const tokenRe = /("(?:\\.|[^"\\])*")\s*:|("(?:\\.|[^"\\])*")|([-]?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?)|\b(true|false)\b|\b(null)\b/g;
|
|
368
|
-
let result = "";
|
|
369
|
-
let lastIndex = 0;
|
|
370
|
-
let m: RegExpExecArray | null;
|
|
371
|
-
|
|
372
|
-
while ((m = tokenRe.exec(json)) !== null) {
|
|
373
|
-
// Escape text between tokens (brackets, commas, whitespace, colons)
|
|
374
|
-
if (m.index > lastIndex) {
|
|
375
|
-
result += escapeHtml(json.slice(lastIndex, m.index));
|
|
376
|
-
}
|
|
377
|
-
const [, key, str, num, bool, nil] = m;
|
|
378
|
-
if (key) result += `<span class="json-key">${escapeHtml(key)}</span>:`;
|
|
379
|
-
else if (str) result += `<span class="json-string">${escapeHtml(str)}</span>`;
|
|
380
|
-
else if (num) result += `<span class="json-number">${num}</span>`;
|
|
381
|
-
else if (bool) result += `<span class="json-boolean">${bool}</span>`;
|
|
382
|
-
else if (nil) result += `<span class="json-null">null</span>`;
|
|
383
|
-
lastIndex = tokenRe.lastIndex;
|
|
384
|
-
}
|
|
385
|
-
// Remaining text after last token
|
|
386
|
-
if (lastIndex < json.length) {
|
|
387
|
-
result += escapeHtml(json.slice(lastIndex));
|
|
388
|
-
}
|
|
389
|
-
return result;
|
|
390
|
-
}
|
|
391
|
-
|
|
392
|
-
function httpStatusText(code: number): string {
|
|
393
|
-
const map: Record<number, string> = {
|
|
394
|
-
200: "OK", 201: "Created", 204: "No Content",
|
|
395
|
-
301: "Moved Permanently", 302: "Found", 304: "Not Modified",
|
|
396
|
-
400: "Bad Request", 401: "Unauthorized", 403: "Forbidden", 404: "Not Found",
|
|
397
|
-
405: "Method Not Allowed", 409: "Conflict", 422: "Unprocessable Entity",
|
|
398
|
-
429: "Too Many Requests",
|
|
399
|
-
500: "Internal Server Error", 502: "Bad Gateway", 503: "Service Unavailable",
|
|
400
|
-
};
|
|
401
|
-
return map[code] ?? "";
|
|
402
|
-
}
|
|
@@ -1,92 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Health strip: coverage donut, run stats, env alert banner.
|
|
3
|
-
*/
|
|
4
|
-
|
|
5
|
-
import type { CollectionState } from "../data/collection-state.ts";
|
|
6
|
-
import { formatDuration } from "../../core/reporter/console.ts";
|
|
7
|
-
|
|
8
|
-
export function renderHealthStrip(state: CollectionState): string {
|
|
9
|
-
const { coveragePct, coveredCount, totalEndpoints, runPassed, runFailed, runSkipped, runTotal, runDurationMs, envAlert, latestRun } = state;
|
|
10
|
-
|
|
11
|
-
const donut = renderCoverageDonut(coveragePct, coveredCount, totalEndpoints);
|
|
12
|
-
|
|
13
|
-
const hasRun = latestRun !== null;
|
|
14
|
-
const duration = runDurationMs != null ? formatDuration(runDurationMs) : "-";
|
|
15
|
-
|
|
16
|
-
const statsHtml = hasRun
|
|
17
|
-
? `<div class="health-stats">
|
|
18
|
-
<div class="stat-block stat-pass"><span class="stat-value">${runPassed}</span><span class="stat-label">passed</span></div>
|
|
19
|
-
<div class="stat-block stat-fail"><span class="stat-value">${runFailed}</span><span class="stat-label">failed</span></div>
|
|
20
|
-
<div class="stat-block stat-skip"><span class="stat-value">${runSkipped}</span><span class="stat-label">skipped</span></div>
|
|
21
|
-
<div class="stat-block"><span class="stat-value">${duration}</span><span class="stat-label">duration</span></div>
|
|
22
|
-
</div>`
|
|
23
|
-
: `<div class="health-stats">
|
|
24
|
-
<div class="stat-block"><span class="stat-value">-</span><span class="stat-label">No runs yet</span></div>
|
|
25
|
-
</div>`;
|
|
26
|
-
|
|
27
|
-
// Mini progress bar
|
|
28
|
-
const progressHtml = hasRun && runTotal > 0
|
|
29
|
-
? `<div class="health-progress">
|
|
30
|
-
<div class="progress-bar" style="height:6px;">
|
|
31
|
-
<div class="progress-pass" style="width:${(runPassed / runTotal * 100).toFixed(1)}%"></div>
|
|
32
|
-
<div class="progress-fail" style="width:${(runFailed / runTotal * 100).toFixed(1)}%"></div>
|
|
33
|
-
<div class="progress-skip" style="width:${(runSkipped / runTotal * 100).toFixed(1)}%"></div>
|
|
34
|
-
</div>
|
|
35
|
-
<span class="health-progress-label">${runPassed}/${runTotal} steps passed</span>
|
|
36
|
-
</div>`
|
|
37
|
-
: "";
|
|
38
|
-
|
|
39
|
-
const envAlertHtml = envAlert ? renderEnvAlert(envAlert) : "";
|
|
40
|
-
|
|
41
|
-
return `
|
|
42
|
-
<div class="health-strip">
|
|
43
|
-
<div class="health-donut-zone">
|
|
44
|
-
${donut}
|
|
45
|
-
<div class="coverage-label">
|
|
46
|
-
<span class="label-title">Coverage</span>
|
|
47
|
-
<span class="label-value">${coveredCount} / ${totalEndpoints} endpoints</span>
|
|
48
|
-
</div>
|
|
49
|
-
</div>
|
|
50
|
-
<div class="health-info-zone">
|
|
51
|
-
${statsHtml}
|
|
52
|
-
${progressHtml}
|
|
53
|
-
</div>
|
|
54
|
-
${envAlertHtml}
|
|
55
|
-
</div>`;
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export function renderCoverageDonut(pct: number, covered: number, total: number): string {
|
|
59
|
-
// SVG donut chart
|
|
60
|
-
const size = 80;
|
|
61
|
-
const stroke = 8;
|
|
62
|
-
const radius = (size - stroke) / 2;
|
|
63
|
-
const circumference = 2 * Math.PI * radius;
|
|
64
|
-
const offset = circumference - (pct / 100) * circumference;
|
|
65
|
-
|
|
66
|
-
const color = pct >= 80 ? "var(--pass)" : pct >= 50 ? "var(--warn, #fbbf24)" : "var(--fail)";
|
|
67
|
-
const trackColor = "var(--border)";
|
|
68
|
-
|
|
69
|
-
return `
|
|
70
|
-
<div class="coverage-donut">
|
|
71
|
-
<svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}">
|
|
72
|
-
<circle cx="${size / 2}" cy="${size / 2}" r="${radius}"
|
|
73
|
-
fill="none" stroke="${trackColor}" stroke-width="${stroke}" />
|
|
74
|
-
<circle cx="${size / 2}" cy="${size / 2}" r="${radius}"
|
|
75
|
-
fill="none" stroke="${color}" stroke-width="${stroke}"
|
|
76
|
-
stroke-dasharray="${circumference}" stroke-dashoffset="${offset}"
|
|
77
|
-
stroke-linecap="round"
|
|
78
|
-
transform="rotate(-90 ${size / 2} ${size / 2})" />
|
|
79
|
-
<text x="${size / 2}" y="${size / 2}" text-anchor="middle" dominant-baseline="central"
|
|
80
|
-
fill="var(--text)" font-size="16" font-weight="700" font-family="inherit">${pct}%</text>
|
|
81
|
-
</svg>
|
|
82
|
-
<div class="donut-label">${covered}/${total} endpoints</div>
|
|
83
|
-
</div>`;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
export function renderEnvAlert(message: string): string {
|
|
87
|
-
return `
|
|
88
|
-
<div class="env-alert">
|
|
89
|
-
<span class="env-alert-icon">⚠</span>
|
|
90
|
-
<span>${message}</span>
|
|
91
|
-
</div>`;
|
|
92
|
-
}
|