@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.
- package/CHANGELOG.md +648 -0
- package/README.md +58 -6
- package/package.json +9 -6
- 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 +43 -0
- package/src/cli/commands/clean.ts +212 -0
- package/src/cli/commands/cleanup.ts +262 -0
- package/src/cli/commands/completions.ts +16 -0
- package/src/cli/commands/coverage.ts +605 -132
- package/src/cli/commands/db.ts +178 -7
- 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 -46
- package/src/cli/commands/init/bootstrap.ts +30 -1
- package/src/cli/commands/{init.ts → init/index.ts} +99 -5
- package/src/cli/commands/init/skills.ts +56 -3
- package/src/cli/commands/init/templates/agents.md +65 -61
- 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 +592 -125
- package/src/cli/commands/init/templates/zond-config.yml +8 -9
- 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 +842 -53
- package/src/cli/commands/session.ts +244 -0
- package/src/cli/commands/use.ts +18 -1
- package/src/cli/index.ts +20 -3
- package/src/cli/json-envelope.ts +112 -3
- package/src/cli/json-schemas.ts +263 -0
- package/src/cli/program.ts +198 -635
- 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 +5 -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 +22 -6
- 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 +151 -11
- 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 +42 -16
- 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 +445 -19
- 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 +37 -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 +103 -13
- package/src/core/generator/suite-generator.ts +419 -111
- 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 +129 -4
- package/src/core/parser/types.ts +19 -1
- package/src/core/parser/variables.ts +0 -0
- package/src/core/parser/yaml-parser.ts +58 -12
- 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 +43 -76
- package/src/core/probe/method-shared.ts +69 -0
- package/src/core/probe/negative-probe.ts +183 -149
- 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 +41 -2
- package/src/core/reporter/index.ts +2 -3
- package/src/core/reporter/json.ts +11 -1
- 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 +58 -1
- 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 +264 -20
- package/src/core/runner/form-encode.ts +51 -0
- package/src/core/runner/http-client.ts +75 -2
- 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 +89 -17
- 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 +415 -16
- 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/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 +178 -50
- package/src/cli/commands/export.ts +0 -144
- package/src/cli/commands/guide.ts +0 -127
- package/src/cli/commands/init/templates/skills/scenarios.md +0 -97
- package/src/cli/commands/probe-methods.ts +0 -108
- package/src/cli/commands/probe-validation.ts +0 -124
- package/src/cli/commands/serve.ts +0 -114
- package/src/cli/commands/sync.ts +0 -268
- package/src/cli/commands/update.ts +0 -189
- package/src/cli/commands/validate.ts +0 -34
- package/src/core/diagnostics/render-md.ts +0 -112
- package/src/core/exporter/postman.ts +0 -963
- package/src/core/generator/guide-builder.ts +0 -253
- package/src/core/meta/types.ts +0 -19
- 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
|
@@ -0,0 +1,270 @@
|
|
|
1
|
+
import type { RunRecord, StoredStepResult } from "../../../db/queries.ts";
|
|
2
|
+
import type { FailureClass } from "../../diagnostics/failure-class.ts";
|
|
3
|
+
import { buildCurl } from "../curl.ts";
|
|
4
|
+
|
|
5
|
+
export interface CaseStudyOptions {
|
|
6
|
+
result: StoredStepResult;
|
|
7
|
+
run: RunRecord;
|
|
8
|
+
/** Pulled from OpenAPI `info.title` if available. */
|
|
9
|
+
specTitle?: string | null;
|
|
10
|
+
specVersion?: string | null;
|
|
11
|
+
zondVersion: string;
|
|
12
|
+
/** TASK-164 (m-9 P8): cap response/request bodies to N bytes. 0 or
|
|
13
|
+
* unset = no cap. The CLI wrapper defaults to 8 KB. */
|
|
14
|
+
bodyCapBytes?: number;
|
|
15
|
+
/** ARV-106/107: short registry slug (`--api <name>`). When set, the
|
|
16
|
+
* case-study renders a `zond request --api <name>` alternative below the
|
|
17
|
+
* curl repro and falls back to it for the "API" field when specTitle is
|
|
18
|
+
* null. */
|
|
19
|
+
apiName?: string | null;
|
|
20
|
+
/** ARV-107: loaded OpenAPI spec. When provided, the renderer auto-extracts
|
|
21
|
+
* the relevant operation block for the "What the spec says" section instead
|
|
22
|
+
* of leaving the `<TODO: paste …>` placeholder. */
|
|
23
|
+
specDoc?: OpenApiDocLike | null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Minimal shape we touch from an OpenAPI spec — keeps this module
|
|
27
|
+
* framework-free. */
|
|
28
|
+
interface OpenApiDocLike {
|
|
29
|
+
paths?: Record<string, Record<string, unknown>> | null;
|
|
30
|
+
components?: { schemas?: Record<string, unknown> } | null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function capBody(content: string | null | undefined, capBytes: number | undefined): string | null {
|
|
34
|
+
if (!content) return content ?? null;
|
|
35
|
+
if (!capBytes || capBytes <= 0 || content.length <= capBytes) return content;
|
|
36
|
+
const dropped = content.length - capBytes;
|
|
37
|
+
return `${content.slice(0, capBytes)}\n[truncated ${dropped} bytes; first ${capBytes} shown; full body in run DB]`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const CLASS_HUMAN: Record<FailureClass, string> = {
|
|
41
|
+
definitely_bug: "definitely_bug",
|
|
42
|
+
likely_bug: "likely_bug",
|
|
43
|
+
quirk: "quirk",
|
|
44
|
+
env_issue: "env_issue",
|
|
45
|
+
cascade: "cascade",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const TODO = (hint: string): string => `<TODO: ${hint}>`;
|
|
49
|
+
|
|
50
|
+
function tryPretty(s: string | null | undefined): string {
|
|
51
|
+
if (!s) return "";
|
|
52
|
+
try {
|
|
53
|
+
return JSON.stringify(JSON.parse(s), null, 2);
|
|
54
|
+
} catch {
|
|
55
|
+
return s;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function extractPath(url: string | null): string {
|
|
60
|
+
if (!url) return TODO("path");
|
|
61
|
+
try {
|
|
62
|
+
return new URL(url).pathname;
|
|
63
|
+
} catch {
|
|
64
|
+
return url;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function shortDescription(result: StoredStepResult): string {
|
|
69
|
+
if (result.failure_class_reason) return result.failure_class_reason;
|
|
70
|
+
if (result.error_message) return result.error_message;
|
|
71
|
+
if (result.response_status != null && result.response_status >= 500) {
|
|
72
|
+
return `unexpected ${result.response_status} from server`;
|
|
73
|
+
}
|
|
74
|
+
if (result.response_status != null && result.response_status >= 400) {
|
|
75
|
+
return `unexpected ${result.response_status} response`;
|
|
76
|
+
}
|
|
77
|
+
return TODO("one-line summary of the failure");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function tldrLine(result: StoredStepResult): string {
|
|
81
|
+
switch (result.failure_class) {
|
|
82
|
+
case "definitely_bug":
|
|
83
|
+
return "Backend bug — endpoint contradicts its own contract.";
|
|
84
|
+
case "likely_bug":
|
|
85
|
+
return "Suspicious behaviour — contract leaves it ambiguous, but the response is hard to defend.";
|
|
86
|
+
case "quirk":
|
|
87
|
+
return "Documented quirk worth flagging in onboarding docs / a spec PR.";
|
|
88
|
+
case "env_issue":
|
|
89
|
+
return "Environment / fixture issue — not an API bug, but blocks the test from running.";
|
|
90
|
+
default:
|
|
91
|
+
return TODO("one-sentence takeaway");
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function whyItMatters(result: StoredStepResult): string {
|
|
96
|
+
switch (result.failure_class) {
|
|
97
|
+
case "definitely_bug":
|
|
98
|
+
case "likely_bug": {
|
|
99
|
+
const reason = result.failure_class_reason ?? TODO("explain why this is a bug — what user-visible impact?");
|
|
100
|
+
const failedAsserts = result.assertions.filter((a) => !a.passed);
|
|
101
|
+
const detail = failedAsserts.length > 0
|
|
102
|
+
? failedAsserts
|
|
103
|
+
.map((a) => `- \`${a.rule}\` at \`${a.field}\`: expected \`${JSON.stringify(a.expected)}\`, got \`${JSON.stringify(a.actual)}\``)
|
|
104
|
+
.join("\n")
|
|
105
|
+
: "";
|
|
106
|
+
return detail ? `${reason}\n\nFailed assertions:\n\n${detail}` : reason;
|
|
107
|
+
}
|
|
108
|
+
case "quirk":
|
|
109
|
+
return `Spec implies one thing, server does another:\n\n- Expected: ${TODO("what the spec promised")}\n- Actual: ${TODO("what the server did")}`;
|
|
110
|
+
case "env_issue":
|
|
111
|
+
return result.failure_class_reason ?? TODO("describe what the test setup needed and didn't get");
|
|
112
|
+
default:
|
|
113
|
+
return TODO("explain why this finding matters to the reader");
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function howZondFoundIt(result: StoredStepResult): string {
|
|
118
|
+
const prov = result.provenance;
|
|
119
|
+
if (!prov || prov.type === "manual") {
|
|
120
|
+
return `Manually authored test case: \`${result.suite_name}\` → \`${result.test_name}\`.`;
|
|
121
|
+
}
|
|
122
|
+
const generator = prov.generator ?? "openapi-generated";
|
|
123
|
+
const lines: string[] = [];
|
|
124
|
+
lines.push(`Generated by \`${generator}\` (${prov.type ?? "unknown"}).`);
|
|
125
|
+
if (prov.endpoint) lines.push(`Targeted endpoint: \`${prov.endpoint}\`.`);
|
|
126
|
+
if (prov.response_branch) lines.push(`Asserted response branch: \`${prov.response_branch}\`.`);
|
|
127
|
+
lines.push(`Suite: \`${result.suite_name}\` → \`${result.test_name}\`.`);
|
|
128
|
+
return lines.join("\n");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function matchSpecOperation(
|
|
132
|
+
spec: OpenApiDocLike,
|
|
133
|
+
method: string,
|
|
134
|
+
concretePath: string,
|
|
135
|
+
): { template: string; operation: Record<string, unknown> } | null {
|
|
136
|
+
const paths = spec.paths;
|
|
137
|
+
if (!paths) return null;
|
|
138
|
+
const want = method.toLowerCase();
|
|
139
|
+
// Prefer an exact path hit (no path-params), then fall back to a regex-based
|
|
140
|
+
// template match. Both passes ignore query strings.
|
|
141
|
+
const cleanPath = concretePath.split("?")[0] ?? concretePath;
|
|
142
|
+
if (paths[cleanPath]) {
|
|
143
|
+
const op = (paths[cleanPath] as Record<string, unknown>)[want];
|
|
144
|
+
if (op && typeof op === "object") return { template: cleanPath, operation: op as Record<string, unknown> };
|
|
145
|
+
}
|
|
146
|
+
for (const [template, item] of Object.entries(paths)) {
|
|
147
|
+
if (template === cleanPath) continue;
|
|
148
|
+
if (!template.includes("{")) continue;
|
|
149
|
+
const re = new RegExp("^" + template.replace(/\{[^}]+\}/g, "[^/]+") + "$");
|
|
150
|
+
if (!re.test(cleanPath)) continue;
|
|
151
|
+
const op = (item as Record<string, unknown>)[want];
|
|
152
|
+
if (op && typeof op === "object") return { template, operation: op as Record<string, unknown> };
|
|
153
|
+
}
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function specSnippet(result: StoredStepResult, specDoc?: OpenApiDocLike | null): string {
|
|
158
|
+
if (!result.spec_pointer && !result.spec_excerpt) {
|
|
159
|
+
// ARV-107: try to recover the operation slice from the loaded spec.
|
|
160
|
+
if (specDoc && result.request_method && result.request_url) {
|
|
161
|
+
const path = extractPath(result.request_url);
|
|
162
|
+
const match = matchSpecOperation(specDoc, result.request_method, path);
|
|
163
|
+
if (match) {
|
|
164
|
+
const ptr = `JSON pointer: \`#/paths/${match.template.replace(/\//g, "~1")}/${result.request_method.toLowerCase()}\`\n\n`;
|
|
165
|
+
return ptr + "```json\n" + JSON.stringify(match.operation, null, 2) + "\n```";
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
return TODO("paste the relevant slice of the OpenAPI spec");
|
|
169
|
+
}
|
|
170
|
+
const ptr = result.spec_pointer ? `JSON pointer: \`${result.spec_pointer}\`\n\n` : "";
|
|
171
|
+
const excerpt = result.spec_excerpt
|
|
172
|
+
? "```json\n" + tryPretty(result.spec_excerpt) + "\n```"
|
|
173
|
+
: TODO("snippet not captured at run time");
|
|
174
|
+
return ptr + excerpt;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function provenanceLine(result: StoredStepResult): string {
|
|
178
|
+
const prov = result.provenance;
|
|
179
|
+
if (!prov) return `Test source: \`${result.suite_name}\` (no provenance metadata).`;
|
|
180
|
+
const parts: string[] = [];
|
|
181
|
+
if (prov.generator) parts.push(`generator \`${prov.generator}\``);
|
|
182
|
+
if (prov.spec) parts.push(`spec \`${prov.spec}\``);
|
|
183
|
+
if (prov.response_branch) parts.push(`branch \`${prov.response_branch}\``);
|
|
184
|
+
return parts.length > 0
|
|
185
|
+
? `Provenance: ${parts.join(", ")}.`
|
|
186
|
+
: `Provenance: \`${prov.type ?? "unknown"}\`.`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function buildZondRequestLine(apiName: string, result: StoredStepResult): string {
|
|
190
|
+
const method = (result.request_method ?? "GET").toUpperCase();
|
|
191
|
+
const path = extractPath(result.request_url);
|
|
192
|
+
const parts: string[] = [`zond request --api ${apiName} ${method} ${path}`];
|
|
193
|
+
if (result.request_body) {
|
|
194
|
+
const escaped = result.request_body.replace(/'/g, `'\\''`);
|
|
195
|
+
parts.push(` --json '${escaped}'`);
|
|
196
|
+
}
|
|
197
|
+
return parts.join(" \\\n");
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function renderCaseStudy(opts: CaseStudyOptions): string {
|
|
201
|
+
const { result, run, specTitle, specVersion, zondVersion, apiName } = opts;
|
|
202
|
+
const method = (result.request_method ?? TODO("HTTP method")).toUpperCase();
|
|
203
|
+
const path = extractPath(result.request_url);
|
|
204
|
+
const fc = result.failure_class ? CLASS_HUMAN[result.failure_class] : TODO("classification");
|
|
205
|
+
|
|
206
|
+
// ARV-107: cascade — explicit spec title, then registry slug (`--api`), then
|
|
207
|
+
// collection-id breadcrumb, then unrecoverable.
|
|
208
|
+
const apiLine = specTitle
|
|
209
|
+
? `${specTitle}${specVersion ? ` ${specVersion}` : ""}`
|
|
210
|
+
: apiName
|
|
211
|
+
? apiName
|
|
212
|
+
: (run.collection_id != null ? TODO("API title — spec was not loadable at export time") : TODO("API name"));
|
|
213
|
+
|
|
214
|
+
const responseStatus = result.response_status != null ? String(result.response_status) : "no response";
|
|
215
|
+
const cappedBody = capBody(result.response_body, opts.bodyCapBytes);
|
|
216
|
+
const responseBody = cappedBody
|
|
217
|
+
? "```json\n" + tryPretty(cappedBody) + "\n```"
|
|
218
|
+
: (result.error_message ? `Network/runtime error: \`${result.error_message}\`` : "_(empty body)_");
|
|
219
|
+
|
|
220
|
+
// ARV-106: prefer the `zond request` form when an api slug is known
|
|
221
|
+
// (skill anti-curl rule). The curl block stays as a copy-paste fallback for
|
|
222
|
+
// recipients without zond installed, with a redacted Authorization header.
|
|
223
|
+
const reproAlt = apiName
|
|
224
|
+
? `\n\n_Or with zond:_\n\n\`\`\`bash\n${buildZondRequestLine(apiName, result)}\n\`\`\``
|
|
225
|
+
: "";
|
|
226
|
+
|
|
227
|
+
return `# ${method} ${path} — ${shortDescription(result)}
|
|
228
|
+
|
|
229
|
+
## TL;DR
|
|
230
|
+
|
|
231
|
+
- **What we found:** ${fc}
|
|
232
|
+
- **What went wrong:** ${tldrLine(result)}
|
|
233
|
+
- **Repro:** see below
|
|
234
|
+
|
|
235
|
+
## Context
|
|
236
|
+
|
|
237
|
+
- **API:** ${apiLine}
|
|
238
|
+
- **Endpoint:** \`${method} ${path}\`
|
|
239
|
+
- ${provenanceLine(result)}
|
|
240
|
+
|
|
241
|
+
### What the spec says
|
|
242
|
+
|
|
243
|
+
${specSnippet(result, opts.specDoc)}
|
|
244
|
+
|
|
245
|
+
## Repro
|
|
246
|
+
|
|
247
|
+
\`\`\`bash
|
|
248
|
+
${buildCurl(result)}
|
|
249
|
+
\`\`\`${reproAlt}
|
|
250
|
+
|
|
251
|
+
## What happened
|
|
252
|
+
|
|
253
|
+
- **Status:** ${responseStatus}
|
|
254
|
+
- **Duration:** ${result.duration_ms} ms
|
|
255
|
+
|
|
256
|
+
${responseBody}
|
|
257
|
+
|
|
258
|
+
## Why it matters
|
|
259
|
+
|
|
260
|
+
${whyItMatters(result)}
|
|
261
|
+
|
|
262
|
+
## How zond found it
|
|
263
|
+
|
|
264
|
+
${howZondFoundIt(result)}
|
|
265
|
+
|
|
266
|
+
---
|
|
267
|
+
|
|
268
|
+
_Generated by zond ${zondVersion} from run #${run.id} (result #${result.id})._
|
|
269
|
+
`;
|
|
270
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { StoredStepResult } from "../../db/queries.ts";
|
|
2
|
+
|
|
3
|
+
export interface BuildCurlOptions {
|
|
4
|
+
/** "redacted" — emit `Authorization: Bearer <REDACTED>` placeholder so the
|
|
5
|
+
* reader knows the original request was authenticated (ARV-106). "omit" —
|
|
6
|
+
* legacy behaviour. Defaults to "redacted". */
|
|
7
|
+
authHeader?: "redacted" | "omit";
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isRemoteUrl(url: string | null | undefined): boolean {
|
|
11
|
+
if (!url) return false;
|
|
12
|
+
try {
|
|
13
|
+
const u = new URL(url);
|
|
14
|
+
if (u.protocol !== "http:" && u.protocol !== "https:") return false;
|
|
15
|
+
const host = u.hostname.toLowerCase();
|
|
16
|
+
return host !== "localhost" && host !== "127.0.0.1" && host !== "::1" && host !== "0.0.0.0";
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Single-quote-wrapped curl, safe-ish for shells. */
|
|
23
|
+
export function buildCurl(step: StoredStepResult, options: BuildCurlOptions = {}): string {
|
|
24
|
+
const parts: string[] = ["curl"];
|
|
25
|
+
const method = step.request_method?.toUpperCase();
|
|
26
|
+
if (method && method !== "GET") {
|
|
27
|
+
parts.push("-X", method);
|
|
28
|
+
}
|
|
29
|
+
const wantAuth = (options.authHeader ?? "redacted") === "redacted" && isRemoteUrl(step.request_url);
|
|
30
|
+
if (wantAuth) {
|
|
31
|
+
parts.push("-H", "'Authorization: Bearer <REDACTED — replace with your token>'");
|
|
32
|
+
}
|
|
33
|
+
if (step.request_body) {
|
|
34
|
+
const escaped = step.request_body.replace(/'/g, `'\\''`);
|
|
35
|
+
parts.push("-H", "'Content-Type: application/json'");
|
|
36
|
+
parts.push("-d", `'${escaped}'`);
|
|
37
|
+
}
|
|
38
|
+
parts.push(`'${step.request_url ?? ""}'`);
|
|
39
|
+
return parts.join(" ");
|
|
40
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single sanitization seam for everything zond writes to disk or stdout.
|
|
3
|
+
*
|
|
4
|
+
* Background: m-10 (TASK-166..168) introduced a secrets registry and
|
|
5
|
+
* `redact()` helper. The first wave of work added `redact(...)` calls
|
|
6
|
+
* inside individual exporters and reporters, which means a new exporter
|
|
7
|
+
* is one missing call away from leaking a secret. TASK-186 collapses
|
|
8
|
+
* that risk: every exporter declares a pure `render()` and goes through
|
|
9
|
+
* the {@link runExporter} pipeline, which applies the sanitizer exactly
|
|
10
|
+
* once at the boundary.
|
|
11
|
+
*
|
|
12
|
+
* The interface is deliberately string-out only — every consumer
|
|
13
|
+
* (writeFile, console.log, HTTP body) accepts a string anyway.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { redact } from "../secrets/registry.ts";
|
|
17
|
+
|
|
18
|
+
export interface Exporter<I, O = void> {
|
|
19
|
+
/** Stable identifier — used in logs and tests. */
|
|
20
|
+
readonly name: string;
|
|
21
|
+
/** Mime hint for the rendered payload. */
|
|
22
|
+
readonly mime: string;
|
|
23
|
+
/** Pure render — no I/O, no redaction. Receives caller-supplied opts. */
|
|
24
|
+
render(input: I, opts?: O): string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Run an exporter's render through the sanitizer pipeline. This is the
|
|
29
|
+
* only place sanitization happens for exporter output — render() must
|
|
30
|
+
* NOT call `redact()` itself, and callers must NOT redact again on top.
|
|
31
|
+
*
|
|
32
|
+
* Sanitization is currently a single pass of {@link redact}; future
|
|
33
|
+
* sanitizer rules (e.g. identity scrubbing) will plug in here so every
|
|
34
|
+
* exporter inherits them automatically.
|
|
35
|
+
*/
|
|
36
|
+
export function runExporter<I, O>(exporter: Exporter<I, O>, input: I, opts?: O): string {
|
|
37
|
+
return applySanitizer(exporter.render(input, opts));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Sanitizer pipeline used by {@link runExporter}. Exposed for the
|
|
42
|
+
* handful of sites that build their payload outside the exporter
|
|
43
|
+
* interface (e.g. probe digests assembled inline) so they can opt into
|
|
44
|
+
* the same single-pass redaction without duplicating logic.
|
|
45
|
+
*/
|
|
46
|
+
export function applySanitizer(payload: string): string {
|
|
47
|
+
return redact(payload);
|
|
48
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Minimal HTML escaper for safe inlining of user-controlled strings
|
|
2
|
+
// (request URLs, response bodies, error messages) into the single-file report.
|
|
3
|
+
|
|
4
|
+
const HTML_ESCAPES: Record<string, string> = {
|
|
5
|
+
"&": "&",
|
|
6
|
+
"<": "<",
|
|
7
|
+
">": ">",
|
|
8
|
+
'"': """,
|
|
9
|
+
"'": "'",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
export function escapeHtml(input: string | null | undefined): string {
|
|
13
|
+
if (input == null) return "";
|
|
14
|
+
return String(input).replace(/[&<>"']/g, (ch) => HTML_ESCAPES[ch] ?? ch);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function tryPrettyJson(s: string | null | undefined): string {
|
|
18
|
+
if (!s) return "";
|
|
19
|
+
try {
|
|
20
|
+
return JSON.stringify(JSON.parse(s), null, 2);
|
|
21
|
+
} catch {
|
|
22
|
+
return s;
|
|
23
|
+
}
|
|
24
|
+
}
|