@lariocpt/crust-mongo 0.0.0-stage → 0.3.6
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/README.md +174 -2
- package/bin/crust +64 -0
- package/lib/install.mjs +330 -0
- package/lib/skills-prompt.mjs +134 -0
- package/package.json +55 -4
- package/scripts/postinstall.mjs +36 -0
- package/skills/crust-api-testing/SKILL.md +299 -0
- package/skills/crust-builtins/SKILL.md +238 -0
- package/skills/crust-cli/SKILL.md +215 -0
- package/skills/crust-load-testing/SKILL.md +161 -0
- package/skills/crust-logs/SKILL.md +99 -0
- package/skills/crust-mcp/SKILL.md +136 -0
- package/skills/crust-mock-server/SKILL.md +138 -0
- package/skills/crust-pipelines/SKILL.md +209 -0
- package/skills/crust-procs/SKILL.md +108 -0
- package/skills/crust-verify-web-links/SKILL.md +160 -0
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The "install the agent skills?" question, asked once, right after the binary
|
|
2
|
+
// lands in the cache.
|
|
3
|
+
//
|
|
4
|
+
// WHY HERE. The skills are the difference between an agent that guesses at
|
|
5
|
+
// crust's grammar and one that gets it right on the first try, and a user who
|
|
6
|
+
// installs the package is exactly the moment that decision is live. Asking once,
|
|
7
|
+
// at install, beats documenting the verb in a README nobody re-reads.
|
|
8
|
+
//
|
|
9
|
+
// WHAT IT MUST NEVER DO. Break `npm install`. Every failure mode — no TTY, no
|
|
10
|
+
// binary, spawn refused, user typed noise — resolves to "declined", and the
|
|
11
|
+
// caller keeps exit 0. A postinstall that exits non-zero aborts the whole
|
|
12
|
+
// install, and "I did not want to answer a question" is not a reason to refuse
|
|
13
|
+
// to install a package.
|
|
14
|
+
//
|
|
15
|
+
// The gating is a separate pure function because it is the part with real blast
|
|
16
|
+
// radius: prompt in CI and the build hangs on a question nobody is there to
|
|
17
|
+
// answer; skip the cwd check and the skills land inside node_modules/crust.
|
|
18
|
+
import { spawn } from "node:child_process";
|
|
19
|
+
import { existsSync, readdirSync } from "node:fs";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
|
|
22
|
+
// Where the skills live. `.agents/skills` is the canonical, cross-client path;
|
|
23
|
+
// `.claude/skills` is where Claude Code looks, and crust keeps a per-skill link
|
|
24
|
+
// there. Both are checked for "already installed" so someone upgrading from a
|
|
25
|
+
// crust that wrote only the Claude path is not asked to start over.
|
|
26
|
+
const SKILLS_SUBDIR = ".agents/skills";
|
|
27
|
+
const OTHER_SKILL_SUBDIRS = [".claude/skills"];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Decide whether to ask, and where "this project" is.
|
|
31
|
+
*
|
|
32
|
+
* `INIT_CWD` is the directory `npm`/`bun`/`pnpm` were actually run from; the
|
|
33
|
+
* process cwd during a lifecycle script is the package directory
|
|
34
|
+
* (node_modules/crust), which is NOT where a project-scoped skill belongs.
|
|
35
|
+
*/
|
|
36
|
+
export function shouldAsk({
|
|
37
|
+
env = {},
|
|
38
|
+
stdinTty = false,
|
|
39
|
+
stdoutTty = false,
|
|
40
|
+
dir = env.INIT_CWD,
|
|
41
|
+
} = {}) {
|
|
42
|
+
if (!stdinTty || !stdoutTty) return { ask: false, reason: "not an interactive terminal" };
|
|
43
|
+
// A CI runner has no human to answer, and `CI` is set by every mainstream one.
|
|
44
|
+
if (env.CI) return { ask: false, reason: "CI" };
|
|
45
|
+
if (env.CRUST_NO_SKILLS) return { ask: false, reason: "CRUST_NO_SKILLS is set" };
|
|
46
|
+
// A global install has no project to write into; suggest the global verb instead.
|
|
47
|
+
if (env.npm_config_global === "true" || env.npm_config_global === "1") {
|
|
48
|
+
return { ask: false, reason: "global install" };
|
|
49
|
+
}
|
|
50
|
+
// Nested install (crust pulled in as somebody's dependency): the skills are for
|
|
51
|
+
// the developer's own agent, not for the package that happens to depend on us.
|
|
52
|
+
if (!dir || dir.split(path.sep).includes("node_modules")) {
|
|
53
|
+
return { ask: false, reason: "not run from a project directory" };
|
|
54
|
+
}
|
|
55
|
+
if (!existsSync(dir)) return { ask: false, reason: "project directory is gone" };
|
|
56
|
+
const installedHere = [SKILLS_SUBDIR, ...OTHER_SKILL_SUBDIRS].some((sub) =>
|
|
57
|
+
hasCrustSkills(path.join(dir, sub)),
|
|
58
|
+
);
|
|
59
|
+
if (installedHere) {
|
|
60
|
+
return { ask: false, reason: "skills already installed" };
|
|
61
|
+
}
|
|
62
|
+
return { ask: true, dir };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Any `crust-*` skill already on disk counts as "this user has done this". */
|
|
66
|
+
function hasCrustSkills(skillsDir) {
|
|
67
|
+
let names;
|
|
68
|
+
try {
|
|
69
|
+
names = readdirSync(skillsDir);
|
|
70
|
+
} catch {
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
return names.some((n) => n.startsWith("crust-"));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Ask, and on yes hand the terminal to `crust skills install` so the picker —
|
|
78
|
+
* not this script — decides which skills land where.
|
|
79
|
+
*
|
|
80
|
+
* stdio is inherited precisely so the picker can take raw mode over the same
|
|
81
|
+
* terminal. Reading the answer here and then re-reading keystrokes there would
|
|
82
|
+
* otherwise fight over stdin.
|
|
83
|
+
*/
|
|
84
|
+
export async function askSkills({ bin, env = {}, log = () => {}, cwd = env.INIT_CWD }) {
|
|
85
|
+
// No binary, no question: the picker lives inside it, and asking something we
|
|
86
|
+
// cannot act on turns a helpful prompt into a broken one.
|
|
87
|
+
if (!bin || !existsSync(bin)) return { asked: false, reason: "binary not available yet" };
|
|
88
|
+
const gate = shouldAsk({
|
|
89
|
+
env,
|
|
90
|
+
stdinTty: process.stdin.isTTY === true,
|
|
91
|
+
stdoutTty: process.stdout.isTTY === true,
|
|
92
|
+
dir: cwd,
|
|
93
|
+
});
|
|
94
|
+
if (!gate.ask) return { asked: false, reason: gate.reason };
|
|
95
|
+
|
|
96
|
+
log("");
|
|
97
|
+
log("crust ships agent skills — the SKILL.md guides Codex, Cursor, Claude Code");
|
|
98
|
+
log("and OpenCode read to drive crust correctly.");
|
|
99
|
+
log(`Install them into ${gate.dir}/.agents/skills? [y/N] `);
|
|
100
|
+
|
|
101
|
+
const answer = await readAnswer();
|
|
102
|
+
if (!/^y(es)?$/i.test(answer.trim())) {
|
|
103
|
+
log("skipped — `crust skills install` does it later (add --global for ~/.agents).");
|
|
104
|
+
log("to stop being asked: CRUST_NO_SKILLS=1");
|
|
105
|
+
return { asked: true, installed: false };
|
|
106
|
+
}
|
|
107
|
+
const code = await new Promise((resolve) => {
|
|
108
|
+
const child = spawn(bin, ["skills", "install"], { cwd: gate.dir, stdio: "inherit" });
|
|
109
|
+
child.on("error", () => resolve(-1));
|
|
110
|
+
child.on("close", (c) => resolve(c ?? -1));
|
|
111
|
+
});
|
|
112
|
+
return { asked: true, installed: code === 0, code };
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/** One line from stdin. Resolves empty on EOF so a half-open pipe cannot hang. */
|
|
116
|
+
function readAnswer() {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
let buf = "";
|
|
119
|
+
const done = (value) => {
|
|
120
|
+
process.stdin.off("data", onData);
|
|
121
|
+
process.stdin.off("end", onEnd);
|
|
122
|
+
resolve(value);
|
|
123
|
+
};
|
|
124
|
+
const onData = (chunk) => {
|
|
125
|
+
buf += chunk.toString("utf8");
|
|
126
|
+
const nl = buf.indexOf("\n");
|
|
127
|
+
if (nl !== -1) done(buf.slice(0, nl));
|
|
128
|
+
};
|
|
129
|
+
const onEnd = () => done(buf);
|
|
130
|
+
process.stdin.on("data", onData);
|
|
131
|
+
process.stdin.once("end", onEnd);
|
|
132
|
+
process.stdin.resume();
|
|
133
|
+
});
|
|
134
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,57 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lariocpt/crust-mongo",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"version": "0.3.6",
|
|
4
|
+
"description": "crust with the MongoDB driver bundled — the mongo variant of the crust binary launcher",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "Lario Borges",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lariocpt/crust.git"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://lariocpt.github.io/crust-website/",
|
|
13
|
+
"bugs": "https://github.com/lariocpt/crust/issues",
|
|
14
|
+
"keywords": [
|
|
15
|
+
"bun",
|
|
16
|
+
"devops",
|
|
17
|
+
"pipeline",
|
|
18
|
+
"load-testing",
|
|
19
|
+
"mock-server",
|
|
20
|
+
"openapi",
|
|
21
|
+
"testing",
|
|
22
|
+
"fixtures",
|
|
23
|
+
"test-generation",
|
|
24
|
+
"cli",
|
|
25
|
+
"mongo",
|
|
26
|
+
"mongodb"
|
|
27
|
+
],
|
|
28
|
+
"bin": {
|
|
29
|
+
"crust": "./bin/crust"
|
|
30
|
+
},
|
|
31
|
+
"crust": {
|
|
32
|
+
"source": "github",
|
|
33
|
+
"repo": "lariocpt/crust",
|
|
34
|
+
"variant": "mongo"
|
|
35
|
+
},
|
|
36
|
+
"files": [
|
|
37
|
+
"bin",
|
|
38
|
+
"lib",
|
|
39
|
+
"scripts",
|
|
40
|
+
"skills",
|
|
41
|
+
"README.md"
|
|
42
|
+
],
|
|
43
|
+
"scripts": {
|
|
44
|
+
"postinstall": "node scripts/postinstall.mjs"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18"
|
|
48
|
+
},
|
|
49
|
+
"os": [
|
|
50
|
+
"linux",
|
|
51
|
+
"darwin"
|
|
52
|
+
],
|
|
53
|
+
"cpu": [
|
|
54
|
+
"x64",
|
|
55
|
+
"arm64"
|
|
56
|
+
]
|
|
57
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// crust — npm postinstall.
|
|
2
|
+
//
|
|
3
|
+
// A fast path, not the mechanism. npm 12 blocks install scripts by default, so bin/crust
|
|
4
|
+
// fetches the binary lazily on first use and this script only front-loads that work for
|
|
5
|
+
// environments where scripts still run. All the logic lives in lib/install.mjs so the two
|
|
6
|
+
// entry points cannot drift apart.
|
|
7
|
+
//
|
|
8
|
+
// It must never fail the install. A postinstall that exits non-zero aborts the whole
|
|
9
|
+
// `npm install`, and "you are not on the LAN right now" is not a reason to refuse to install
|
|
10
|
+
// a package whose launcher can recover later.
|
|
11
|
+
import { ensureBinary, installHint } from "../lib/install.mjs";
|
|
12
|
+
import { askSkills } from "../lib/skills-prompt.mjs";
|
|
13
|
+
|
|
14
|
+
const say = (m) => console.log(`[crust] ${m}`);
|
|
15
|
+
const warn = (m) => console.warn(`[crust] ${m}`);
|
|
16
|
+
|
|
17
|
+
let manifest = null;
|
|
18
|
+
try {
|
|
19
|
+
manifest = await ensureBinary({ log: say, warn });
|
|
20
|
+
} catch (e) {
|
|
21
|
+
warn(e.message);
|
|
22
|
+
warn("it will be fetched on first run instead.");
|
|
23
|
+
for (const l of installHint()) warn(l);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// The one question this installer asks. Its own gate keeps it silent on CI, on a
|
|
27
|
+
// non-tty, on a global install, and once the skills are already in place — and a
|
|
28
|
+
// throw here must not take the install down with it, so it is caught rather than
|
|
29
|
+
// trusted.
|
|
30
|
+
try {
|
|
31
|
+
await askSkills({ bin: manifest?.bin, env: process.env, log: say });
|
|
32
|
+
} catch (e) {
|
|
33
|
+
warn(`skills prompt skipped: ${e.message}`);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
process.exit(0);
|
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
---
|
|
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, 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
|
+
---
|
|
5
|
+
|
|
6
|
+
# crust API testing
|
|
7
|
+
|
|
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:
|
|
21
|
+
|
|
22
|
+
| Layer | File | Best for |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `test-pipes` | `.pipes` — one pipeline per line | readable CRUD suites, chaining, SQL cross-checks |
|
|
25
|
+
| `test-fixture` | `.crust.ts` — TS fixture modules | complex setup, matcher functions, stress (`-n`) |
|
|
26
|
+
| `gen-fixtures` | generated from `openapi.json` | negative matrices + CRUD flows for every documented op |
|
|
27
|
+
|
|
28
|
+
## .pipes suites (test-pipes)
|
|
29
|
+
|
|
30
|
+
One shorthand pipeline per line; `#` comments; lines run sequentially per
|
|
31
|
+
file. `capture` chains requests; `sql` verifies the database inline:
|
|
32
|
+
|
|
33
|
+
```crust
|
|
34
|
+
{"name": "Court", "floors": 3} | POST $BASE/api/buildings -H "authorization: Bearer $TOKEN" | assert (r => r.status === 201) | (r => r.json()) | capture BID (b => b.building.id)
|
|
35
|
+
GET $BASE/api/buildings/$BID -H "authorization: Bearer $TOKEN" | expect 200
|
|
36
|
+
sql "SELECT count(*)::int AS c FROM buildings WHERE name = 'Court'" | assert (r => r.c === 1)
|
|
37
|
+
DELETE $BASE/api/buildings/$BID -H "authorization: Bearer $TOKEN" | expect 204
|
|
38
|
+
GET $BASE/api/buildings/$BID -H "authorization: Bearer $TOKEN" | expect 404
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Run: `test-pipes 'tests/**/*.pipes' [-b] [-t ms] [-s mod.ts] [-o report.xml]`.
|
|
42
|
+
PASS/FAIL report lines are prefixed `file:LINE` with real file line numbers.
|
|
43
|
+
`-o`'s extension picks the format: `.xml` is JUnit (testcase per line),
|
|
44
|
+
`.json` the raw report — both CI-ingestable. Any other extension (or none)
|
|
45
|
+
writes the same text report stdout shows; the file never changes the exit code.
|
|
46
|
+
|
|
47
|
+
- **Setup module**: `-s mod.ts`, else sibling `<name>.setup.ts`; its
|
|
48
|
+
default export is awaited before the file and seeds `process.env`
|
|
49
|
+
($BASE/$TOKEN). `sql` needs `$DATABASE_URL`.
|
|
50
|
+
- **SQL row types**: rows arrive as the driver returns them — uuid and enum
|
|
51
|
+
columns as strings, but `count(*)` and `numeric` may arrive as
|
|
52
|
+
strings/bigints. Cast in SQL when comparing numbers
|
|
53
|
+
(`count(*)::int AS c`), and compare ids as strings.
|
|
54
|
+
- **Hermetic per file**: `process.env` is snapshotted/restored around each
|
|
55
|
+
file — captures and setup vars never leak across files.
|
|
56
|
+
- Status-check placement: `expect N` when the check ends the line;
|
|
57
|
+
`assert (r => r.status === N)` when more stages follow (expect only
|
|
58
|
+
throws at drain).
|
|
59
|
+
- Redirect control: `--redirect error|manual|follow` on any http line —
|
|
60
|
+
`error` refuses a 3xx (a moved endpoint cannot pass by landing elsewhere;
|
|
61
|
+
the line fails with `redirect refused (--redirect error)`), `manual`
|
|
62
|
+
yields the 3xx so `GET … --redirect manual | expect 301` asserts a
|
|
63
|
+
redirect exists, `follow` is the default. This is the pipe-side twin of a
|
|
64
|
+
fixture's `input.redirect` — when a pipe cannot express a case, the
|
|
65
|
+
fixture can, and it has always had this control. In per-item/load position
|
|
66
|
+
a refusal is a `{status: 0, redirectRefused: true}` RECORD and the line
|
|
67
|
+
exits 0 — gate with `expect 200` or `stats | assert (s =>
|
|
68
|
+
s.redirectRefused === 0)`; exit 0 alone is never request success in load
|
|
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/.
|
|
73
|
+
- Exit codes: 0 all pass · 1 any fail · 2 no files/bad args.
|
|
74
|
+
|
|
75
|
+
## .crust.ts fixtures (test-fixture)
|
|
76
|
+
|
|
77
|
+
One fixture = one request. `setup()` returns a ctx; `input` fields that are
|
|
78
|
+
1-arg functions receive it; `output` fields that are 1+-arg functions are
|
|
79
|
+
matchers `(actual, ctx)` — async matchers awaited:
|
|
80
|
+
|
|
81
|
+
```ts
|
|
82
|
+
export default {
|
|
83
|
+
name: "creates a user",
|
|
84
|
+
setup: async () => ({ token: await login() }),
|
|
85
|
+
input: {
|
|
86
|
+
url: "http://localhost:3000/api/users",
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers: (ctx) => ({ authorization: `Bearer ${ctx.token}` }),
|
|
89
|
+
body: { name: "x" },
|
|
90
|
+
},
|
|
91
|
+
output: {
|
|
92
|
+
status: 201,
|
|
93
|
+
data: async (d, ctx) => (await db.userById(d.id)) !== null,
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
Run: `test-fixture 'tests/*.crust.ts' -j8 [-n N] [-t ms] [-b] [-o report.xml]`.
|
|
99
|
+
`-o`'s extension picks the format — `.xml` is JUnit for CI: testcase per
|
|
100
|
+
run (stress iterations individually), percentiles in `<system-out>`; `.json` is
|
|
101
|
+
the raw report, `.md` markdown, anything else the text report.
|
|
102
|
+
|
|
103
|
+
`input.redirect: "error" | "manual" | "follow"` reaches fetch like any other
|
|
104
|
+
`RequestInit` field — the fixture-side twin of the http stage's `--redirect`
|
|
105
|
+
flag. `"error"` fails the fixture with the field named when the upstream
|
|
106
|
+
answers 3xx (a moved endpoint cannot pass by landing elsewhere); `"manual"`
|
|
107
|
+
records the 3xx so `output: { status: 301 }` asserts a redirect exists. Any
|
|
108
|
+
other value is a loud fixture error, not a fetch mystery.
|
|
109
|
+
|
|
110
|
+
`output.schema`: give it an inline JSON Schema and the response body must
|
|
111
|
+
conform — violations fail with per-field pointer paths. Inline means
|
|
112
|
+
$ref-free: a `$ref` anywhere in the schema is a loud error (the runner has
|
|
113
|
+
no spec to resolve it against — it would otherwise pass silently).
|
|
114
|
+
Unknown schema keywords pass (never-invent-a-violation). gen-fixtures emits
|
|
115
|
+
this automatically when the spec documents a response schema for a case's
|
|
116
|
+
expected status.
|
|
117
|
+
Fixtures may run CONCURRENTLY under -j/--threads — share state only via a
|
|
118
|
+
module-scope promise-cached factory. Fixture files may import ONLY relative
|
|
119
|
+
modules and Bun builtins (the binary can't resolve npm at runtime) —
|
|
120
|
+
`Bun.SQL`, `Bun.file`, `Bun.jwt`.
|
|
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
|
+
|
|
152
|
+
## gen-fixtures — spec-driven cases
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
gen-fixtures ./openapi.json [--no-flows] # --out/--setup default to tests/gen
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
Emits (deterministic, byte-stable — check the output in, regenerate, review
|
|
159
|
+
the `git diff`):
|
|
160
|
+
|
|
161
|
+
- **Negative matrix** per op (`<tag>.gen.crust.ts`): 401 no-creds, 403
|
|
162
|
+
authenticated-outsider (scope-gated paths), 404 unknown-id, and a 400
|
|
163
|
+
matrix per body field — missing required, wrong type, bad enum, too
|
|
164
|
+
short/long (minLength/maxLength), below/above numeric bounds, pattern
|
|
165
|
+
violations, unexpected extra property. Nullable `anyOf` fields unwrapped.
|
|
166
|
+
- **CRUD flows** (`flows/flows.gen.pipes` + sibling setup, run by
|
|
167
|
+
test-pipes with zero extra flags): create → read (body round-trips the
|
|
168
|
+
captured id) → update → delete → read-after-delete-404, chained with
|
|
169
|
+
`capture`, auth via `$GEN_AUTH_HEADER`. SQL assertions are NOT derivable
|
|
170
|
+
from a spec — add those by hand in your own .pipes.
|
|
171
|
+
|
|
172
|
+
Setup-module contract (imported lazily): `shared()` promise-cached context,
|
|
173
|
+
`headersFor(ctx, "none"|"member"|"outsider")`, `resolvePath(ctx, template)`,
|
|
174
|
+
`scopeParam`, `scopeRoots?`, `JSON_HEADERS`, and optional
|
|
175
|
+
`flowOverrides: { "<template>": { body?: {...}, skip?: true } }` for creates
|
|
176
|
+
whose values a spec can't express (business date rules → `body` merge;
|
|
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.
|
|
182
|
+
|
|
183
|
+
The `--out` dir is DELETED and recreated every run — never hand-edit
|
|
184
|
+
generated files.
|
|
185
|
+
|
|
186
|
+
What crust cannot attribute it does not emit. The pattern sampler returns a
|
|
187
|
+
knowingly-wrong placeholder for a pattern it cannot read — a lookahead, Python
|
|
188
|
+
`\A`/`\Z` anchors, a JavaScript regex literal written into `pattern` (groups
|
|
189
|
+
it reads: `(?:…)`, `(a|b)`, `(…){n}`, so an AWS id like
|
|
190
|
+
`^(subnet-[0-9a-f]{8}|subnet-[0-9a-f]{17})$` yields `subnet-00000000`); in a
|
|
191
|
+
base body a placeholder makes a case about `name` invalid in `subnetId`, so its
|
|
192
|
+
400 names the wrong field. Those cases — and a CRUD flow
|
|
193
|
+
whose create body cannot be filled — are dropped and NAMED on stdout
|
|
194
|
+
(`gen-fixtures: 2 case(s) skipped for POST /things …`, `skipping flow for
|
|
195
|
+
/things …`). A silently smaller suite would look like coverage.
|
|
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
|
+
|
|
266
|
+
## Workflow for a new API
|
|
267
|
+
|
|
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.
|