blastproof 0.17.0 → 0.19.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 +66 -3
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -133,7 +133,7 @@ steps:
|
|
|
133
133
|
- complete checkout
|
|
134
134
|
```
|
|
135
135
|
|
|
136
|
-
`priority` is P0–P2 (default P1). `tags`, `setup` steps and `auth` are optional — `auth: false` runs the test signed out, which a login test needs. `routes` declares the URLs a test covers, which is what `--impacted` selects on; write route strings consistently, since they compare by exact equality (`/cart` ≠ `/cart/`). `run` warns to stderr — non-fatal — when a test declares a route no `routes:` mapping declares, since that route contributes nothing to `--impacted` selection.
|
|
136
|
+
`priority` is P0–P2 (default P1). `tags`, `setup` steps and `auth` are optional — `auth: false` runs the test signed out, which a login test needs, and a selection in which *every* test declares it skips the login altogether. `routes` declares the URLs a test covers, which is what `--impacted` selects on; write route strings consistently, since they compare by exact equality (`/cart` ≠ `/cart/`). `run` warns to stderr — non-fatal — when a test declares a route no `routes:` mapping declares, since that route contributes nothing to `--impacted` selection.
|
|
137
137
|
|
|
138
138
|
### Say what each step should produce
|
|
139
139
|
|
|
@@ -227,6 +227,10 @@ The key is the file glob and the value is the routes it can affect — the oppos
|
|
|
227
227
|
|
|
228
228
|
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.
|
|
229
229
|
|
|
230
|
+
**The diff is `git diff <base>...HEAD`, so it compares commits.** Work you have not committed — edited, staged or untracked — is not in it. That is the right comparison in CI, where everything is committed by the time the workflow runs, and the wrong one on the machine you are writing the change on: a run against a dirty working tree selects from your last commit, not from your editor.
|
|
231
|
+
|
|
232
|
+
It says so rather than leaving you to notice. Any working-tree change the diff excluded is named on stderr, with the routes it maps to, on `run --impacted` and `plan --base` alike — non-fatal, and it changes no exit code and nothing about what is selected. Files your `ignore:` globs already cover stay silent, and so do files already in the diff, whose routes are selected either way.
|
|
233
|
+
|
|
230
234
|
**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.
|
|
231
235
|
|
|
232
236
|
Its limit is worth knowing: it catches files that are *unclassified*, not *misclassified*. A shared module mapped to one route when it can break five still slips through. Impact by import graph is the fix, and blastproof does not do it yet.
|
package/dist/cli.js
CHANGED
|
@@ -598,6 +598,23 @@ var StepRecovery = class {
|
|
|
598
598
|
observe(maskedSnapshot) {
|
|
599
599
|
this.readable.push(normalise(maskedSnapshot));
|
|
600
600
|
}
|
|
601
|
+
/**
|
|
602
|
+
* Whether anything succeeded during this step (design close-a-step D1).
|
|
603
|
+
*
|
|
604
|
+
* Reads the history `record` already keeps rather than a flag threaded
|
|
605
|
+
* through the loop: `record` is reached at one place, after `performAction`
|
|
606
|
+
* returns without throwing, so this is "what worked" and not "what was
|
|
607
|
+
* attempted" — a click that raised is correctly absent. A separate counter
|
|
608
|
+
* would be one more thing a future branch can forget to increment, which is
|
|
609
|
+
* the shape that let `timeoutMs` and the budget go unset at a single call
|
|
610
|
+
* site while every other one set them.
|
|
611
|
+
*
|
|
612
|
+
* `observe` deliberately does not move it. Being shown a page is not doing
|
|
613
|
+
* something to it.
|
|
614
|
+
*/
|
|
615
|
+
get acted() {
|
|
616
|
+
return this.history.length > 0;
|
|
617
|
+
}
|
|
601
618
|
/** Records an action that was actually performed and succeeded. */
|
|
602
619
|
record(action, description, result) {
|
|
603
620
|
this.performed.add(identity(action));
|
|
@@ -691,6 +708,7 @@ function slugify(text) {
|
|
|
691
708
|
const slug = text.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
692
709
|
return slug || "test";
|
|
693
710
|
}
|
|
711
|
+
var NOTHING_DONE_REFUSAL = "refused: nothing succeeded during this step, so it was NOT closed. A step closes on what was done in it, not on being declared finished. If the outcome already holds \u2014 there was nothing left to do \u2014 assert that it holds and let the judgment close the step; an assertion is evidence, a declaration is not. If the step cannot be carried out at all, fail it and say why.";
|
|
694
712
|
var StepFailure = class extends Error {
|
|
695
713
|
};
|
|
696
714
|
async function executeTest(page, test, options) {
|
|
@@ -775,6 +793,15 @@ async function executeTest(page, test, options) {
|
|
|
775
793
|
}
|
|
776
794
|
iterations++;
|
|
777
795
|
if (action.action === "done") {
|
|
796
|
+
if (!recovery.acted) {
|
|
797
|
+
failedAttempts++;
|
|
798
|
+
lastResult = NOTHING_DONE_REFUSAL;
|
|
799
|
+
emitAction(index, action, lastResult);
|
|
800
|
+
if (failedAttempts >= maxRetries) {
|
|
801
|
+
throw new StepFailure(lastResult);
|
|
802
|
+
}
|
|
803
|
+
continue;
|
|
804
|
+
}
|
|
778
805
|
emitAction(index, action, action.reasoning);
|
|
779
806
|
break;
|
|
780
807
|
}
|
|
@@ -1289,6 +1316,20 @@ async function getChangedFiles(baseRef, cwd = process.cwd()) {
|
|
|
1289
1316
|
throw toDiffError(error, baseRef);
|
|
1290
1317
|
}
|
|
1291
1318
|
}
|
|
1319
|
+
async function getUncommittedFiles(cwd = process.cwd()) {
|
|
1320
|
+
try {
|
|
1321
|
+
const status = await simpleGit(cwd).status();
|
|
1322
|
+
const paths = /* @__PURE__ */ new Set();
|
|
1323
|
+
for (const file of status.files) {
|
|
1324
|
+
paths.add(file.path.replace(/\\/g, "/"));
|
|
1325
|
+
const from = file.from;
|
|
1326
|
+
if (from) paths.add(from.replace(/\\/g, "/"));
|
|
1327
|
+
}
|
|
1328
|
+
return [...paths].sort();
|
|
1329
|
+
} catch {
|
|
1330
|
+
return [];
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1292
1333
|
|
|
1293
1334
|
// src/impact.ts
|
|
1294
1335
|
import picomatch from "picomatch";
|
|
@@ -1327,6 +1368,25 @@ function mapImpact(changedFiles, routes, ignore = []) {
|
|
|
1327
1368
|
};
|
|
1328
1369
|
}
|
|
1329
1370
|
|
|
1371
|
+
// src/report/uncommitted.ts
|
|
1372
|
+
function printUncommitted(uncommitted, changedFiles, config, baseRef) {
|
|
1373
|
+
const inDiff = new Set(changedFiles.map((file) => file.replace(/\\/g, "/")));
|
|
1374
|
+
const routes = config.routes ?? {};
|
|
1375
|
+
const ignore = config.ignore ?? [];
|
|
1376
|
+
const findings = uncommitted.filter((file) => !inDiff.has(file)).map((file) => ({ file, impact: mapImpact([file], routes, ignore) })).filter(({ impact }) => impact.ignoredFiles.length === 0);
|
|
1377
|
+
if (findings.length === 0) return;
|
|
1378
|
+
console.error(
|
|
1379
|
+
`
|
|
1380
|
+
warning: ${findings.length} file(s) changed in the working tree are not in the diff against '${baseRef}':`
|
|
1381
|
+
);
|
|
1382
|
+
for (const { file, impact } of findings) {
|
|
1383
|
+
console.error(
|
|
1384
|
+
impact.affectedRoutes.length > 0 ? ` ${file} -> ${impact.affectedRoutes.join(", ")}` : ` ${file} (matched by no routes: or ignore: glob)`
|
|
1385
|
+
);
|
|
1386
|
+
}
|
|
1387
|
+
console.error("Nothing above was considered. Commit or stash them to include them.");
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1330
1390
|
// src/llm/brain.ts
|
|
1331
1391
|
import { generateObject } from "ai";
|
|
1332
1392
|
|
|
@@ -2547,7 +2607,8 @@ Dry run: ${selected.length} test(s) selected, base_url=${config.base_url}`);
|
|
|
2547
2607
|
for (const test of selected) {
|
|
2548
2608
|
console.log(` ${test.summary} [${test.priority}] (${path9.relative(cwd, test.path)})`);
|
|
2549
2609
|
}
|
|
2550
|
-
const
|
|
2610
|
+
const needsLogin = selected.some((test) => test.auth);
|
|
2611
|
+
const authSteps = needsLogin ? config.auth?.steps ?? [] : [];
|
|
2551
2612
|
const withAuth = authSteps.length > 0 ? [...selected, { steps: authSteps }] : selected;
|
|
2552
2613
|
const ceiling = estimateMaxModelCalls(
|
|
2553
2614
|
withAuth,
|
|
@@ -2592,6 +2653,7 @@ async function runCommand(options) {
|
|
|
2592
2653
|
throw error;
|
|
2593
2654
|
}
|
|
2594
2655
|
impact = mapImpact(changedFiles, config.routes ?? {}, config.ignore ?? []);
|
|
2656
|
+
printUncommitted(await getUncommittedFiles(options.cwd), changedFiles, config, base);
|
|
2595
2657
|
}
|
|
2596
2658
|
const testsDir = path9.join(options.cwd, TESTS_RELATIVE_DIR);
|
|
2597
2659
|
let files;
|
|
@@ -2684,7 +2746,7 @@ ${results.length} test file(s) could not be parsed:`);
|
|
|
2684
2746
|
let incomplete;
|
|
2685
2747
|
try {
|
|
2686
2748
|
let session;
|
|
2687
|
-
if (config.auth) {
|
|
2749
|
+
if (config.auth && selected.some((test) => test.auth)) {
|
|
2688
2750
|
try {
|
|
2689
2751
|
console.log("Authenticating...");
|
|
2690
2752
|
session = await authenticate({
|
|
@@ -2805,6 +2867,7 @@ async function resolveTargets(options, config, suite) {
|
|
|
2805
2867
|
const base = options.base ?? "main";
|
|
2806
2868
|
const changedFiles = await getChangedFiles(base, options.cwd);
|
|
2807
2869
|
const impact = mapImpact(changedFiles, config.routes ?? {}, config.ignore ?? []);
|
|
2870
|
+
printUncommitted(await getUncommittedFiles(options.cwd), changedFiles, config, base);
|
|
2808
2871
|
const filesForRoute = (route) => changedFiles.filter(
|
|
2809
2872
|
(file) => mapImpact([file], config.routes ?? {}, config.ignore ?? []).affectedRoutes.includes(route)
|
|
2810
2873
|
);
|
|
@@ -3082,7 +3145,7 @@ function parsePositiveNumber(flag) {
|
|
|
3082
3145
|
};
|
|
3083
3146
|
}
|
|
3084
3147
|
var program = new Command();
|
|
3085
|
-
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.
|
|
3148
|
+
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.19.0");
|
|
3086
3149
|
program.command("init").description("Scaffold .blastproof/ (config, tests, sample tests) in the current directory").action(async () => {
|
|
3087
3150
|
try {
|
|
3088
3151
|
const result = await initProject(process.cwd());
|