@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
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ARV-127 (m-19): file-based SQLite migration runner.
|
|
3
|
+
*
|
|
4
|
+
* Why a new runner. The legacy migration path in `schema.ts`
|
|
5
|
+
* (`runMigrations` + `PRAGMA user_version`) is fine for the additive
|
|
6
|
+
* column changes shipped through v10, but the knowledge-base work
|
|
7
|
+
* planned past m-19 will need richer migrations (multi-statement,
|
|
8
|
+
* data backfills, optional rollback notes). Inlining those as `if
|
|
9
|
+
* (ver >= N && ver < N+1)` blocks in TypeScript stops scaling once
|
|
10
|
+
* each migration becomes a small project of its own.
|
|
11
|
+
*
|
|
12
|
+
* This module sits on top of the legacy path:
|
|
13
|
+
* - `runMigrations()` is untouched — it owns the PRAGMA-version era
|
|
14
|
+
* and keeps fresh DBs / older snapshots correct.
|
|
15
|
+
* - `applyMigrations()` runs *after* `runMigrations()`, walks the
|
|
16
|
+
* registered migration list, and applies anything not yet recorded
|
|
17
|
+
* in `schema_migrations`. New work (v11+) lands as files; the
|
|
18
|
+
* 0001_run_kind.sql file mirrors the most recent legacy migration
|
|
19
|
+
* so the two systems agree on the post-v10 schema for fresh DBs.
|
|
20
|
+
*
|
|
21
|
+
* Existing-DB compatibility (AC#5). On a `.zond/zond.db` that already
|
|
22
|
+
* survived the legacy `runMigrations` path (user_version >= 10), the
|
|
23
|
+
* `run_kind` column already exists — re-running `0001_run_kind.sql`
|
|
24
|
+
* would throw a `duplicate column` error. We seed the legacy ids into
|
|
25
|
+
* `schema_migrations` once, on first contact with the new runner, so
|
|
26
|
+
* those rows are treated as "already applied" without executing.
|
|
27
|
+
*
|
|
28
|
+
* Distribution. The SQL bodies are imported as embedded text so
|
|
29
|
+
* `bun build --compile` packs them into the binary (no on-disk
|
|
30
|
+
* lookup at runtime — same pattern as the init/templates skills).
|
|
31
|
+
*/
|
|
32
|
+
import type { Database } from "bun:sqlite";
|
|
33
|
+
|
|
34
|
+
import migration_0001_run_kind from "./migrations/0001_run_kind.sql" with { type: "text" };
|
|
35
|
+
|
|
36
|
+
/** Migration manifest. Each entry is a `{ id, sql }` pair; order in
|
|
37
|
+
* this array is the apply order, matching the lexical sort that the
|
|
38
|
+
* Django / Rails-style `<id>_<slug>.sql` convention would produce on
|
|
39
|
+
* disk. Adding a new migration = add a text-import + push to this
|
|
40
|
+
* list. The runner reads this constant, not the filesystem. */
|
|
41
|
+
const MIGRATIONS: ReadonlyArray<{ id: string; sql: string }> = [
|
|
42
|
+
{ id: "0001_run_kind", sql: migration_0001_run_kind },
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/** Pre-existing migration ids that were already applied by the legacy
|
|
46
|
+
* PRAGMA-version path. When the new runner first encounters a DB
|
|
47
|
+
* whose `user_version >= 10`, we record these as applied without
|
|
48
|
+
* running them — the inline `runMigrations` already did. */
|
|
49
|
+
const LEGACY_SEED_IDS: ReadonlyArray<{ id: string; minUserVersion: number }> = [
|
|
50
|
+
{ id: "0001_run_kind", minUserVersion: 10 },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
function currentUserVersion(db: Database): number {
|
|
54
|
+
const row = db.query("PRAGMA user_version").get() as
|
|
55
|
+
| { user_version: number }
|
|
56
|
+
| undefined;
|
|
57
|
+
return row?.user_version ?? 0;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Idempotently apply every pending migration. Safe to call on every
|
|
62
|
+
* DB open — the registry table makes the no-op case cheap.
|
|
63
|
+
*
|
|
64
|
+
* Failure semantics: each migration runs in its own transaction. A
|
|
65
|
+
* script that throws (bad SQL, constraint violation) rolls its own
|
|
66
|
+
* statements back and re-raises; later migrations don't run. The
|
|
67
|
+
* caller (DB open path) treats this as fatal — there is no partial
|
|
68
|
+
* upgrade.
|
|
69
|
+
*
|
|
70
|
+
* The optional `overrides` parameter lets tests inject a synthetic
|
|
71
|
+
* migration list (e.g. to exercise a migration order or a failing
|
|
72
|
+
* script) without touching the shipped manifest.
|
|
73
|
+
*/
|
|
74
|
+
export function applyMigrations(
|
|
75
|
+
db: Database,
|
|
76
|
+
overrides?: { migrations?: ReadonlyArray<{ id: string; sql: string }>; legacySeed?: ReadonlyArray<{ id: string; minUserVersion: number }> },
|
|
77
|
+
): { applied: string[]; skipped: string[] } {
|
|
78
|
+
const migrations = overrides?.migrations ?? MIGRATIONS;
|
|
79
|
+
const legacySeed = overrides?.legacySeed ?? LEGACY_SEED_IDS;
|
|
80
|
+
|
|
81
|
+
db.exec(`
|
|
82
|
+
CREATE TABLE IF NOT EXISTS schema_migrations (
|
|
83
|
+
id TEXT PRIMARY KEY,
|
|
84
|
+
applied_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
85
|
+
)
|
|
86
|
+
`);
|
|
87
|
+
|
|
88
|
+
// Legacy seed: mark already-applied-by-the-PRAGMA-runner ids as done.
|
|
89
|
+
const userVersion = currentUserVersion(db);
|
|
90
|
+
const insertSeed = db.prepare(
|
|
91
|
+
"INSERT OR IGNORE INTO schema_migrations (id) VALUES (?)",
|
|
92
|
+
);
|
|
93
|
+
for (const seed of legacySeed) {
|
|
94
|
+
if (userVersion >= seed.minUserVersion) {
|
|
95
|
+
insertSeed.run(seed.id);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const appliedRows = db
|
|
100
|
+
.query("SELECT id FROM schema_migrations")
|
|
101
|
+
.all() as Array<{ id: string }>;
|
|
102
|
+
const alreadyApplied = new Set(appliedRows.map((r) => r.id));
|
|
103
|
+
|
|
104
|
+
const applied: string[] = [];
|
|
105
|
+
const skipped: string[] = [];
|
|
106
|
+
|
|
107
|
+
for (const migration of migrations) {
|
|
108
|
+
if (alreadyApplied.has(migration.id)) {
|
|
109
|
+
skipped.push(migration.id);
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
db.transaction(() => {
|
|
113
|
+
db.exec(migration.sql);
|
|
114
|
+
db.prepare("INSERT INTO schema_migrations (id) VALUES (?)").run(migration.id);
|
|
115
|
+
})();
|
|
116
|
+
applied.push(migration.id);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return { applied, skipped };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/** Exported for tests + downstream tooling that wants to know which
|
|
123
|
+
* migration ids ship with the binary. */
|
|
124
|
+
export function listShippedMigrations(): string[] {
|
|
125
|
+
return MIGRATIONS.map((m) => m.id);
|
|
126
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- ARV-127 (m-19): captures the legacy v9→v10 inline migration as the
|
|
2
|
+
-- first file-based migration of the new runner. Mirrors the SQL block
|
|
3
|
+
-- previously written in src/db/schema.ts `runMigrations()`. Existing
|
|
4
|
+
-- `.zond/zond.db` files that already ran the inline migration are
|
|
5
|
+
-- pre-seeded as "applied" by `applyMigrations`, so this script never
|
|
6
|
+
-- re-executes the ALTER on a DB where `run_kind` already exists.
|
|
7
|
+
--
|
|
8
|
+
-- Source: ARV-55 — classify each historical run by suite kind so the
|
|
9
|
+
-- coverage default query becomes a column compare.
|
|
10
|
+
|
|
11
|
+
ALTER TABLE runs ADD COLUMN run_kind TEXT NOT NULL DEFAULT 'regular';
|
|
12
|
+
|
|
13
|
+
UPDATE runs SET run_kind = 'probe'
|
|
14
|
+
WHERE id IN (
|
|
15
|
+
SELECT r.id FROM runs r
|
|
16
|
+
WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%probes/%')
|
|
17
|
+
AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%probes/%')
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
UPDATE runs SET run_kind = 'check'
|
|
21
|
+
WHERE id IN (
|
|
22
|
+
SELECT r.id FROM runs r
|
|
23
|
+
WHERE EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file LIKE '%checks/%')
|
|
24
|
+
AND NOT EXISTS (SELECT 1 FROM results WHERE run_id = r.id AND suite_file IS NOT NULL AND suite_file NOT LIKE '%checks/%')
|
|
25
|
+
);
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { getDb } from "../schema.ts";
|
|
2
|
+
import {
|
|
3
|
+
normalizePath,
|
|
4
|
+
type CollectionRecord,
|
|
5
|
+
type CollectionSummary,
|
|
6
|
+
type CreateCollectionOpts,
|
|
7
|
+
type RunRecord,
|
|
8
|
+
} from "./types.ts";
|
|
9
|
+
|
|
10
|
+
export function createCollection(opts: CreateCollectionOpts): number {
|
|
11
|
+
const db = getDb();
|
|
12
|
+
const stmt = db.prepare(`
|
|
13
|
+
INSERT INTO collections (name, base_dir, test_path, openapi_spec)
|
|
14
|
+
VALUES ($name, $base_dir, $test_path, $openapi_spec)
|
|
15
|
+
`);
|
|
16
|
+
const result = stmt.run({
|
|
17
|
+
$name: opts.name,
|
|
18
|
+
$base_dir: opts.base_dir ?? null,
|
|
19
|
+
$test_path: opts.test_path,
|
|
20
|
+
$openapi_spec: opts.openapi_spec ?? null,
|
|
21
|
+
});
|
|
22
|
+
return Number(result.lastInsertRowid);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getCollectionById(id: number): CollectionRecord | null {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
return db.query("SELECT * FROM collections WHERE id = ?").get(id) as CollectionRecord | null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function getLatestRunByCollection(
|
|
31
|
+
collectionId: number,
|
|
32
|
+
opts: { runKind?: "regular" | "probe" | "check" | "any" } = {},
|
|
33
|
+
): RunRecord | null {
|
|
34
|
+
const db = getDb();
|
|
35
|
+
// ARV-55: 'regular' is the default so coverage skips probe-only runs
|
|
36
|
+
// without an explicit predicate. 'any' opts back into the legacy
|
|
37
|
+
// behaviour (used by `coverage`'s probe-run hint logic).
|
|
38
|
+
const kind = opts.runKind ?? "regular";
|
|
39
|
+
const kindClause = kind === "any" ? "" : "AND run_kind = ?";
|
|
40
|
+
const params: (string | number)[] = [collectionId];
|
|
41
|
+
if (kind !== "any") params.push(kind);
|
|
42
|
+
const row = db.query(`
|
|
43
|
+
SELECT * FROM runs
|
|
44
|
+
WHERE collection_id = ? AND finished_at IS NOT NULL ${kindClause}
|
|
45
|
+
ORDER BY started_at DESC
|
|
46
|
+
LIMIT 1
|
|
47
|
+
`).get(...params) as (Record<string, unknown> & { tags?: unknown }) | null;
|
|
48
|
+
if (!row) return null;
|
|
49
|
+
let tags: string[] | null = null;
|
|
50
|
+
if (typeof row.tags === "string") {
|
|
51
|
+
try {
|
|
52
|
+
const v = JSON.parse(row.tags);
|
|
53
|
+
if (Array.isArray(v) && v.every((x) => typeof x === "string")) tags = v;
|
|
54
|
+
} catch {
|
|
55
|
+
// legacy/corrupt — leave null
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// ARV-55: normalise run_kind alongside tags so RunRecord stays consistent.
|
|
59
|
+
const rk = row.run_kind;
|
|
60
|
+
const run_kind: import("../../core/runner/run-kind.ts").RunKind =
|
|
61
|
+
rk === "probe" || rk === "check" ? rk : "regular";
|
|
62
|
+
return { ...(row as unknown as RunRecord), tags, run_kind };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function listCollections(): CollectionSummary[] {
|
|
66
|
+
const db = getDb();
|
|
67
|
+
return db.query(`
|
|
68
|
+
SELECT
|
|
69
|
+
c.id, c.name, c.base_dir, c.test_path, c.openapi_spec, c.created_at,
|
|
70
|
+
COUNT(r.id) AS total_runs,
|
|
71
|
+
CASE WHEN SUM(r.total) > 0
|
|
72
|
+
THEN ROUND(SUM(r.passed) * 100.0 / SUM(r.total), 1)
|
|
73
|
+
ELSE 0 END AS pass_rate,
|
|
74
|
+
MAX(r.started_at) AS last_run_at,
|
|
75
|
+
COALESCE((SELECT passed FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_passed,
|
|
76
|
+
COALESCE((SELECT failed FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_failed,
|
|
77
|
+
COALESCE((SELECT total FROM runs WHERE collection_id = c.id ORDER BY started_at DESC LIMIT 1), 0) AS last_run_total
|
|
78
|
+
FROM collections c
|
|
79
|
+
LEFT JOIN runs r ON r.collection_id = c.id AND r.finished_at IS NOT NULL
|
|
80
|
+
GROUP BY c.id
|
|
81
|
+
ORDER BY c.name
|
|
82
|
+
`).all() as CollectionSummary[];
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function updateCollection(id: number, opts: Partial<CreateCollectionOpts>): boolean {
|
|
86
|
+
const db = getDb();
|
|
87
|
+
const sets: string[] = [];
|
|
88
|
+
const params: Record<string, any> = { $id: id };
|
|
89
|
+
|
|
90
|
+
if (opts.name !== undefined) { sets.push("name = $name"); params.$name = opts.name; }
|
|
91
|
+
if (opts.base_dir !== undefined) { sets.push("base_dir = $base_dir"); params.$base_dir = opts.base_dir; }
|
|
92
|
+
if (opts.test_path !== undefined) { sets.push("test_path = $test_path"); params.$test_path = opts.test_path; }
|
|
93
|
+
if (opts.openapi_spec !== undefined) { sets.push("openapi_spec = $openapi_spec"); params.$openapi_spec = opts.openapi_spec; }
|
|
94
|
+
|
|
95
|
+
if (sets.length === 0) return false;
|
|
96
|
+
|
|
97
|
+
const result = db.prepare(`UPDATE collections SET ${sets.join(", ")} WHERE id = $id`).run(params);
|
|
98
|
+
return result.changes > 0;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function deleteCollection(id: number, deleteRuns = false): boolean {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
if (deleteRuns) {
|
|
104
|
+
const runIds = db.query("SELECT id FROM runs WHERE collection_id = ?").all(id) as { id: number }[];
|
|
105
|
+
for (const row of runIds) {
|
|
106
|
+
db.prepare("DELETE FROM results WHERE run_id = ?").run(row.id);
|
|
107
|
+
}
|
|
108
|
+
db.prepare("DELETE FROM runs WHERE collection_id = ?").run(id);
|
|
109
|
+
} else {
|
|
110
|
+
db.prepare("UPDATE runs SET collection_id = NULL WHERE collection_id = ?").run(id);
|
|
111
|
+
}
|
|
112
|
+
const result = db.prepare("DELETE FROM collections WHERE id = ?").run(id);
|
|
113
|
+
return result.changes > 0;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export function findCollectionByTestPath(path: string): CollectionRecord | null {
|
|
117
|
+
const db = getDb();
|
|
118
|
+
const normalized = normalizePath(path);
|
|
119
|
+
return db.query("SELECT * FROM collections WHERE test_path = ?").get(normalized) as CollectionRecord | null;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function findCollectionByNameOrId(nameOrId: string): CollectionRecord | null {
|
|
123
|
+
const db = getDb();
|
|
124
|
+
// Try as numeric ID first
|
|
125
|
+
const id = parseInt(nameOrId, 10);
|
|
126
|
+
if (!isNaN(id)) {
|
|
127
|
+
const byId = db.query("SELECT * FROM collections WHERE id = ?").get(id) as CollectionRecord | null;
|
|
128
|
+
if (byId) return byId;
|
|
129
|
+
}
|
|
130
|
+
// Then by name (case-insensitive)
|
|
131
|
+
return db.query("SELECT * FROM collections WHERE lower(name) = lower(?)").get(nameOrId) as CollectionRecord | null;
|
|
132
|
+
}
|
|
133
|
+
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reserved for coverage-domain DB queries. As of TASK-187, coverage
|
|
3
|
+
* tables (`coverage_runs`, etc.) are still managed entirely from
|
|
4
|
+
* `src/core/coverage/`; nothing in `queries.ts` was coverage-specific to
|
|
5
|
+
* move here. The file exists so the per-domain layout is future-proof
|
|
6
|
+
* — when a coverage table-backed feature lands, queries land here and
|
|
7
|
+
* the cli/UI imports stay stable.
|
|
8
|
+
*/
|
|
9
|
+
export {};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { getDb } from "../schema.ts";
|
|
2
|
+
import type {
|
|
3
|
+
DashboardStats,
|
|
4
|
+
PassRateTrendPoint,
|
|
5
|
+
SlowestTest,
|
|
6
|
+
FlakyTest,
|
|
7
|
+
} from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export function getDashboardStats(): DashboardStats {
|
|
10
|
+
const db = getDb();
|
|
11
|
+
const row = db.query(`
|
|
12
|
+
SELECT
|
|
13
|
+
COUNT(*) AS totalRuns,
|
|
14
|
+
COALESCE(SUM(total), 0) AS totalTests,
|
|
15
|
+
CASE WHEN SUM(total) > 0
|
|
16
|
+
THEN ROUND(SUM(passed) * 100.0 / SUM(total), 1)
|
|
17
|
+
ELSE 0 END AS overallPassRate,
|
|
18
|
+
COALESCE(ROUND(AVG(duration_ms), 0), 0) AS avgDuration
|
|
19
|
+
FROM runs
|
|
20
|
+
WHERE finished_at IS NOT NULL
|
|
21
|
+
`).get() as { totalRuns: number; totalTests: number; overallPassRate: number; avgDuration: number };
|
|
22
|
+
return row;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getPassRateTrend(limit = 30): PassRateTrendPoint[] {
|
|
26
|
+
const db = getDb();
|
|
27
|
+
return db.query(`
|
|
28
|
+
SELECT id AS run_id, started_at,
|
|
29
|
+
CASE WHEN total > 0 THEN ROUND(passed * 100.0 / total, 1) ELSE 0 END AS pass_rate
|
|
30
|
+
FROM runs
|
|
31
|
+
WHERE finished_at IS NOT NULL
|
|
32
|
+
ORDER BY started_at DESC
|
|
33
|
+
LIMIT ?
|
|
34
|
+
`).all(limit) as PassRateTrendPoint[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getSlowestTests(limit = 5): SlowestTest[] {
|
|
38
|
+
const db = getDb();
|
|
39
|
+
return db.query(`
|
|
40
|
+
SELECT suite_name, test_name, ROUND(AVG(duration_ms), 0) AS avg_duration
|
|
41
|
+
FROM results
|
|
42
|
+
GROUP BY suite_name, test_name
|
|
43
|
+
ORDER BY avg_duration DESC
|
|
44
|
+
LIMIT ?
|
|
45
|
+
`).all(limit) as SlowestTest[];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function getFlakyTests(runsBack = 20, limit = 5): FlakyTest[] {
|
|
49
|
+
const db = getDb();
|
|
50
|
+
return db.query(`
|
|
51
|
+
SELECT r.suite_name, r.test_name, COUNT(DISTINCT r.status) AS distinct_statuses
|
|
52
|
+
FROM results r
|
|
53
|
+
INNER JOIN (SELECT id FROM runs ORDER BY started_at DESC LIMIT ?) recent ON r.run_id = recent.id
|
|
54
|
+
GROUP BY r.suite_name, r.test_name
|
|
55
|
+
HAVING COUNT(DISTINCT r.status) > 1
|
|
56
|
+
ORDER BY distinct_statuses DESC
|
|
57
|
+
LIMIT ?
|
|
58
|
+
`).all(runsBack, limit) as FlakyTest[];
|
|
59
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { getDb, withDbRetry } from "../schema.ts";
|
|
2
|
+
import type { TestRunResult } from "../../core/runner/types.ts";
|
|
3
|
+
import { getSecretRegistry } from "../../core/secrets/registry.ts";
|
|
4
|
+
import type { StoredStepResult } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
function parseProvenance(raw: unknown): import("../../core/parser/types.ts").SourceMetadata | null {
|
|
7
|
+
if (typeof raw !== "string" || raw.length === 0) return null;
|
|
8
|
+
try {
|
|
9
|
+
const parsed = JSON.parse(raw);
|
|
10
|
+
return parsed && typeof parsed === "object" ? parsed : null;
|
|
11
|
+
} catch {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function saveResults(runId: number, suiteResults: TestRunResult[]): void {
|
|
17
|
+
const db = getDb();
|
|
18
|
+
|
|
19
|
+
const stmt = db.prepare(`
|
|
20
|
+
INSERT INTO results
|
|
21
|
+
(run_id, suite_name, test_name, status, duration_ms,
|
|
22
|
+
request_method, request_url, request_body,
|
|
23
|
+
response_status, response_body, response_headers, error_message, assertions, captures, suite_file, provenance, failure_class, failure_class_reason, spec_pointer, spec_excerpt)
|
|
24
|
+
VALUES
|
|
25
|
+
($run_id, $suite_name, $test_name, $status, $duration_ms,
|
|
26
|
+
$request_method, $request_url, $request_body,
|
|
27
|
+
$response_status, $response_body, $response_headers, $error_message, $assertions, $captures, $suite_file, $provenance, $failure_class, $failure_class_reason, $spec_pointer, $spec_excerpt)
|
|
28
|
+
`);
|
|
29
|
+
|
|
30
|
+
// TASK-167 (m-10): every string field that can carry a leaked secret
|
|
31
|
+
// (URL with token in query, body echo on 401, Set-Cookie header, etc.)
|
|
32
|
+
// goes through the registry sanitizer before INSERT.
|
|
33
|
+
const reg = getSecretRegistry();
|
|
34
|
+
const redactString = (s: string | null | undefined): string | null =>
|
|
35
|
+
s == null ? null : reg.redact(s);
|
|
36
|
+
const redactJson = (v: unknown): string | null => {
|
|
37
|
+
if (v == null) return null;
|
|
38
|
+
if (typeof v === "string") return reg.redact(v);
|
|
39
|
+
return reg.redact(JSON.stringify(v));
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
withDbRetry("saveResults", () => db.transaction(() => {
|
|
43
|
+
for (const suite of suiteResults) {
|
|
44
|
+
for (const step of suite.steps) {
|
|
45
|
+
const maxBodySize = 50_000;
|
|
46
|
+
const truncBody = (s: string | null | undefined) =>
|
|
47
|
+
s && s.length > maxBodySize ? s.slice(0, maxBodySize) + "\n...[truncated]" : (s ?? null);
|
|
48
|
+
stmt.run({
|
|
49
|
+
$run_id: runId,
|
|
50
|
+
$suite_name: suite.suite_name,
|
|
51
|
+
$test_name: step.name,
|
|
52
|
+
$status: step.status,
|
|
53
|
+
$duration_ms: step.duration_ms,
|
|
54
|
+
$request_method: step.request.method,
|
|
55
|
+
$request_url: redactString(step.request.url),
|
|
56
|
+
$request_body: redactString(truncBody(step.request.body)),
|
|
57
|
+
$response_status: step.response?.status ?? null,
|
|
58
|
+
$response_body: redactString(truncBody(step.response?.body)),
|
|
59
|
+
$response_headers: step.response?.headers
|
|
60
|
+
? redactJson(step.response.headers)
|
|
61
|
+
: null,
|
|
62
|
+
$error_message: redactString(step.error ?? null),
|
|
63
|
+
$assertions: step.assertions.length > 0 ? redactJson(step.assertions) : null,
|
|
64
|
+
$captures: Object.keys(step.captures).length > 0 ? redactJson(step.captures) : null,
|
|
65
|
+
$suite_file: suite.suite_file ?? null,
|
|
66
|
+
$provenance: step.provenance ? JSON.stringify(step.provenance) : null,
|
|
67
|
+
$failure_class: step.failure_class ?? null,
|
|
68
|
+
$failure_class_reason: step.failure_class_reason ?? null,
|
|
69
|
+
$spec_pointer: step.spec_pointer ?? null,
|
|
70
|
+
$spec_excerpt: redactString(step.spec_excerpt ?? null),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
})());
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export function getResultsByRunId(runId: number): StoredStepResult[] {
|
|
78
|
+
const db = getDb();
|
|
79
|
+
const rows = db.query("SELECT * FROM results WHERE run_id = ? ORDER BY id").all(runId) as Array<
|
|
80
|
+
Omit<StoredStepResult, "assertions" | "captures" | "provenance"> & {
|
|
81
|
+
assertions: string | null;
|
|
82
|
+
captures: string | null;
|
|
83
|
+
provenance: string | null;
|
|
84
|
+
}
|
|
85
|
+
>;
|
|
86
|
+
return rows.map((row) => ({
|
|
87
|
+
...row,
|
|
88
|
+
assertions: row.assertions ? JSON.parse(row.assertions) : [],
|
|
89
|
+
captures: row.captures ? JSON.parse(row.captures) : {},
|
|
90
|
+
provenance: parseProvenance(row.provenance),
|
|
91
|
+
}));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function getFilteredResults(
|
|
95
|
+
runId: number,
|
|
96
|
+
filters: {
|
|
97
|
+
method?: string;
|
|
98
|
+
/** Compiled SQL fragment for the `--status` filter (TASK-140). */
|
|
99
|
+
statusSql?: { sql: string; params: number[] };
|
|
100
|
+
},
|
|
101
|
+
): StoredStepResult[] {
|
|
102
|
+
const db = getDb();
|
|
103
|
+
const conditions = ["run_id = ?"];
|
|
104
|
+
const params: (string | number)[] = [runId];
|
|
105
|
+
|
|
106
|
+
if (filters.method) {
|
|
107
|
+
conditions.push("request_method = ?");
|
|
108
|
+
params.push(filters.method.toUpperCase());
|
|
109
|
+
}
|
|
110
|
+
if (filters.statusSql) {
|
|
111
|
+
conditions.push(filters.statusSql.sql);
|
|
112
|
+
params.push(...filters.statusSql.params);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const rows = db.query(`SELECT * FROM results WHERE ${conditions.join(" AND ")} ORDER BY id`).all(...params) as Array<
|
|
116
|
+
Omit<StoredStepResult, "assertions" | "captures" | "provenance"> & {
|
|
117
|
+
assertions: string | null;
|
|
118
|
+
captures: string | null;
|
|
119
|
+
provenance: string | null;
|
|
120
|
+
}
|
|
121
|
+
>;
|
|
122
|
+
return rows.map((row) => ({
|
|
123
|
+
...row,
|
|
124
|
+
assertions: row.assertions ? JSON.parse(row.assertions) : [],
|
|
125
|
+
captures: row.captures ? JSON.parse(row.captures) : {},
|
|
126
|
+
provenance: parseProvenance(row.provenance),
|
|
127
|
+
}));
|
|
128
|
+
}
|