blastproof 0.12.0 → 0.13.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/README.md +5 -1
- package/dist/cli.js +29 -4
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -141,7 +141,9 @@ Write steps that end in an observable result — text on the page, a count, a st
|
|
|
141
141
|
- fill the note field # cannot run
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
The agent is **forbidden from inventing values** — one it types must come from the step, from the page, or from an `{{env.*}}` placeholder.
|
|
144
|
+
The agent is **forbidden from inventing values** — one it types must come from the step, from the page, or from an `{{env.*}}` placeholder. But that rule lives in a prompt, and a prompt instructs rather than enforces. Run against a real model, the second step does not fail: the agent makes a value up, fills it, and the step **passes**. `fill the note field` produced "This is a test note." on two runs and "This is a new note" on a third.
|
|
145
|
+
|
|
146
|
+
That is worse than a failure. The test goes green having verified a value nobody wrote, differing between runs — a passing check over an unspecified input. Closing the gap in the runner is [#57](https://github.com/hamc/blastproof/issues/57); until then, this warning is what stands between you and a green test that means nothing.
|
|
145
147
|
|
|
146
148
|
`run` warns about it first, on every path, before launching a browser or asking for a key:
|
|
147
149
|
|
|
@@ -196,6 +198,8 @@ ignore:
|
|
|
196
198
|
- "**/*.md"
|
|
197
199
|
```
|
|
198
200
|
|
|
201
|
+
The key is the file glob and the value is the routes it can affect — the opposite way round from a test file's own `routes:`, which is a plain list of the routes that test covers. Inverted, the map matches nothing at all and every run reports a diff that affected no page, so `blastproof` refuses a map written that way rather than running green against it.
|
|
202
|
+
|
|
199
203
|
Every changed file lands in one of three buckets: it matches `routes:` and contributes them, matches `ignore:` and is knowingly irrelevant, or **matches neither — nobody has said what it affects**. `--fail-on-unmapped` blocks on that third case, naming the files and both ways to resolve them.
|
|
200
204
|
|
|
201
205
|
**Nothing is ignored by default**, on purpose: a default that guesses on your behalf would hide the first files worth thinking about. The flag is additive — a run can meet `--min-score` and still be blocked here, because "the tests I ran passed" and "something changed that nobody classified" are different claims.
|
package/dist/cli.js
CHANGED
|
@@ -49,6 +49,9 @@ browser:
|
|
|
49
49
|
# max_retries_per_step: 3
|
|
50
50
|
|
|
51
51
|
# Impact hints mapping file globs to routes (used by \`blastproof run --impacted\`).
|
|
52
|
+
# Read each line as "if this file changed, these pages are at risk" \u2014 the key is the
|
|
53
|
+
# file glob, the value is the routes. Not the other way round: a test file's own
|
|
54
|
+
# \`routes:\` is a list of routes, and this one is a map keyed by source path.
|
|
52
55
|
routes:
|
|
53
56
|
"src/auth/**": ["/login"]
|
|
54
57
|
"src/cart/**": ["/cart", "/checkout"]
|
|
@@ -988,11 +991,33 @@ var authSchema = z.object({
|
|
|
988
991
|
});
|
|
989
992
|
}
|
|
990
993
|
});
|
|
994
|
+
var ROUTE_SHAPED = /^\/[^*?]*$/;
|
|
995
|
+
var FILE_SHAPED = /[*?]|\.[a-z0-9]{1,8}$/i;
|
|
996
|
+
function findInvertedRouteEntries(routes) {
|
|
997
|
+
const inverted = [];
|
|
998
|
+
for (const [key, values] of Object.entries(routes)) {
|
|
999
|
+
if (!ROUTE_SHAPED.test(key)) continue;
|
|
1000
|
+
const file = values.find((value) => FILE_SHAPED.test(value));
|
|
1001
|
+
if (file !== void 0) inverted.push({ key, file });
|
|
1002
|
+
}
|
|
1003
|
+
return inverted;
|
|
1004
|
+
}
|
|
991
1005
|
var configSchema = z.object({
|
|
992
1006
|
base_url: z.string().url(),
|
|
993
1007
|
llm: llmSchema.default({}),
|
|
994
1008
|
browser: browserSchema.default({}),
|
|
995
|
-
routes: z.record(z.array(z.string())).
|
|
1009
|
+
routes: z.record(z.array(z.string())).superRefine((value, ctx) => {
|
|
1010
|
+
const inverted = findInvertedRouteEntries(value);
|
|
1011
|
+
const first = inverted[0];
|
|
1012
|
+
if (!first) return;
|
|
1013
|
+
const others = inverted.length > 1 ? ` (and ${inverted.length - 1} more)` : "";
|
|
1014
|
+
ctx.addIssue({
|
|
1015
|
+
code: z.ZodIssueCode.custom,
|
|
1016
|
+
message: `is the wrong way round${others} \u2014 the key is the file glob, the value is the routes it affects.
|
|
1017
|
+
found: "${first.key}": ["${first.file}"]
|
|
1018
|
+
expected: "${first.file}": ["${first.key}"]`
|
|
1019
|
+
});
|
|
1020
|
+
}).optional(),
|
|
996
1021
|
/** Globs for files knowingly irrelevant to any route (docs, CI config, licences). */
|
|
997
1022
|
ignore: z.array(z.string()).optional(),
|
|
998
1023
|
/**
|
|
@@ -1280,7 +1305,7 @@ Rules:
|
|
|
1280
1305
|
- Prefer the journey the changed files touch over a generic tour of the page. The changed files tell you which part of the page matters.
|
|
1281
1306
|
- **The test starts at the application's base URL, not at this route.** Begin with a step that navigates to the route and says what should be visible once it loads \u2014 "navigate to /support and verify the heading "Contact support" is shown". Without it the run opens the home page and every later step looks for controls that are not there.
|
|
1282
1307
|
- **Every step says what it should produce.** Name what must be true once the step has been carried out, not the action alone: "submit the support form and verify the confirmation page shows the ticket number", never "submit the support form". A step that names an action without an outcome asks the runner to judge whether something happened while looking at the page that succeeding produces \u2014 a submitted form comes back empty, a redirect moves the URL \u2014 and that is the shape behind several real failures.
|
|
1283
|
-
- **A step that enters a value writes the value.** "fill the subject field with Order not received", never "enter a subject". The runner is forbidden from inventing values,
|
|
1308
|
+
- **A step that enters a value writes the value.** "fill the subject field with Order not received", never "enter a subject". The runner is forbidden from inventing values, but a step that supplies none does not fail \u2014 the value gets made up, the step passes, and the test then verifies something nobody wrote.
|
|
1284
1309
|
- If a step needs a credential or any secret, write it as a placeholder like {{env.TEST_PASSWORD}}. Never write a real or invented password, token or key.
|
|
1285
1310
|
- Keep the whole test to a handful of steps: one journey, not an exhaustive suite.`;
|
|
1286
1311
|
}
|
|
@@ -2346,7 +2371,7 @@ function printAuthoring(result, cwd) {
|
|
|
2346
2371
|
console.error(` \u2192 ${suggestValueClause(step)}`);
|
|
2347
2372
|
}
|
|
2348
2373
|
console.error(
|
|
2349
|
-
"The runner is forbidden from inventing values,
|
|
2374
|
+
"The runner is forbidden from inventing values, but only by instruction: in practice the model supplies one anyway, the step passes, and the test verifies a value nobody wrote \u2014 a different one on each run."
|
|
2350
2375
|
);
|
|
2351
2376
|
console.error("Steps are inspected in English only; steps in other languages are not checked.");
|
|
2352
2377
|
}
|
|
@@ -2893,7 +2918,7 @@ function parsePositiveNumber(flag) {
|
|
|
2893
2918
|
};
|
|
2894
2919
|
}
|
|
2895
2920
|
var program = new Command();
|
|
2896
|
-
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.
|
|
2921
|
+
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.13.0");
|
|
2897
2922
|
program.command("init").description("Scaffold .blastproof/ (config, tests, sample tests) in the current directory").action(async () => {
|
|
2898
2923
|
try {
|
|
2899
2924
|
const result = await initProject(process.cwd());
|