blastproof 0.2.0 → 0.2.2
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 +23 -9
- package/dist/cli.js +95 -44
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,21 +25,26 @@ git diff → impact mapping → test generation → agentic execution → report
|
|
|
25
25
|
```bash
|
|
26
26
|
npm install -g blastproof # requires Node.js >= 20.19
|
|
27
27
|
npx playwright install --with-deps chromium # one-time browser download
|
|
28
|
+
|
|
28
29
|
cd your-project
|
|
29
|
-
blastproof init
|
|
30
|
+
blastproof init # scaffolds .blastproof/
|
|
31
|
+
# start your app, then point base_url at it in .blastproof/config.yaml
|
|
30
32
|
export ANTHROPIC_API_KEY=... # or OPENAI_API_KEY, or a local Ollama model
|
|
31
33
|
blastproof run # runs .blastproof/tests/**/*.yaml agentically
|
|
32
34
|
```
|
|
33
35
|
|
|
36
|
+
`init` scaffolds one smoke test that works against any app, plus a commented login template you rename and edit once it describes *your* login. Nothing is written that assumes anything about your application.
|
|
37
|
+
|
|
34
38
|
Published with [npm provenance](https://docs.npmjs.com/generating-provenance-statements), so the tarball is verifiably built from this repository.
|
|
35
39
|
|
|
36
|
-
Try it
|
|
40
|
+
Try it without your own app — the demo shop lives in this repository, so clone it:
|
|
37
41
|
|
|
38
42
|
```bash
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
git clone https://github.com/hamc/blastproof && cd blastproof
|
|
44
|
+
npm install && npm run build
|
|
45
|
+
node examples/demo-app/serve.mjs 4173 & # home, login, cart, checkout, orders
|
|
41
46
|
export ANTHROPIC_API_KEY=...
|
|
42
|
-
|
|
47
|
+
node dist/cli.js run
|
|
43
48
|
```
|
|
44
49
|
|
|
45
50
|
> **Status:** the pipeline is complete and published. `init`, `run` (including `--impacted`), `plan` and `test`, with authentication, JUnit and HTML reports, a merge gate, and a GitHub Action. Pre-1.0: the command surface may still change.
|
|
@@ -116,7 +121,7 @@ Drafts are **previews by default** — nothing touches disk until `--write`, and
|
|
|
116
121
|
|
|
117
122
|
Exit codes: 0 when every route generated (or nothing needed coverage), 1 when a route failed, 2 on usage/config/diff errors. A route that fails to load never aborts the others.
|
|
118
123
|
|
|
119
|
-
|
|
124
|
+
`plan` uses the same `auth:` recipe as `run`, so a route behind a login is drafted from the real page rather than from the login wall.
|
|
120
125
|
|
|
121
126
|
### Closing the coverage hole
|
|
122
127
|
|
|
@@ -205,7 +210,7 @@ jobs:
|
|
|
205
210
|
|
|
206
211
|
- run: npm start & # however your app boots
|
|
207
212
|
|
|
208
|
-
- uses: hamc/blastproof@v0.2.
|
|
213
|
+
- uses: hamc/blastproof@v0.2.2
|
|
209
214
|
with:
|
|
210
215
|
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
211
216
|
base: ${{ github.event.pull_request.base.ref }}
|
|
@@ -217,8 +222,9 @@ Exit non-zero blocks the merge. Use the score in a later step:
|
|
|
217
222
|
|
|
218
223
|
```yaml
|
|
219
224
|
- id: bp
|
|
220
|
-
uses: hamc/blastproof@v0.2.
|
|
225
|
+
uses: hamc/blastproof@v0.2.2
|
|
221
226
|
with:
|
|
227
|
+
version: '0.2.2' # pin both when the result gates merges
|
|
222
228
|
api-key: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
223
229
|
base: ${{ github.event.pull_request.base.ref }}
|
|
224
230
|
min-score: '80'
|
|
@@ -267,7 +273,9 @@ allowed_origins:
|
|
|
267
273
|
|
|
268
274
|
This is enforced by comparison, not by asking the model nicely, so it holds regardless of what the page says.
|
|
269
275
|
|
|
270
|
-
**Your secrets
|
|
276
|
+
**Your secrets stay out of the model's prompts.** `{{env.*}}` placeholders stay intact all the way through the prompt and are substituted at the moment of typing. Every value any test or the auth recipe references is also redacted from anything else crossing into a prompt — page snapshots included, since your app may render the credential itself — in both literal and percent-encoded form. This matters because blastproof encourages pointing `llm.base_url` at a gateway you do not run.
|
|
277
|
+
|
|
278
|
+
Redaction matches known values, so it cannot anticipate every way a page might transform one before rendering it. Treat it as a strong default, not a guarantee against a hostile application.
|
|
271
279
|
|
|
272
280
|
The system prompt also tells the model that page content is data under test and never an instruction to obey. That raises the cost of a casual injection and is **not** a security boundary — a determined one will get past prompt wording. The origin constraint is the boundary; treat the rest as hygiene, and do not point blastproof at an application you would not run locally.
|
|
273
281
|
|
|
@@ -356,6 +364,12 @@ Note the last one names *which variable* holds your key — the key itself is ne
|
|
|
356
364
|
|
|
357
365
|
## Development
|
|
358
366
|
|
|
367
|
+
Built with AI assistance, using spec-driven development throughout: every change began as a written
|
|
368
|
+
proposal with its design rationale, and those documents are kept rather than discarded — `openspec/`
|
|
369
|
+
holds the reasoning behind each decision, including the alternatives that were rejected and why. The
|
|
370
|
+
`.claude/`, `.cursor/` and `.opencode/` directories are agent configuration for the tools used to
|
|
371
|
+
build it; ignore them unless you are contributing with one.
|
|
372
|
+
|
|
359
373
|
This project uses **spec-driven development** via [OpenSpec](https://github.com/Fission-AI/OpenSpec). See [`AGENTS.md`](./AGENTS.md) for architecture, conventions and the contribution workflow — every change starts with an OpenSpec proposal.
|
|
360
374
|
|
|
361
375
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -93,21 +93,25 @@ sessions/
|
|
|
93
93
|
.auth-state.json
|
|
94
94
|
auth.json
|
|
95
95
|
`;
|
|
96
|
-
var
|
|
96
|
+
var SAMPLE_LOGIN_TEMPLATE = `# TEMPLATE \u2014 rename to login.yaml once these steps match your app.
|
|
97
|
+
# Until then it is ignored: only .yaml/.yml files are discovered.
|
|
98
|
+
#
|
|
99
|
+
# Credentials come from the environment. Export them before running:
|
|
100
|
+
# export TEST_EMAIL=... TEST_PASSWORD=...
|
|
101
|
+
# Substituted values are masked as *** everywhere and never reach the model.
|
|
102
|
+
summary: Login with valid credentials succeeds
|
|
97
103
|
priority: P0
|
|
98
104
|
tags: [smoke, auth]
|
|
99
105
|
routes: ["/login"]
|
|
100
|
-
#
|
|
101
|
-
#
|
|
106
|
+
# Exercises the login itself, so it must start signed out even when an auth
|
|
107
|
+
# recipe is configured.
|
|
102
108
|
auth: false
|
|
103
109
|
steps:
|
|
104
110
|
- navigate to /login
|
|
105
|
-
- fill the email field with
|
|
106
|
-
- fill the password field with
|
|
111
|
+
- fill the email field with {{env.TEST_EMAIL}}
|
|
112
|
+
- fill the password field with {{env.TEST_PASSWORD}}
|
|
107
113
|
- submit the login form
|
|
108
114
|
- verify a welcome message is shown
|
|
109
|
-
# Secrets tip: use {{env.VAR_NAME}} placeholders in steps (e.g. {{env.TEST_PASSWORD}})
|
|
110
|
-
# and export the variable before running. Values are masked as *** in all output.
|
|
111
115
|
`;
|
|
112
116
|
async function exists(file) {
|
|
113
117
|
try {
|
|
@@ -132,7 +136,7 @@ async function initProject(cwd = process.cwd()) {
|
|
|
132
136
|
await writeIfAbsent(path.join(root, ".gitignore"), GITIGNORE, result);
|
|
133
137
|
await writeIfAbsent(path.join(root, "config.yaml"), DEFAULT_CONFIG, result);
|
|
134
138
|
await writeIfAbsent(path.join(root, "tests", "app-load.yaml"), SAMPLE_TEST, result);
|
|
135
|
-
await writeIfAbsent(path.join(root, "tests", "login.yaml"),
|
|
139
|
+
await writeIfAbsent(path.join(root, "tests", "login.yaml.example"), SAMPLE_LOGIN_TEMPLATE, result);
|
|
136
140
|
return result;
|
|
137
141
|
}
|
|
138
142
|
function formatInitGuidance(result) {
|
|
@@ -149,7 +153,7 @@ function formatInitGuidance(result) {
|
|
|
149
153
|
"",
|
|
150
154
|
"Next steps:",
|
|
151
155
|
" 1. Set your LLM API key: export ANTHROPIC_API_KEY=... (or OPENAI_API_KEY; ollama needs no key)",
|
|
152
|
-
" 2.
|
|
156
|
+
" 2. Start your app, then point base_url at it in .blastproof/config.yaml",
|
|
153
157
|
" 3. Run your tests: blastproof run",
|
|
154
158
|
"",
|
|
155
159
|
"First time with Playwright? Install the browser: npx playwright install chromium"
|
|
@@ -211,9 +215,21 @@ var SecretsMask = class {
|
|
|
211
215
|
if (value === void 0) {
|
|
212
216
|
throw new MissingEnvError(name);
|
|
213
217
|
}
|
|
214
|
-
this.
|
|
218
|
+
this.add(value);
|
|
215
219
|
}
|
|
216
220
|
}
|
|
221
|
+
/**
|
|
222
|
+
* Registers a value and the forms it takes on the way to a prompt. `navigate`
|
|
223
|
+
* reports a resolved URL, and `new URL()` percent-encodes — so a secret with a
|
|
224
|
+
* space stopped matching a literal search and passed through unmasked.
|
|
225
|
+
* This cannot cover every transform a page might apply; see the README.
|
|
226
|
+
*/
|
|
227
|
+
add(value) {
|
|
228
|
+
if (!value) return;
|
|
229
|
+
this.secrets.add(value);
|
|
230
|
+
const encoded = encodeURIComponent(value);
|
|
231
|
+
if (encoded !== value) this.secrets.add(encoded);
|
|
232
|
+
}
|
|
217
233
|
mask(text) {
|
|
218
234
|
return maskSecrets(text, this.secrets);
|
|
219
235
|
}
|
|
@@ -392,8 +408,8 @@ async function executeTest(page, test, options) {
|
|
|
392
408
|
action = await brain.nextAction({
|
|
393
409
|
step,
|
|
394
410
|
isSetup: setup,
|
|
395
|
-
snapshot: snap,
|
|
396
|
-
lastResult,
|
|
411
|
+
snapshot: mask(snap),
|
|
412
|
+
lastResult: lastResult === void 0 ? void 0 : mask(lastResult),
|
|
397
413
|
retriesLeft: maxRetries - failedAttempts,
|
|
398
414
|
iterationsLeft: maxIterationsPerStep - iterations
|
|
399
415
|
});
|
|
@@ -416,7 +432,7 @@ async function executeTest(page, test, options) {
|
|
|
416
432
|
}
|
|
417
433
|
if (action.action === "assert") {
|
|
418
434
|
const expectation = action.expectation ?? action.reasoning;
|
|
419
|
-
const judgment = await brain.judge(expectation, snap);
|
|
435
|
+
const judgment = await brain.judge(mask(expectation), mask(snap));
|
|
420
436
|
const result = judgment.pass ? `ok: assertion passed: ${judgment.reason}` : `assertion failed: ${judgment.reason}`;
|
|
421
437
|
emitAction(index, action, result);
|
|
422
438
|
if (judgment.pass) {
|
|
@@ -521,8 +537,7 @@ async function fromStorageState(file) {
|
|
|
521
537
|
}
|
|
522
538
|
return { storageState: parsed };
|
|
523
539
|
}
|
|
524
|
-
function fromStatic(auth) {
|
|
525
|
-
const mask = new SecretsMask();
|
|
540
|
+
function fromStatic(auth, mask) {
|
|
526
541
|
const session = {};
|
|
527
542
|
if (auth.headers) {
|
|
528
543
|
const headers = {};
|
|
@@ -539,7 +554,7 @@ function fromStatic(auth) {
|
|
|
539
554
|
});
|
|
540
555
|
session.storageState = { cookies, origins: [] };
|
|
541
556
|
}
|
|
542
|
-
return
|
|
557
|
+
return session;
|
|
543
558
|
}
|
|
544
559
|
async function runJourney(...args) {
|
|
545
560
|
try {
|
|
@@ -551,10 +566,8 @@ async function runJourney(...args) {
|
|
|
551
566
|
}
|
|
552
567
|
}
|
|
553
568
|
async function fromSteps(options) {
|
|
554
|
-
const { auth, baseUrl, browser, brain, maxRetries, snapshot = defaultSnapshot, onEvent } = options;
|
|
569
|
+
const { auth, baseUrl, browser, brain, maxRetries, snapshot = defaultSnapshot, onEvent, mask } = options;
|
|
555
570
|
const steps = auth.steps;
|
|
556
|
-
const mask = new SecretsMask();
|
|
557
|
-
for (const step of steps) mask.registerFrom(step);
|
|
558
571
|
const context = await browser.newContext();
|
|
559
572
|
try {
|
|
560
573
|
const page = await context.newPage();
|
|
@@ -587,7 +600,7 @@ async function fromSteps(options) {
|
|
|
587
600
|
);
|
|
588
601
|
}
|
|
589
602
|
if (auth.verify) {
|
|
590
|
-
const judgment = await brain.judge(auth.verify, await snapshot(page));
|
|
603
|
+
const judgment = await brain.judge(auth.verify, mask.mask(await snapshot(page)));
|
|
591
604
|
if (!judgment.pass) {
|
|
592
605
|
throw new AuthError(`Authentication could not be verified: ${mask.mask(judgment.reason)}`);
|
|
593
606
|
}
|
|
@@ -620,7 +633,7 @@ async function authenticate(options) {
|
|
|
620
633
|
return fromStorageState(path3.resolve(cwd, auth.storage_state));
|
|
621
634
|
}
|
|
622
635
|
if (auth.headers || auth.cookies) {
|
|
623
|
-
return fromStatic(auth)
|
|
636
|
+
return fromStatic(auth, options.mask);
|
|
624
637
|
}
|
|
625
638
|
const session = await fromSteps(options);
|
|
626
639
|
if (auth.cache) await writeCached(cwd, session);
|
|
@@ -1164,7 +1177,7 @@ function renderTestYaml(draft, meta) {
|
|
|
1164
1177
|
});
|
|
1165
1178
|
}
|
|
1166
1179
|
async function generateForRoute(page, options) {
|
|
1167
|
-
const { route, baseUrl, changedFiles, brain, snapshot = defaultSnapshot } = options;
|
|
1180
|
+
const { route, baseUrl, changedFiles, brain, mask, snapshot = defaultSnapshot } = options;
|
|
1168
1181
|
const url = new URL(route, baseUrl).toString();
|
|
1169
1182
|
try {
|
|
1170
1183
|
await page.goto(url, { timeout: 3e4 });
|
|
@@ -1173,7 +1186,11 @@ async function generateForRoute(page, options) {
|
|
|
1173
1186
|
`Cannot load ${url}: ${error instanceof Error ? error.message : String(error)}`
|
|
1174
1187
|
);
|
|
1175
1188
|
}
|
|
1176
|
-
const generated = await brain.planTest({
|
|
1189
|
+
const generated = await brain.planTest({
|
|
1190
|
+
route,
|
|
1191
|
+
snapshot: mask(await snapshot(page)),
|
|
1192
|
+
changedFiles
|
|
1193
|
+
});
|
|
1177
1194
|
const leaked = findSecretLiterals(generated.steps);
|
|
1178
1195
|
if (leaked.length > 0) {
|
|
1179
1196
|
throw new PlannerError(
|
|
@@ -1489,12 +1506,21 @@ function applyUrlOverride(config, url) {
|
|
|
1489
1506
|
}
|
|
1490
1507
|
return { ...config, base_url: url };
|
|
1491
1508
|
}
|
|
1492
|
-
function
|
|
1509
|
+
function buildRunMask(config, tests) {
|
|
1493
1510
|
const mask = new SecretsMask();
|
|
1511
|
+
for (const step of config.auth?.steps ?? []) mask.registerFrom(step);
|
|
1512
|
+
for (const value of Object.values(config.auth?.headers ?? {})) mask.registerFrom(value);
|
|
1513
|
+
for (const cookie of config.auth?.cookies ?? []) mask.registerFrom(cookie.value);
|
|
1514
|
+
for (const test of tests) {
|
|
1515
|
+
for (const step of [...test.setup ?? [], ...test.steps]) mask.registerFrom(step);
|
|
1516
|
+
}
|
|
1517
|
+
return mask;
|
|
1518
|
+
}
|
|
1519
|
+
function resolveSecretsAndSteps(test) {
|
|
1494
1520
|
for (const step of [...test.setup ?? [], ...test.steps]) {
|
|
1495
|
-
|
|
1521
|
+
new SecretsMask().registerFrom(step);
|
|
1496
1522
|
}
|
|
1497
|
-
return { test
|
|
1523
|
+
return { test };
|
|
1498
1524
|
}
|
|
1499
1525
|
function printEvent(event) {
|
|
1500
1526
|
switch (event.type) {
|
|
@@ -1542,12 +1568,12 @@ X ${r.summary} (${r.file})`);
|
|
|
1542
1568
|
if (r.screenshot) console.log(` screenshot: ${r.screenshot}`);
|
|
1543
1569
|
}
|
|
1544
1570
|
}
|
|
1545
|
-
async function runOne(browser, test, config, sessionDir, session) {
|
|
1571
|
+
async function runOne(browser, test, config, sessionDir, session, runMask) {
|
|
1546
1572
|
const brain = createBrain(createModel(config.llm).model);
|
|
1547
1573
|
let resolved;
|
|
1548
|
-
|
|
1574
|
+
const mask = runMask;
|
|
1549
1575
|
try {
|
|
1550
|
-
({ test: resolved
|
|
1576
|
+
({ test: resolved } = resolveSecretsAndSteps(test));
|
|
1551
1577
|
} catch (error) {
|
|
1552
1578
|
if (error instanceof MissingEnvError) {
|
|
1553
1579
|
return {
|
|
@@ -1627,22 +1653,24 @@ async function finalize(results, skipped, options, sessionDir, durationMs, impac
|
|
|
1627
1653
|
await writeHtml(target, html);
|
|
1628
1654
|
console.log(`HTML report: ${path9.relative(options.cwd, target)}`);
|
|
1629
1655
|
}
|
|
1630
|
-
|
|
1631
|
-
if (unclassified.length > 0) {
|
|
1632
|
-
console.error(
|
|
1633
|
-
`error: ${unclassified.length} changed file(s) match no routes: or ignore: glob, so their blast radius is unknown:`
|
|
1634
|
-
);
|
|
1635
|
-
for (const file of unclassified) console.error(` ${file}`);
|
|
1636
|
-
console.error(
|
|
1637
|
-
"Map them in routes: if they can affect a page, or list them in ignore: if they cannot."
|
|
1638
|
-
);
|
|
1639
|
-
return EXIT_FAILED;
|
|
1640
|
-
}
|
|
1656
|
+
if (reportUnclassified(options, impact)) return EXIT_FAILED;
|
|
1641
1657
|
if (options.minScore !== void 0) {
|
|
1642
1658
|
return score >= options.minScore ? EXIT_OK : EXIT_FAILED;
|
|
1643
1659
|
}
|
|
1644
1660
|
return results.some((result) => result.status === "failed") ? EXIT_FAILED : EXIT_OK;
|
|
1645
1661
|
}
|
|
1662
|
+
function reportUnclassified(options, impact) {
|
|
1663
|
+
const unclassified = options.failOnUnmapped ? impact?.unmappedFiles ?? [] : [];
|
|
1664
|
+
if (unclassified.length === 0) return false;
|
|
1665
|
+
console.error(
|
|
1666
|
+
`error: ${unclassified.length} changed file(s) match no routes: or ignore: glob, so their blast radius is unknown:`
|
|
1667
|
+
);
|
|
1668
|
+
for (const file of unclassified) console.error(` ${file}`);
|
|
1669
|
+
console.error(
|
|
1670
|
+
"Map them in routes: if they can affect a page, or list them in ignore: if they cannot."
|
|
1671
|
+
);
|
|
1672
|
+
return true;
|
|
1673
|
+
}
|
|
1646
1674
|
function printDryRun(selected, baseUrl, cwd) {
|
|
1647
1675
|
console.log(`
|
|
1648
1676
|
Dry run: ${selected.length} test(s) selected, base_url=${baseUrl}`);
|
|
@@ -1664,6 +1692,12 @@ async function runCommand(options) {
|
|
|
1664
1692
|
}
|
|
1665
1693
|
const impacted = options.impacted ?? false;
|
|
1666
1694
|
const base = options.base ?? "main";
|
|
1695
|
+
if (options.failOnUnmapped && !impacted) {
|
|
1696
|
+
console.error(
|
|
1697
|
+
"error: --fail-on-unmapped classifies the files in a diff, so it requires --impacted."
|
|
1698
|
+
);
|
|
1699
|
+
return EXIT_USAGE;
|
|
1700
|
+
}
|
|
1667
1701
|
let impact;
|
|
1668
1702
|
if (impacted) {
|
|
1669
1703
|
let changedFiles;
|
|
@@ -1719,7 +1753,13 @@ async function runCommand(options) {
|
|
|
1719
1753
|
}
|
|
1720
1754
|
if (options.dryRun) {
|
|
1721
1755
|
printDryRun(selected, config.base_url, options.cwd);
|
|
1722
|
-
|
|
1756
|
+
if (results.length > 0) {
|
|
1757
|
+
console.error(`
|
|
1758
|
+
${results.length} test file(s) could not be parsed:`);
|
|
1759
|
+
for (const broken of results) console.error(` ${broken.file}: ${broken.reason ?? "invalid"}`);
|
|
1760
|
+
return EXIT_FAILED;
|
|
1761
|
+
}
|
|
1762
|
+
return reportUnclassified(options, impact) ? EXIT_FAILED : EXIT_OK;
|
|
1723
1763
|
}
|
|
1724
1764
|
const sessionDir = path9.join(options.cwd, ".blastproof", "reports", sessionId());
|
|
1725
1765
|
const startedAt = Date.now();
|
|
@@ -1742,6 +1782,7 @@ async function runCommand(options) {
|
|
|
1742
1782
|
console.log(
|
|
1743
1783
|
`blastproof run: ${selected.length} test(s), provider=${provider} model=${modelId}, base_url=${config.base_url}`
|
|
1744
1784
|
);
|
|
1785
|
+
const runMask = buildRunMask(config, parsed);
|
|
1745
1786
|
const browser = await chromium.launch({ headless: config.browser.headless });
|
|
1746
1787
|
try {
|
|
1747
1788
|
let session;
|
|
@@ -1756,6 +1797,7 @@ async function runCommand(options) {
|
|
|
1756
1797
|
browser,
|
|
1757
1798
|
brain: createBrain(createModel(config.llm).model),
|
|
1758
1799
|
maxRetries: config.max_retries_per_step,
|
|
1800
|
+
mask: runMask,
|
|
1759
1801
|
onEvent: printEvent
|
|
1760
1802
|
});
|
|
1761
1803
|
} catch (error) {
|
|
@@ -1769,7 +1811,7 @@ async function runCommand(options) {
|
|
|
1769
1811
|
for (const test of selected) {
|
|
1770
1812
|
console.log(`
|
|
1771
1813
|
> ${test.summary} [${test.priority}] (${path9.relative(options.cwd, test.path)})`);
|
|
1772
|
-
results.push(await runOne(browser, test, config, sessionDir, session));
|
|
1814
|
+
results.push(await runOne(browser, test, config, sessionDir, session, runMask));
|
|
1773
1815
|
}
|
|
1774
1816
|
} finally {
|
|
1775
1817
|
await browser.close();
|
|
@@ -1874,6 +1916,13 @@ async function planCommand(options) {
|
|
|
1874
1916
|
console.log(
|
|
1875
1917
|
`blastproof plan: ${work.length} route(s), provider=${provider} model=${modelId}, base_url=${config.base_url}`
|
|
1876
1918
|
);
|
|
1919
|
+
const mask = new SecretsMask();
|
|
1920
|
+
for (const step of config.auth?.steps ?? []) mask.registerFrom(step);
|
|
1921
|
+
for (const value of Object.values(config.auth?.headers ?? {})) mask.registerFrom(value);
|
|
1922
|
+
for (const cookie of config.auth?.cookies ?? []) mask.registerFrom(cookie.value);
|
|
1923
|
+
for (const test of suite) {
|
|
1924
|
+
for (const step of [...test.setup ?? [], ...test.steps]) mask.registerFrom(step);
|
|
1925
|
+
}
|
|
1877
1926
|
const generated = [];
|
|
1878
1927
|
const written = [];
|
|
1879
1928
|
const failed = [];
|
|
@@ -1891,6 +1940,7 @@ async function planCommand(options) {
|
|
|
1891
1940
|
browser,
|
|
1892
1941
|
brain: createBrain(createModel(config.llm).model),
|
|
1893
1942
|
maxRetries: config.max_retries_per_step,
|
|
1943
|
+
mask,
|
|
1894
1944
|
onEvent: printAuthEvent
|
|
1895
1945
|
});
|
|
1896
1946
|
} catch (error) {
|
|
@@ -1913,7 +1963,8 @@ async function planCommand(options) {
|
|
|
1913
1963
|
route,
|
|
1914
1964
|
baseUrl: config.base_url,
|
|
1915
1965
|
changedFiles,
|
|
1916
|
-
brain
|
|
1966
|
+
brain,
|
|
1967
|
+
mask: (text) => mask.mask(text)
|
|
1917
1968
|
});
|
|
1918
1969
|
} catch (error) {
|
|
1919
1970
|
failed.push({ route, reason: error instanceof Error ? error.message : String(error) });
|
|
@@ -2013,7 +2064,7 @@ function parseMinScore(value) {
|
|
|
2013
2064
|
return score;
|
|
2014
2065
|
}
|
|
2015
2066
|
var program = new Command();
|
|
2016
|
-
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.2.
|
|
2067
|
+
program.name("blastproof").description("Open-source AI testing agent: plain-English YAML tests executed agentically on a real browser.").version("0.2.2");
|
|
2017
2068
|
program.command("init").description("Scaffold .blastproof/ (config, tests, sample tests) in the current directory").action(async () => {
|
|
2018
2069
|
try {
|
|
2019
2070
|
const result = await initProject(process.cwd());
|