@lariocpt/crust-mongo 0.3.5 → 0.3.7
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/package.json
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crust-api-testing
|
|
3
|
-
description: Test HTTP APIs with crust — .pipes suites (one fixture pipeline per line, request chaining via capture, inline SQL verification), .crust.ts fixtures with setup/matchers,
|
|
3
|
+
description: Test HTTP APIs with crust — .pipes suites (one fixture pipeline per line, request chaining via capture, inline SQL verification), .crust.ts fixtures with setup/matchers, gen-fixtures which derives negative-case matrices and CRUD flow suites from an OpenAPI spec, and the evidence-tier taxonomy (snapshot vs DB-verified vs demo). Use for API integration tests, contract tests from a spec, auth/authz negative cases, or DB-verified endpoint tests.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# crust API testing
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
## The shared principle: the lightest CORRECT layer
|
|
9
|
+
|
|
10
|
+
Choose the lightest native layer that preserves correctness, observability,
|
|
11
|
+
and safety — not merely the shortest syntax.
|
|
12
|
+
|
|
13
|
+
- Use `.pipes` for straightforward requests, assertions, and supported request chaining.
|
|
14
|
+
- Use `.crust.ts` when setup, custom matchers, or required safety controls cannot be expressed in pipes.
|
|
15
|
+
- Use `gen-fixtures` for supported contract-derived cases, followed by review.
|
|
16
|
+
- Use native load pipelines for paced traffic, measurements, and explicit success gates.
|
|
17
|
+
- Do not force a case into pipes when doing so removes a required guarantee.
|
|
18
|
+
- Consult repository-specific execution and credential policies before running examples.
|
|
19
|
+
|
|
20
|
+
Three layers, one binary:
|
|
9
21
|
|
|
10
22
|
| Layer | File | Best for |
|
|
11
23
|
|---|---|---|
|
|
@@ -55,6 +67,9 @@ writes the same text report stdout shows; the file never changes the exit code.
|
|
|
55
67
|
exits 0 — gate with `expect 200` or `stats | assert (s =>
|
|
56
68
|
s.redirectRefused === 0)`; exit 0 alone is never request success in load
|
|
57
69
|
mode.
|
|
70
|
+
- Result shapes per execution context (`Response` vs timed `{status, ms, url}`
|
|
71
|
+
records, body draining, `.json()` availability): canonical table at
|
|
72
|
+
https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
|
|
58
73
|
- Exit codes: 0 all pass · 1 any fail · 2 no files/bad args.
|
|
59
74
|
|
|
60
75
|
## .crust.ts fixtures (test-fixture)
|
|
@@ -104,6 +119,36 @@ module-scope promise-cached factory. Fixture files may import ONLY relative
|
|
|
104
119
|
modules and Bun builtins (the binary can't resolve npm at runtime) —
|
|
105
120
|
`Bun.SQL`, `Bun.file`, `Bun.jwt`.
|
|
106
121
|
|
|
122
|
+
## What evidence does your test produce?
|
|
123
|
+
|
|
124
|
+
**Tier 1 — snapshot / contract verification.** Response vs a declared case +
|
|
125
|
+
supplied snapshot; setup prepares REQUEST CONTEXT (url, headers, expected),
|
|
126
|
+
not state. Never call it "seeded"/"integration" when nothing is seeded or
|
|
127
|
+
verified at the data layer — it proves contract shape + declared oracle.
|
|
128
|
+
|
|
129
|
+
**Tier 2 — DB-verified flow.** seed → verify the seed → call the API → query
|
|
130
|
+
the DB → compare against the LIVE query result. Native shape:
|
|
131
|
+
|
|
132
|
+
```crust
|
|
133
|
+
{"email": "u-$RUN_ID@t.dev"} | POST $BASE/api/users -H "authorization: Bearer $TOKEN" | assert (r => r.status === 201) | (r => r.json()) | capture UID (u => u.id)
|
|
134
|
+
sql "SELECT email FROM users WHERE id = '$UID'" | assert (r => r.email === "u-$RUN_ID@t.dev")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Carries: run-owned data (`$RUN_ID` unique per run, set by setup — never
|
|
138
|
+
shared mutable state); explicit identity matching (query the id you
|
|
139
|
+
captured, never an unrelated record); failure-path cleanup defined BEFORE
|
|
140
|
+
writes — a later line is happy-path only; guaranteed teardown is `crust.onExit`.
|
|
141
|
+
|
|
142
|
+
**Tier 3 — demo / quickstart.** Disposable local resource, ordinary CRUD —
|
|
143
|
+
the built-in mock server is the native fit — isolates "how little glue does
|
|
144
|
+
crust need" from app safeguards (graph resolution, provisioning denial,
|
|
145
|
+
identity-sensitive stores) — separate approved fixture, never folded in.
|
|
146
|
+
|
|
147
|
+
Cross-cutting: label the tier so a green run cannot imply DB-level proof it
|
|
148
|
+
never produced; approval/redirect/transport safeguards stay regardless of
|
|
149
|
+
tier; setup attestations (flags asserting IAM/graph/provisioning policy) are
|
|
150
|
+
DECLARATIONS, not backend verification.
|
|
151
|
+
|
|
107
152
|
## gen-fixtures — spec-driven cases
|
|
108
153
|
|
|
109
154
|
```
|
|
@@ -130,6 +175,10 @@ Setup-module contract (imported lazily): `shared()` promise-cached context,
|
|
|
130
175
|
`flowOverrides: { "<template>": { body?: {...}, skip?: true } }` for creates
|
|
131
176
|
whose values a spec can't express (business date rules → `body` merge;
|
|
132
177
|
required foreign keys → `skip`).
|
|
178
|
+
When the contract disagrees with the 400 convention (422 backends, 404/403
|
|
179
|
+
concealment), the optional `expectations` block configures correct statuses —
|
|
180
|
+
a documented status always wins over a configured one;
|
|
181
|
+
`docs/USAGE.md §gen-fixtures` has the schema and precedence.
|
|
133
182
|
|
|
134
183
|
The `--out` dir is DELETED and recreated every run — never hand-edit
|
|
135
184
|
generated files.
|
|
@@ -145,10 +194,106 @@ whose create body cannot be filled — are dropped and NAMED on stdout
|
|
|
145
194
|
(`gen-fixtures: 2 case(s) skipped for POST /things …`, `skipping flow for
|
|
146
195
|
/things …`). A silently smaller suite would look like coverage.
|
|
147
196
|
|
|
197
|
+
Every generation's LAST write is `<out>/gen-manifest.json`: each case with
|
|
198
|
+
its expectation's provenance (`contract:<spec pointer>` when the spec
|
|
199
|
+
documents the status), each skip, each unsupported op — review it BEFORE
|
|
200
|
+
running. Deterministic (same spec + version → byte-identical); it records
|
|
201
|
+
generation, never execution.
|
|
202
|
+
https://lariocpt.github.io/crust-website/docs/recipes/gen-fixtures/
|
|
203
|
+
|
|
204
|
+
## Generated cases are candidates, not certified coverage
|
|
205
|
+
|
|
206
|
+
A generator can derive request mutations from a schema. It cannot universally
|
|
207
|
+
infer backend-specific status codes, error envelopes, authorization
|
|
208
|
+
concealment policies, business preconditions, or cleanup behavior.
|
|
209
|
+
|
|
210
|
+
For negative tests:
|
|
211
|
+
|
|
212
|
+
- Begin with an otherwise valid request.
|
|
213
|
+
- Mutate one intended condition.
|
|
214
|
+
- Ensure authentication and resource preconditions allow the request to
|
|
215
|
+
reach the intended validation layer (a request that dies at auth never
|
|
216
|
+
proves the validation code).
|
|
217
|
+
- Assert the documented status.
|
|
218
|
+
- Where the contract provides structured error evidence, verify the relevant
|
|
219
|
+
field, location, or error code.
|
|
220
|
+
- Avoid exact human-readable message matching unless wording is contractual.
|
|
221
|
+
|
|
222
|
+
A generic error status is insufficient evidence when the request could have
|
|
223
|
+
failed for an unrelated reason. And the generated **400 validation matrix is
|
|
224
|
+
a generator assumption, not a universal API convention** — some contracts
|
|
225
|
+
specify `422` (or another status) for validation failures; when the contract
|
|
226
|
+
disagrees, say so in review and adjust expectations to the CONTRACT, never
|
|
227
|
+
the other way around. Document unsupported assumptions and skipped cases
|
|
228
|
+
rather than silently reducing coverage or normalizing the contract to fit
|
|
229
|
+
generation.
|
|
230
|
+
|
|
231
|
+
## Contract review before generation
|
|
232
|
+
|
|
233
|
+
Review the contract BEFORE generating and record the review beside the
|
|
234
|
+
generated output. Record fields (one per line):
|
|
235
|
+
|
|
236
|
+
```
|
|
237
|
+
API/service:
|
|
238
|
+
Contract path:
|
|
239
|
+
Contract revision:
|
|
240
|
+
Backend revision, if available:
|
|
241
|
+
Crust version:
|
|
242
|
+
Reviewer:
|
|
243
|
+
Review date:
|
|
244
|
+
Generated output directory:
|
|
245
|
+
Intended execution scope:
|
|
246
|
+
Unresolved discrepancies:
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
Highest-risk checks (full checklist:
|
|
250
|
+
https://lariocpt.github.io/crust-website/docs/contract-review/):
|
|
251
|
+
|
|
252
|
+
- Expected statuses come from the reviewed contract — validation cases do
|
|
253
|
+
NOT default to 400 (a 422 contract must generate 422 expectations).
|
|
254
|
+
- Authorization cases account for intentional 404 concealment; unknown-id
|
|
255
|
+
cases use syntactically valid identifiers.
|
|
256
|
+
- One intended condition per negative case; unrelated fields stay valid so
|
|
257
|
+
the request reaches the intended validation layer.
|
|
258
|
+
- Dedicated `--out` dir; regeneration cannot touch manual fixtures; never
|
|
259
|
+
hand-edit generated files.
|
|
260
|
+
- Classify every failure (backend · contract · generator · environment ·
|
|
261
|
+
incorrect test) BEFORE changing any expectation.
|
|
262
|
+
- Execution is approved separately — target, identity, resource scope,
|
|
263
|
+
writes, cleanup. This review grants no permission beyond the recorded
|
|
264
|
+
execution scope.
|
|
265
|
+
|
|
148
266
|
## Workflow for a new API
|
|
149
267
|
|
|
150
|
-
1.
|
|
151
|
-
2.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
268
|
+
1. Identify the authoritative contract and record its version or revision.
|
|
269
|
+
2. Review generation assumptions against that contract: validation status
|
|
270
|
+
codes; authentication and authorization behavior; error envelopes;
|
|
271
|
+
resource identifiers; response schemas; CRUD lifecycle support.
|
|
272
|
+
3. Generate into a dedicated output directory (`--out`, default `tests/gen`).
|
|
273
|
+
Generation DELETES and recreates that directory; never target manually
|
|
274
|
+
authored sources.
|
|
275
|
+
4. Review generated cases, expected responses, setup requirements, and
|
|
276
|
+
skipped cases (they are NAMED on stdout) before execution.
|
|
277
|
+
5. Obtain the required target, identity, resource, write, cleanup, and load
|
|
278
|
+
approvals from repository policy — the skill never grants any of them.
|
|
279
|
+
6. Execute only the approved scope.
|
|
280
|
+
7. Classify failures before changing expectations or implementation:
|
|
281
|
+
backend defect · contract defect · unsupported generator assumption ·
|
|
282
|
+
environment/setup failure · incorrect test.
|
|
283
|
+
8. Add manually authored native fixtures or pipelines for unsupported cases.
|
|
284
|
+
Do not hand-edit generated files (the next regeneration erases the edit).
|
|
285
|
+
9. Record remaining coverage gaps explicitly.
|
|
286
|
+
|
|
287
|
+
> Never change an expected result solely because the backend returned
|
|
288
|
+
> something different. A passing test must reflect a reviewed contract.
|
|
289
|
+
|
|
290
|
+
## Version and evidence
|
|
291
|
+
|
|
292
|
+
Behavior described here was verified against **crust v0.3.5** (source-read
|
|
293
|
+
and run through the compiled binary where it matters: `--redirect` record
|
|
294
|
+
shapes, `output.schema` $ref refusal, gen-fixtures skip reporting). New in
|
|
295
|
+
**v0.3.6**: the `gen-manifest.json` review artifact, the setup-module
|
|
296
|
+
`expectations` block, and the evidence-tier taxonomy. Newer
|
|
297
|
+
crust versions may extend behavior; re-verify before documenting it. Keep
|
|
298
|
+
approved targets, credentials, transport exceptions, resource ownership, and
|
|
299
|
+
execution restrictions in repository policy, not in this skill.
|
|
@@ -27,12 +27,25 @@ load 10s 50/s, 30s 200/s | parallel 100 | GET :3000/health | stats --every 5
|
|
|
27
27
|
load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users | expect 2xx | stats
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
> **Report availability:** `expect` throws at drain, so an `expect` placed
|
|
31
|
+
> BEFORE `stats` that fails prevents the summary from ever being emitted
|
|
32
|
+
> (the first two shapes above are illustrative of syntax, not of CI
|
|
33
|
+
> reporting). When the summary/report must survive a status failure, put
|
|
34
|
+
> `stats` (and `stats --out report.json`) before the gates and gate status
|
|
35
|
+
> on the summary instead — see CI gates below.
|
|
36
|
+
|
|
30
37
|
- `parallel` puts ANY http verb in load mode: output is `{status, ms, url}`
|
|
31
38
|
timing records, bodies drained, network errors become `status: 0` records
|
|
32
39
|
(they show in the histogram instead of killing the run). So a run whose
|
|
33
40
|
every request was refused prints `{"status":{"0":40}}` and still exits 0
|
|
34
41
|
unless a gate is present — `expect 200` over a dead port exits 1. `stats`
|
|
35
42
|
reports, it does not gate.
|
|
43
|
+
- **Completion ≠ success — three separate outcomes:** the run *completed*
|
|
44
|
+
(exit 0, no fatal stage error), *request correctness passed* (explicit
|
|
45
|
+
gates accepted the outcomes), *performance passed* (latency/throughput
|
|
46
|
+
thresholds held). A gate that only checks latency can be satisfied by fast
|
|
47
|
+
error responses — a 500 in 2ms is under any p95 budget. Latency-only
|
|
48
|
+
examples are NOT the definition of a passing load test; gate status too.
|
|
36
49
|
- The summary carries failure tallies `timedOut` and `redirectRefused`
|
|
37
50
|
(always present, 0 on a clean run): a `--timeout` budget hit or a
|
|
38
51
|
`--redirect error` refusal is a `{status: 0, timedOut/redirectRefused:
|
|
@@ -43,6 +56,8 @@ load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users |
|
|
|
43
56
|
Little's law sizes the pool: `N ≥ rate × p99-in-seconds`.
|
|
44
57
|
- Durations `ms|s|m`; rates `N/s` or `N/m`, decimals allowed.
|
|
45
58
|
|
|
59
|
+
Canonical, version-pinned result-shape table: https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
|
|
60
|
+
|
|
46
61
|
## Honesty guarantees (read before quoting numbers)
|
|
47
62
|
|
|
48
63
|
- A gate that measured NOTHING fails: `stats` over an empty stream is tagged
|
|
@@ -67,12 +82,24 @@ load 10s 20/s | (t => ({name: "user" + t.n})) | parallel 8 | POST :3000/users |
|
|
|
67
82
|
|
|
68
83
|
## CI gates — thresholds are assert composition
|
|
69
84
|
|
|
85
|
+
The primary shape gates **status first, then performance**, with `stats`
|
|
86
|
+
(and the report) upstream of every gate so the summary survives a failing
|
|
87
|
+
gate. When failure artifacts must survive request-status failures, collect
|
|
88
|
+
statistics and write the report BEFORE applying aggregate gates:
|
|
89
|
+
|
|
70
90
|
```crust
|
|
71
|
-
load 30s 100/s | parallel 50 | GET :3000/health | stats | assert (s => s.p95 < 200) | assert (s => s.rps > 80)
|
|
91
|
+
load 30s 100/s | parallel 50 | GET :3000/health | stats --out load/last.json | assert (s => Object.keys(s.status).every(k => k.startsWith("2"))) | assert (s => s.timedOut === 0) | assert (s => s.redirectRefused === 0) | assert (s => s.p95 < 200) | assert (s => s.rps > 80)
|
|
72
92
|
```
|
|
73
93
|
|
|
74
94
|
One assert per threshold: the failure names the predicate and prints the
|
|
75
|
-
actual summary, and the line exits 1 (`crust -c` stops there).
|
|
95
|
+
actual summary, and the line exits 1 (`crust -c` stops there). The gates
|
|
96
|
+
above cover all four things a load gate must mean: every response was 2xx
|
|
97
|
+
(a fast 500 is not a pass), no timeout or redirect-refusal records, the
|
|
98
|
+
latency budget held, and the achieved rate held. An empty run is refused by
|
|
99
|
+
`assert` itself (`empty: true`), so "measured nothing" never passes.
|
|
100
|
+
`rps` is the ACHIEVED rate — if the target was not sustained, stderr already
|
|
101
|
+
said so (`emitted … dropped … achieved …`); a drop report means the number
|
|
102
|
+
below the rate gate is the honest one.
|
|
76
103
|
|
|
77
104
|
Baseline gate — `--out` writes a versioned artifact
|
|
78
105
|
(`{crustStats: 1, startedAt, urls, summary, windows?}`), an async assert
|
|
@@ -121,3 +148,14 @@ const out = await load([{ durMs: 10_000, rps: 100 }])
|
|
|
121
148
|
|
|
122
149
|
`timedHttpItem(method, url, opts?)` is the any-verb per-item timer when you
|
|
123
150
|
need POST bodies from upstream items.
|
|
151
|
+
|
|
152
|
+
## Version and evidence
|
|
153
|
+
|
|
154
|
+
Behavior here was verified against **crust v0.3.5** — summary schema
|
|
155
|
+
(`{count, wallMs, rps, status, timedOut, redirectRefused, p50, p95, p99,
|
|
156
|
+
meanMs}`, `empty: true` on a zero-request run) and the `--out`-before-gates
|
|
157
|
+
write order are source-verified; throughput claims (5000+ ticks/s) are
|
|
158
|
+
single-process measurements on the author's hardware — environment-dependent,
|
|
159
|
+
not a promise. Pick the lightest correct layer for the job (see the
|
|
160
|
+
crust-api-testing skill's shared principle), and keep approved targets and
|
|
161
|
+
execution restrictions in repository policy, not in this skill.
|
|
@@ -66,6 +66,31 @@ printf '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion
|
|
|
66
66
|
range(1,5) | (n => n * n) | stats
|
|
67
67
|
```
|
|
68
68
|
|
|
69
|
+
## Tool classes and teardown tiers
|
|
70
|
+
|
|
71
|
+
The `initialize` handshake carries this taxonomy in `instructions` (0.3.6):
|
|
72
|
+
|
|
73
|
+
- **Static check** — `crust_check_line`: parses, runs nothing.
|
|
74
|
+
- **Execution** — `crust_run_line` and the test/gen tools: arbitrary stages,
|
|
75
|
+
real side effects. Target approval, credentials, writes and cleanup stay
|
|
76
|
+
the caller's responsibility.
|
|
77
|
+
- **Network/load generation** — `crust_load_run`, `crust_verify_web_links`:
|
|
78
|
+
bounded generation, approved non-production targets only.
|
|
79
|
+
- **Long-lived resources** — mock server, procs, log sessions: held by the
|
|
80
|
+
server process, die when it exits.
|
|
81
|
+
|
|
82
|
+
Teardown has three tiers and no tool crosses them: **local processes** are
|
|
83
|
+
killed whole (process group, grandchildren included); **mock sessions** close
|
|
84
|
+
(port released — `--state` files/DBs are application data and stay on disk);
|
|
85
|
+
**application data** is never touched — a container, row or queue a proc
|
|
86
|
+
created remotely survives its local teardown, releasing it is your job.
|
|
87
|
+
|
|
88
|
+
Transport contract: stdio JSON-RPC, one frame per line; one-shot tools spawn
|
|
89
|
+
a fresh crust child per call (`cwd`/`env` per call, nothing persists). A
|
|
90
|
+
refusal whose error string never appears in crust's own output (an
|
|
91
|
+
"unavailable tool" style host error) never reached the server — debug the
|
|
92
|
+
host's spawn command, not crust.
|
|
93
|
+
|
|
69
94
|
## Honest failure — never a false pass over MCP
|
|
70
95
|
|
|
71
96
|
A nonzero child exit, a killed timeout, a failed `assert`, a mock validation
|
|
@@ -97,8 +122,11 @@ filter (l => l.proc === "api") | (l => l.line) | grep -i timeout
|
|
|
97
122
|
the returned `url`. `proxy: "<upstream>"` turns it into a conformance audit;
|
|
98
123
|
read findings with `crust_mock_server_violations`.
|
|
99
124
|
- `crust_load_run` composes the documented pipeline (`load … | parallel … |
|
|
100
|
-
… | stats | assert`);
|
|
101
|
-
|
|
125
|
+
… | stats | assert`); gates go in `assert` — one predicate, or an array of
|
|
126
|
+
them (0.3.6), one assert stage per gate. An `expect` inside `target` runs BEFORE
|
|
127
|
+
stats and suppresses the summary when it fails; a post-stats `assert`
|
|
128
|
+
failure is `isError` with the stats still included. A run that measured
|
|
129
|
+
nothing is refused (`empty`), never a green gate.
|
|
102
130
|
|
|
103
131
|
## Limits, stated
|
|
104
132
|
|
|
@@ -9,6 +9,10 @@ crust is a Bun-powered pipeline runner where every command is a typed stream
|
|
|
9
9
|
(`Pipeline<T>`). Stages compose under `|`. Plain shell still works — a line
|
|
10
10
|
whose stages are all ordinary commands is handed to `sh -c` untouched.
|
|
11
11
|
|
|
12
|
+
Choose the lightest native stage that preserves correctness, observability,
|
|
13
|
+
and safety — not merely the shortest syntax (full statement: the
|
|
14
|
+
crust-api-testing skill's shared principle).
|
|
15
|
+
|
|
12
16
|
## How a stage is classified (first token decides)
|
|
13
17
|
|
|
14
18
|
| You type | You get |
|
|
@@ -39,6 +43,33 @@ whose stages are all ordinary commands is handed to `sh -c` untouched.
|
|
|
39
43
|
|
|
40
44
|
`:3000/path` expands to `http://localhost:3000/path` everywhere.
|
|
41
45
|
|
|
46
|
+
## HTTP result shapes — same syntax, different payloads
|
|
47
|
+
|
|
48
|
+
Similar HTTP syntax does NOT guarantee identical result shapes. Know which
|
|
49
|
+
shape a stage receives before reaching for `.json()` or gating results
|
|
50
|
+
(verified against crust v0.3.5):
|
|
51
|
+
|
|
52
|
+
| Context | What the stage emits | Authoring implication |
|
|
53
|
+
|---|---|---|
|
|
54
|
+
| First-stage ordinary verb (`GET :3000/x`) | a `Response` | body parsing is available: `(r => r.json())` |
|
|
55
|
+
| Mid-pipeline ordinary verb, non-GET (per-item, untimed) | a `Response` per item | same — bodies still parseable |
|
|
56
|
+
| Timed per-item path (mid-pipeline `GET` — with or without `parallel` — or `parallel N \| <any other verb>`) | `{status, ms, url}` records; **bodies drained**; network errors are `status: 0` records; `--timeout`/`--redirect error` outcomes are `{status: 0, timedOut/redirectRefused: true}` records | `.json()` does NOT exist on these; gate status and the failure tallies explicitly |
|
|
57
|
+
|
|
58
|
+
Canonical, version-pinned table: https://lariocpt.github.io/crust-website/docs/http-result-shapes/.
|
|
59
|
+
|
|
60
|
+
## Completion is not success — three separate outcomes
|
|
61
|
+
|
|
62
|
+
1. **Execution completed** — the pipeline ran without a fatal stage error (exit 0).
|
|
63
|
+
2. **Request correctness passed** — explicit gates (`expect`, `assert`) accepted the request outcomes.
|
|
64
|
+
3. **Performance passed** — explicit latency/throughput thresholds passed.
|
|
65
|
+
|
|
66
|
+
`stats` reports measurements; it is not itself a success gate. In timed/load
|
|
67
|
+
paths, timeouts, network failures, and redirect refusals are per-item
|
|
68
|
+
RECORDS (`status: 0` + a tally field), not exits — exit 0 alone is never
|
|
69
|
+
evidence that requests succeeded. Use an explicit expected-status gate
|
|
70
|
+
(`expect 200`/`expect 2xx`), and where stats is in play also check
|
|
71
|
+
`s.timedOut === 0` / `s.redirectRefused === 0`.
|
|
72
|
+
|
|
42
73
|
## Env expansion — parse time, opt-in per position
|
|
43
74
|
|
|
44
75
|
`$VAR`/`${VAR}` expand from `process.env` in: URLs, whole `-H` header
|
|
@@ -159,3 +190,20 @@ Any stage throw → `crust: <message>` on stderr, line exits 1. `crust -c`,
|
|
|
159
190
|
the same fail-fast loop and STOP at the first non-zero — put warmup and
|
|
160
191
|
cleanup on their own lines. Blank lines and `#` comments are skipped, so a
|
|
161
192
|
`#!/usr/bin/env crust` shebang works in `.crust` files.
|
|
193
|
+
|
|
194
|
+
**Cleanup on a later line is happy-path cleanup, not guaranteed teardown.**
|
|
195
|
+
Under fail-fast, an earlier failure means that line never runs. When
|
|
196
|
+
failure-path cleanup is a requirement (containers, temp rows, mock servers),
|
|
197
|
+
use the verified lifecycle hook: `crust.onExit = async (code) => {…}` in
|
|
198
|
+
`init.ts` — it fires on every shutdown-path exit,
|
|
199
|
+
including the fail-fast failure path of `-c`, script, and stdin runs
|
|
200
|
+
(test-pinned by `tests/onexit.test.ts`), and receives the exit code. If a needed
|
|
201
|
+
guarantee is still missing (e.g. `kill -9`), document the limitation rather
|
|
202
|
+
than implying line placement solves it.
|
|
203
|
+
|
|
204
|
+
## Version and evidence
|
|
205
|
+
|
|
206
|
+
Behavior here was verified against **crust v0.3.5** — result shapes and
|
|
207
|
+
failure-record semantics by source and through the compiled binary; keep
|
|
208
|
+
approved targets, credentials, and execution restrictions in repository
|
|
209
|
+
policy, not in this skill.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: crust-verify-web-links
|
|
3
|
-
description: Crawl a site from its sitemap and verify every link, #anchor, redirect chain, OG image and meta tag against .crust.ts fixtures. `--base-url` auto-discovers the sitemap (robots.txt → /sitemap.xml → /sitemap-index.xml), `--site-map-url` takes a local sitemap so you can check build output, `--fixtures` diffs meta. Use when link or SEO health must gate a deploy, when a built site should be checked before publishing, or when OG/meta regressions need catching.
|
|
3
|
+
description: Crawl a site from its sitemap and verify every link, #anchor, redirect chain, OG image and meta tag against .crust.ts fixtures. `--base-url` auto-discovers the sitemap (robots.txt → /sitemap.xml → /sitemap-index.xml), `--site-map-url` takes a local sitemap so you can check build output, `--fixtures` diffs meta. Every failure names its source page and element (structured `sources`/`meta`), and every failing run ends with an aligned fix-list. Use when link or SEO health must gate a deploy, when a built site should be checked before publishing, or when OG/meta regressions need catching.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# crust verify-web-links
|
|
@@ -21,6 +21,41 @@ having left discovered links unchecked. `2` also covers `--site-map-url`
|
|
|
21
21
|
together with `--base-url`, neither of them given, `no fixture files matched
|
|
22
22
|
<glob>`, `--concurrency must be >= 1`, and `unexpected argument`.
|
|
23
23
|
|
|
24
|
+
## Every failure names its source (new in 0.3.6)
|
|
25
|
+
|
|
26
|
+
Failures are **one per failing target**, and each answers "which page did I
|
|
27
|
+
find this on, and which element":
|
|
28
|
+
|
|
29
|
+
- `sources[]` — every link that pointed at the failing target, as
|
|
30
|
+
`{fromUrl, kind, href}`. In text each prints as an indented
|
|
31
|
+
`from <page> (<kind>)` line under the failure; in `--json` it is the full
|
|
32
|
+
array. A broken URL linked from fifty pages is still **one** failure with
|
|
33
|
+
fifty sources — `totals.failures` and exit codes are unchanged.
|
|
34
|
+
`kind: "sitemap"` means the URL arrived as a sitemap `<loc>` and no page
|
|
35
|
+
linked it; the sitemap URL is named as `fromUrl` instead of the source being
|
|
36
|
+
omitted.
|
|
37
|
+
- `meta` — structured `{key, expected, actual}` for `meta-mismatch`
|
|
38
|
+
(`meta-fixture-no-page` carries `key: "fixture"`).
|
|
39
|
+
- **Fix-list**: every failing text run ends with a separate aligned section —
|
|
40
|
+
one line per (page, element, broken-thing) triple, nothing else (excerpt of
|
|
41
|
+
the real output; the full failing report is in USAGE and on the website):
|
|
42
|
+
|
|
43
|
+
```text
|
|
44
|
+
fix list
|
|
45
|
+
page element broken link / diff
|
|
46
|
+
http://localhost:47831/page-a a http://localhost:47831/broken
|
|
47
|
+
http://localhost:47831/page-a a http://localhost:47831/page-x#ghost
|
|
48
|
+
http://localhost:47831/page-b img http://localhost:47831/broken
|
|
49
|
+
http://localhost:47831/page-x meta:og:title got "Page X — Real Title" (expected "Page X — Wrong Title")
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
A clean run prints no fix-list section on either channel. Under `--json` the
|
|
53
|
+
report owns stdout, so the same table is written to **stderr** (0.3.7) — the
|
|
54
|
+
structured `sources`/`meta` fields carry the same facts for machines.
|
|
55
|
+
|
|
56
|
+
The `detail` prose is unchanged from v0.3.5: the structured fields are purely
|
|
57
|
+
additive.
|
|
58
|
+
|
|
24
59
|
## Getting it pointed at the right thing
|
|
25
60
|
|
|
26
61
|
```crust
|
|
@@ -71,6 +106,9 @@ Redirect the whole invocation instead, then judge the report:
|
|
|
71
106
|
crust -c 'verify-web-links --base-url https://example.com --json --no-progress' > report.json
|
|
72
107
|
```
|
|
73
108
|
|
|
109
|
+
The redirect is the artifact; the fix-list arrives on stderr, so the CI log
|
|
110
|
+
still names the broken page and element without a second command.
|
|
111
|
+
|
|
74
112
|
```crust
|
|
75
113
|
read report.json | (s => JSON.parse(s)) | assert (r => r.totals.failures === 0)
|
|
76
114
|
read report.json | (s => JSON.parse(s)) | assert (r => r.totals.complete)
|