bangonit 0.5.0 → 0.5.4
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 +13 -2
- package/app/desktopapp/dist/main/index.js +14 -10
- package/app/desktopapp/dist/shared/args.js +1 -0
- package/app/replay/dist/replay.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/app-build-manifest.json +7 -7
- package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +3 -3
- package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/required-server-files.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js.nft.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js.nft.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page.js +6 -6
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page_client-reference-manifest.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +2 -2
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/page.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/app/page_client-reference-manifest.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/{708.js → 151.js} +9 -7
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/679.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/middleware-build-manifest.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/pages-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/793-be37f231ca512496.js +36 -0
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-4c6dfbf12230aab7.js +1 -0
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +1 -0
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/{main-app-76384b941f0b51cb.js → main-app-106dd83f859b9dfa.js} +1 -1
- package/app/webapp/.next/standalone/app/webapp/package.json +1 -0
- package/app/webapp/.next/standalone/app/webapp/server.js +1 -1
- package/app/webapp/.next/standalone/package.json +2 -2
- package/app/webapp/.next/static/chunks/793-be37f231ca512496.js +36 -0
- package/app/webapp/.next/static/chunks/app/app/page-4c6dfbf12230aab7.js +1 -0
- package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +1 -0
- package/app/webapp/.next/static/chunks/{main-app-76384b941f0b51cb.js → main-app-106dd83f859b9dfa.js} +1 -1
- package/app/webapp/package.json +1 -0
- package/app/webapp/src/shared/api/chat.ts +13 -2
- package/app/webapp/src/shared/components/AppShell.tsx +15 -5
- package/app/webapp/src/shared/components/SessionView.tsx +10 -2
- package/app/webapp/src/shared/lib/browser/index.ts +2 -2
- package/app/webapp/src/shared/lib/browser/screenshot.ts +2 -2
- package/app/webapp/src/shared/lib/browser/snapshot.ts +107 -7
- package/app/webapp/src/shared/lib/browser/types.ts +1 -0
- package/bin/app/desktopapp/src/shared/args.js +1 -0
- package/bin/src/cli/bangonit.js +259 -194
- package/package.json +2 -2
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/631-0edca788fa58159b.js +0 -36
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +0 -1
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +0 -1
- package/app/webapp/.next/static/chunks/631-0edca788fa58159b.js +0 -36
- package/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +0 -1
- package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +0 -1
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_buildManifest.js +0 -0
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_ssgManifest.js +0 -0
- /package/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_buildManifest.js +0 -0
- /package/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_ssgManifest.js +0 -0
package/bin/src/cli/bangonit.js
CHANGED
|
@@ -44,7 +44,6 @@ const fs = __importStar(require("fs"));
|
|
|
44
44
|
const net = __importStar(require("net"));
|
|
45
45
|
const readline = __importStar(require("readline"));
|
|
46
46
|
const TOML = __importStar(require("@iarna/toml"));
|
|
47
|
-
const Minio = __importStar(require("minio"));
|
|
48
47
|
const yargs_1 = __importDefault(require("yargs"));
|
|
49
48
|
const helpers_1 = require("yargs/helpers");
|
|
50
49
|
const is_ci_1 = __importDefault(require("is-ci"));
|
|
@@ -183,50 +182,6 @@ function findTestPlans(dir, filter) {
|
|
|
183
182
|
}
|
|
184
183
|
return results;
|
|
185
184
|
}
|
|
186
|
-
function createS3Client(opts) {
|
|
187
|
-
const accessKey = opts.accessKey || process.env.AWS_ACCESS_KEY_ID || "";
|
|
188
|
-
const secretKey = opts.secretKey || process.env.AWS_SECRET_ACCESS_KEY || "";
|
|
189
|
-
const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
|
|
190
|
-
if (opts.endpoint) {
|
|
191
|
-
const useSSL = !opts.endpoint.startsWith("http://");
|
|
192
|
-
const endPoint = opts.endpoint.replace(/^https?:\/\//, "");
|
|
193
|
-
return new Minio.Client({ endPoint, useSSL, accessKey, secretKey, region });
|
|
194
|
-
}
|
|
195
|
-
return new Minio.Client({
|
|
196
|
-
endPoint: "s3.amazonaws.com",
|
|
197
|
-
useSSL: true,
|
|
198
|
-
accessKey,
|
|
199
|
-
secretKey,
|
|
200
|
-
region,
|
|
201
|
-
});
|
|
202
|
-
}
|
|
203
|
-
async function uploadDir(client, localDir, bucket, prefix) {
|
|
204
|
-
const entries = fs.readdirSync(localDir, { withFileTypes: true });
|
|
205
|
-
for (const entry of entries) {
|
|
206
|
-
const fullPath = path.join(localDir, entry.name);
|
|
207
|
-
const objectName = `${prefix}/${entry.name}`;
|
|
208
|
-
if (entry.isDirectory()) {
|
|
209
|
-
await uploadDir(client, fullPath, bucket, objectName);
|
|
210
|
-
}
|
|
211
|
-
else {
|
|
212
|
-
await client.fPutObject(bucket, objectName, fullPath, {});
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
async function uploadToS3(localDir, opts) {
|
|
217
|
-
const client = createS3Client(opts);
|
|
218
|
-
await uploadDir(client, localDir, opts.bucket, opts.prefix);
|
|
219
|
-
const region = opts.region || process.env.AWS_REGION || process.env.AWS_DEFAULT_REGION || "us-east-1";
|
|
220
|
-
if (opts.endpoint) {
|
|
221
|
-
const proto = opts.endpoint.startsWith("http://") ? "http" : "https";
|
|
222
|
-
const host = opts.endpoint.replace(/^https?:\/\//, "");
|
|
223
|
-
return `${proto}://${opts.bucket}.${host}/${opts.prefix}/index.html`;
|
|
224
|
-
}
|
|
225
|
-
if (region === "us-east-1") {
|
|
226
|
-
return `https://${opts.bucket}.s3.amazonaws.com/${opts.prefix}/index.html`;
|
|
227
|
-
}
|
|
228
|
-
return `https://${opts.bucket}.s3.${region}.amazonaws.com/${opts.prefix}/index.html`;
|
|
229
|
-
}
|
|
230
185
|
function createPrompter() {
|
|
231
186
|
let closed = false;
|
|
232
187
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
@@ -261,63 +216,40 @@ function createPrompter() {
|
|
|
261
216
|
};
|
|
262
217
|
}
|
|
263
218
|
// --- init command ---
|
|
264
|
-
// Common timezone offsets from UTC (standard time)
|
|
265
|
-
const TIMEZONE_OFFSETS = {
|
|
266
|
-
"us/eastern": -5,
|
|
267
|
-
"us/central": -6,
|
|
268
|
-
"us/mountain": -7,
|
|
269
|
-
"us/pacific": -8,
|
|
270
|
-
"europe/london": 0,
|
|
271
|
-
"europe/berlin": 1,
|
|
272
|
-
"europe/paris": 1,
|
|
273
|
-
"asia/tokyo": 9,
|
|
274
|
-
"asia/shanghai": 8,
|
|
275
|
-
"asia/kolkata": 5,
|
|
276
|
-
"australia/sydney": 11,
|
|
277
|
-
};
|
|
278
|
-
function localHourToUtc(localHour, utcOffset) {
|
|
279
|
-
return (((localHour - utcOffset) % 24) + 24) % 24;
|
|
280
|
-
}
|
|
281
219
|
async function initProject() {
|
|
282
220
|
const p = createPrompter();
|
|
283
221
|
console.log(`\n ${c.bold}${c.magenta}Bang On It! init${c.reset}\n`);
|
|
284
222
|
const testplans = await p.ask("Test plans directory", "testplans");
|
|
285
|
-
const apiKey = await p.ask("Anthropic API key (empty to use env var)", "");
|
|
286
223
|
const recordingsDir = await p.ask("Recordings directory", "recordings");
|
|
287
|
-
const s3Bucket = await p.ask("S3 bucket for recordings (empty to skip)", "");
|
|
288
|
-
let s3Endpoint = "";
|
|
289
|
-
let s3Region = "";
|
|
290
|
-
let s3Prefix = "";
|
|
291
|
-
if (s3Bucket) {
|
|
292
|
-
s3Endpoint = await p.ask("S3 endpoint (empty for AWS, or e.g. nyc3.digitaloceanspaces.com)", "");
|
|
293
|
-
s3Region = await p.ask("S3 region", "us-east-1");
|
|
294
|
-
s3Prefix = await p.ask("S3 prefix", "bangonit");
|
|
295
|
-
}
|
|
296
224
|
// --- Config file ---
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
toml += `\n[s3]\nbucket = "${s3Bucket}"\n`;
|
|
307
|
-
if (s3Endpoint)
|
|
308
|
-
toml += `endpoint = "${s3Endpoint}"\n`;
|
|
309
|
-
if (s3Region && s3Region !== "us-east-1")
|
|
310
|
-
toml += `region = "${s3Region}"\n`;
|
|
311
|
-
if (s3Prefix && s3Prefix !== "bangonit")
|
|
312
|
-
toml += `prefix = "${s3Prefix}"\n`;
|
|
313
|
-
toml += `# access_key = "\${AWS_ACCESS_KEY_ID}"\n`;
|
|
314
|
-
toml += `# secret_key = "\${AWS_SECRET_ACCESS_KEY}"\n`;
|
|
315
|
-
}
|
|
225
|
+
const toml = `# Bang On It! configuration
|
|
226
|
+
# Docs: https://bangonit.dev/docs/config
|
|
227
|
+
|
|
228
|
+
testplans = "${testplans}"
|
|
229
|
+
recordings_dir = "${recordingsDir}"
|
|
230
|
+
|
|
231
|
+
# EDIT: Uncomment and set your Anthropic API key, or set ANTHROPIC_API_KEY env var
|
|
232
|
+
# anthropic_api_key = "sk-ant-..."
|
|
233
|
+
`;
|
|
316
234
|
const bangDir = path.join(process.cwd(), ".bangonit");
|
|
317
235
|
fs.mkdirSync(bangDir, { recursive: true });
|
|
318
236
|
const configOutPath = path.join(bangDir, "config.toml");
|
|
319
237
|
fs.writeFileSync(configOutPath, toml);
|
|
320
238
|
console.log(`\n ${c.green}Created${c.reset} .bangonit/config.toml`);
|
|
239
|
+
// --- System prompt script ---
|
|
240
|
+
const systemPromptPath = path.join(bangDir, "system_prompt.sh");
|
|
241
|
+
if (!fs.existsSync(systemPromptPath)) {
|
|
242
|
+
fs.writeFileSync(systemPromptPath, `#!/bin/bash
|
|
243
|
+
# This script is executed before each test run.
|
|
244
|
+
# Its stdout becomes the project-level system prompt.
|
|
245
|
+
# Environment variables are available for interpolation.
|
|
246
|
+
|
|
247
|
+
# Example:
|
|
248
|
+
# echo "The app is running on http://localhost:\${DEV_SERVER_PORT}"
|
|
249
|
+
`);
|
|
250
|
+
fs.chmodSync(systemPromptPath, 0o755);
|
|
251
|
+
console.log(` ${c.green}Created${c.reset} .bangonit/system_prompt.sh`);
|
|
252
|
+
}
|
|
321
253
|
// --- Test plan directories ---
|
|
322
254
|
const testplanBase = path.join(process.cwd(), testplans);
|
|
323
255
|
const dirs = ["smoke", "acceptance", "regression"];
|
|
@@ -329,26 +261,12 @@ async function initProject() {
|
|
|
329
261
|
fs.writeFileSync(gitkeep, "");
|
|
330
262
|
console.log(` ${c.green}Created${c.reset} ${testplans}/${dir}/`);
|
|
331
263
|
}
|
|
332
|
-
// Write example smoke test
|
|
333
|
-
const smokeExample = path.join(testplanBase, "smoke", "homepage.md");
|
|
334
|
-
if (!fs.existsSync(smokeExample)) {
|
|
335
|
-
fs.writeFileSync(smokeExample, `---
|
|
336
|
-
name: Homepage loads
|
|
337
|
-
---
|
|
338
|
-
|
|
339
|
-
## Steps
|
|
340
|
-
1. Navigate to http://localhost:3000
|
|
341
|
-
2. Verify the page loads with a heading
|
|
342
|
-
3. Verify there are no console errors
|
|
343
|
-
`);
|
|
344
|
-
console.log(` ${c.green}Created${c.reset} ${testplans}/smoke/homepage.md`);
|
|
345
|
-
}
|
|
346
264
|
// --- Claude Code skills ---
|
|
347
265
|
const claudeSkillsDir = path.join(process.cwd(), ".claude", "skills");
|
|
348
|
-
const testSkillDir = path.join(claudeSkillsDir, "test");
|
|
266
|
+
const testSkillDir = path.join(claudeSkillsDir, "boi-test");
|
|
349
267
|
fs.mkdirSync(testSkillDir, { recursive: true });
|
|
350
268
|
fs.writeFileSync(path.join(testSkillDir, "SKILL.md"), `---
|
|
351
|
-
name: test
|
|
269
|
+
name: boi-test
|
|
352
270
|
description: Run Bang On It! E2E tests locally. Pass test plan files or a filter as $ARGUMENTS (e.g. "testplans/smoke/" or "-t login"). With no arguments, runs all test plans.
|
|
353
271
|
tools: Bash, Read
|
|
354
272
|
---
|
|
@@ -380,11 +298,11 @@ Run Bang On It! end-to-end tests locally.
|
|
|
380
298
|
|
|
381
299
|
5. If tests fail, read the test plan file and the output to diagnose the failure. Suggest whether the test plan needs updating or there's a real bug.
|
|
382
300
|
`);
|
|
383
|
-
console.log(` ${c.green}Created${c.reset} .claude/skills/test/SKILL.md`);
|
|
384
|
-
const createTestSkillDir = path.join(claudeSkillsDir, "create-test");
|
|
301
|
+
console.log(` ${c.green}Created${c.reset} .claude/skills/boi-test/SKILL.md`);
|
|
302
|
+
const createTestSkillDir = path.join(claudeSkillsDir, "boi-create-test");
|
|
385
303
|
fs.mkdirSync(createTestSkillDir, { recursive: true });
|
|
386
304
|
fs.writeFileSync(path.join(createTestSkillDir, "SKILL.md"), `---
|
|
387
|
-
name: create-test
|
|
305
|
+
name: boi-create-test
|
|
388
306
|
description: Create new Bang On It! test plan(s). Pass a description of what to test as $ARGUMENTS, or omit to auto-generate from git changes.
|
|
389
307
|
tools: Bash, Read, Write, Glob, Grep
|
|
390
308
|
---
|
|
@@ -439,67 +357,170 @@ Create new Bang On It! test plan files.
|
|
|
439
357
|
|
|
440
358
|
6. Output the path to the created file.
|
|
441
359
|
`);
|
|
442
|
-
console.log(` ${c.green}Created${c.reset} .claude/skills/create-test/SKILL.md`);
|
|
360
|
+
console.log(` ${c.green}Created${c.reset} .claude/skills/boi-create-test/SKILL.md`);
|
|
443
361
|
// --- CI setup ---
|
|
444
362
|
console.log("");
|
|
445
|
-
const setupCi = await p.askChoice("Set up GitHub Actions
|
|
363
|
+
const setupCi = await p.askChoice("Set up GitHub Actions?", ["y", "n"], "y");
|
|
446
364
|
if (setupCi === "y") {
|
|
447
|
-
|
|
448
|
-
const
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
365
|
+
// Build the comment shell script once, reused by both workflows
|
|
366
|
+
const commentScript = `OUTPUT="\${{ github.workspace }}/bangonit-output.json"
|
|
367
|
+
if [ ! -f "$OUTPUT" ]; then
|
|
368
|
+
BODY="## \${{ github.workflow }} — ❌ Failed\\n\\nNo test output was produced. Check the [workflow logs](\${{ github.server_url }}/\${{ github.repository }}/actions/runs/\${{ github.run_id }})."
|
|
369
|
+
else
|
|
370
|
+
STATUS=$(jq -r '.status' "$OUTPUT")
|
|
371
|
+
if [ "$STATUS" = "pass" ]; then
|
|
372
|
+
HEADER="## \${{ github.workflow }} — ✅ Passed"
|
|
373
|
+
else
|
|
374
|
+
HEADER="## \${{ github.workflow }} — ❌ Failed"
|
|
375
|
+
fi
|
|
376
|
+
|
|
377
|
+
TABLE="| Test | Status | Duration | Recording |\\n|------|--------|----------|-----------|"
|
|
378
|
+
for row in $(jq -r '.tests[] | @base64' "$OUTPUT"); do
|
|
379
|
+
NAME=$(echo "$row" | base64 -d | jq -r '.name')
|
|
380
|
+
TEST_STATUS=$(echo "$row" | base64 -d | jq -r '.status')
|
|
381
|
+
DURATION=$(echo "$row" | base64 -d | jq -r '(.duration / 1000 * 10 | round / 10)')
|
|
382
|
+
if [ "$TEST_STATUS" = "pass" ]; then
|
|
383
|
+
EMOJI="✅"
|
|
384
|
+
else
|
|
385
|
+
EMOJI="❌"
|
|
386
|
+
fi
|
|
387
|
+
RECORDING=""
|
|
388
|
+
RECORDING_URL=$(jq -r --arg name "$NAME" '.recordings[]? | select(.name | contains($name)) | .url // empty' "$OUTPUT" 2>/dev/null || true)
|
|
389
|
+
if [ -z "$RECORDING_URL" ] && [ -n "$BANGONIT_S3_BASE_URL" ] && [ -d "recordings" ]; then
|
|
390
|
+
for rdir in recordings/*/index.html; do
|
|
391
|
+
if [ -f "$rdir" ]; then
|
|
392
|
+
RNAME=$(basename "$(dirname "$rdir")")
|
|
393
|
+
RECORDING_URL="$BANGONIT_S3_BASE_URL/$RNAME/index.html"
|
|
394
|
+
break
|
|
395
|
+
fi
|
|
396
|
+
done
|
|
397
|
+
fi
|
|
398
|
+
if [ -n "$RECORDING_URL" ]; then
|
|
399
|
+
RECORDING="[View recording]($RECORDING_URL)"
|
|
400
|
+
fi
|
|
401
|
+
TABLE="$TABLE\\n| $NAME | $EMOJI $TEST_STATUS | \${DURATION}s | $RECORDING |"
|
|
402
|
+
done
|
|
403
|
+
|
|
404
|
+
BODY="$HEADER\\n\\n$TABLE"
|
|
405
|
+
fi`;
|
|
406
|
+
const smokeWorkflow = `# Bang On It! — Smoke Tests
|
|
407
|
+
# Runs on every push to main/master and on pull requests.
|
|
408
|
+
#
|
|
409
|
+
# EDIT: Review the steps below and adjust for your project:
|
|
410
|
+
# - node-version: set to your project's Node.js version
|
|
411
|
+
# - "Setup project": your install/build commands
|
|
412
|
+
# - "Start server": command to start your app in the background
|
|
413
|
+
# - "Wait for server": URL your app serves on
|
|
414
|
+
# - "Run smoke tests": timeout and test plan path
|
|
415
|
+
#
|
|
416
|
+
# REQUIRED SECRET:
|
|
417
|
+
# ANTHROPIC_API_KEY — your Anthropic API key
|
|
418
|
+
# Set at: https://github.com/<owner>/<repo>/settings/secrets/actions
|
|
419
|
+
#
|
|
420
|
+
# OPTIONAL — to upload recordings to S3 and link them in PR comments:
|
|
421
|
+
# 1. Add secrets: AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY
|
|
422
|
+
# 2. Uncomment the "Upload recordings to S3" step below
|
|
423
|
+
# 3. Set BANGONIT_S3_BASE_URL in the env section
|
|
424
|
+
|
|
425
|
+
name: "Bang On It! Smoke Tests"
|
|
426
|
+
|
|
427
|
+
on:
|
|
428
|
+
push:
|
|
429
|
+
branches: [main, master]
|
|
430
|
+
pull_request:
|
|
431
|
+
|
|
432
|
+
permissions:
|
|
433
|
+
contents: write
|
|
434
|
+
pull-requests: write
|
|
435
|
+
|
|
436
|
+
jobs:
|
|
437
|
+
smoke:
|
|
438
|
+
runs-on: ubuntu-latest
|
|
439
|
+
timeout-minutes: 10
|
|
440
|
+
env:
|
|
441
|
+
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
442
|
+
# EDIT: Uncomment for S3 recording uploads
|
|
443
|
+
# AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
444
|
+
# AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
445
|
+
# BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
|
|
446
|
+
|
|
460
447
|
steps:
|
|
461
448
|
- uses: actions/checkout@v4
|
|
462
449
|
|
|
463
450
|
- uses: actions/setup-node@v4
|
|
464
451
|
with:
|
|
465
|
-
node-version: '
|
|
452
|
+
node-version: '24' # EDIT: your Node.js version
|
|
466
453
|
|
|
467
454
|
- name: Install system dependencies
|
|
468
455
|
run: |
|
|
469
456
|
sudo apt-get update
|
|
470
|
-
sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2
|
|
457
|
+
sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64
|
|
471
458
|
|
|
472
459
|
- name: Setup project
|
|
473
|
-
run:
|
|
460
|
+
run: npm install && npm run build # EDIT: your install/build commands
|
|
474
461
|
|
|
475
462
|
- name: Start server
|
|
476
|
-
run:
|
|
463
|
+
run: npm start & # EDIT: command to start your web server
|
|
477
464
|
|
|
478
465
|
- name: Wait for server
|
|
479
|
-
run: npx wait-on
|
|
466
|
+
run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
|
|
480
467
|
|
|
481
468
|
- name: Install bangonit
|
|
482
|
-
run: npm install -g bangonit
|
|
483
|
-
const smokeWorkflow = `name: "Bang On It! Smoke Tests"
|
|
484
|
-
|
|
485
|
-
on:
|
|
486
|
-
push:
|
|
487
|
-
branches: [main, master]
|
|
488
|
-
pull_request:
|
|
469
|
+
run: npm install -g bangonit
|
|
489
470
|
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
$
|
|
471
|
+
- name: Comment test starting
|
|
472
|
+
id: start-comment
|
|
473
|
+
if: github.event_name == 'pull_request'
|
|
474
|
+
run: |
|
|
475
|
+
COMMENT_ID=$(gh api repos/\${{ github.repository }}/issues/\${{ github.event.pull_request.number }}/comments \\
|
|
476
|
+
--method POST --field "body=🧪 **Bang On It!** smoke tests starting..." --jq '.id')
|
|
477
|
+
echo "comment_id=$COMMENT_ID" >> "$GITHUB_OUTPUT"
|
|
478
|
+
env:
|
|
479
|
+
GH_TOKEN: \${{ github.token }}
|
|
497
480
|
|
|
498
481
|
- name: Run smoke tests
|
|
499
482
|
run: |
|
|
500
|
-
xvfb-run --auto-servernum boi run ${testplans}/smoke
|
|
501
|
-
--timeout
|
|
502
|
-
--output bangonit-output.json --record
|
|
483
|
+
xvfb-run --auto-servernum boi run ${testplans}/smoke/*.md \\
|
|
484
|
+
--timeout 300 \\
|
|
485
|
+
--output \${{ github.workspace }}/bangonit-output.json --record
|
|
486
|
+
|
|
487
|
+
# EDIT: Uncomment to upload recordings to S3-compatible storage.
|
|
488
|
+
# Requires AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY secrets.
|
|
489
|
+
# - name: Upload recordings to S3
|
|
490
|
+
# if: always()
|
|
491
|
+
# run: |
|
|
492
|
+
# if [ -d recordings ]; then
|
|
493
|
+
# for dir in recordings/*/; do
|
|
494
|
+
# if [ -d "$dir" ]; then
|
|
495
|
+
# DIRNAME=$(basename "$dir")
|
|
496
|
+
# aws s3 sync "$dir" "s3://MY-BUCKET/bangonit/$DIRNAME/" \\
|
|
497
|
+
# --endpoint-url https://s3.us-east-1.amazonaws.com --no-progress
|
|
498
|
+
# aws s3api list-objects-v2 --bucket MY-BUCKET --prefix "bangonit/$DIRNAME/" \\
|
|
499
|
+
# --endpoint-url https://s3.us-east-1.amazonaws.com --query 'Contents[].Key' --output text | tr '\\t' '\\n' | while read -r key; do
|
|
500
|
+
# aws s3api put-object-acl --bucket MY-BUCKET --key "$key" --acl public-read \\
|
|
501
|
+
# --endpoint-url https://s3.us-east-1.amazonaws.com
|
|
502
|
+
# done
|
|
503
|
+
# fi
|
|
504
|
+
# done
|
|
505
|
+
# fi
|
|
506
|
+
|
|
507
|
+
- name: Comment test results
|
|
508
|
+
if: always()
|
|
509
|
+
run: |
|
|
510
|
+
${commentScript}
|
|
511
|
+
|
|
512
|
+
if [ "\${{ github.event_name }}" = "pull_request" ] && [ -n "\${{ steps.start-comment.outputs.comment_id }}" ]; then
|
|
513
|
+
printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/issues/comments/\${{ steps.start-comment.outputs.comment_id }} \\
|
|
514
|
+
--method PATCH --field "body=@-"
|
|
515
|
+
elif [ "\${{ github.event_name }}" = "pull_request" ]; then
|
|
516
|
+
printf "%b" "$BODY" | gh pr comment \${{ github.event.pull_request.number }} \\
|
|
517
|
+
--repo \${{ github.repository }} --body-file -
|
|
518
|
+
else
|
|
519
|
+
printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/commits/\${{ github.sha }}/comments \\
|
|
520
|
+
--method POST --field "body=@-"
|
|
521
|
+
fi
|
|
522
|
+
env:
|
|
523
|
+
GH_TOKEN: \${{ github.token }}
|
|
503
524
|
|
|
504
525
|
- name: Upload test results
|
|
505
526
|
if: always()
|
|
@@ -507,30 +528,82 @@ ${stepsYaml}
|
|
|
507
528
|
with:
|
|
508
529
|
name: bangonit-smoke-results
|
|
509
530
|
path: |
|
|
510
|
-
bangonit-output.json
|
|
531
|
+
\${{ github.workspace }}/bangonit-output.json
|
|
511
532
|
recordings/
|
|
512
533
|
if-no-files-found: ignore
|
|
513
534
|
`;
|
|
514
|
-
const fullWorkflow =
|
|
535
|
+
const fullWorkflow = `# Bang On It! — Full Tests
|
|
536
|
+
# Runs all test plans daily at 6 PM US/Eastern (23:00 UTC) and on manual trigger.
|
|
537
|
+
# Edit the cron schedule below to change the time or timezone.
|
|
538
|
+
#
|
|
539
|
+
# Same setup as smoke tests — see bangonit-smoke.yml for EDIT instructions.
|
|
540
|
+
|
|
541
|
+
name: "Bang On It! Full Tests"
|
|
515
542
|
|
|
516
543
|
on:
|
|
517
544
|
schedule:
|
|
518
|
-
- cron: '0
|
|
545
|
+
- cron: '0 23 * * *' # EDIT: daily at 6 PM US/Eastern (23:00 UTC)
|
|
519
546
|
workflow_dispatch:
|
|
520
547
|
|
|
548
|
+
permissions:
|
|
549
|
+
contents: write
|
|
550
|
+
|
|
521
551
|
jobs:
|
|
522
552
|
full:
|
|
523
|
-
runs-on:
|
|
524
|
-
timeout-minutes:
|
|
553
|
+
runs-on: ubuntu-latest
|
|
554
|
+
timeout-minutes: 30
|
|
525
555
|
env:
|
|
526
556
|
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
527
|
-
|
|
557
|
+
# EDIT: Uncomment for S3 recording uploads (see bangonit-smoke.yml)
|
|
558
|
+
# AWS_ACCESS_KEY_ID: \${{ secrets.AWS_ACCESS_KEY_ID }}
|
|
559
|
+
# AWS_SECRET_ACCESS_KEY: \${{ secrets.AWS_SECRET_ACCESS_KEY }}
|
|
560
|
+
# BANGONIT_S3_BASE_URL: "https://my-bucket.s3.amazonaws.com/bangonit"
|
|
561
|
+
|
|
562
|
+
steps:
|
|
563
|
+
- uses: actions/checkout@v4
|
|
564
|
+
|
|
565
|
+
- uses: actions/setup-node@v4
|
|
566
|
+
with:
|
|
567
|
+
node-version: '24' # EDIT: your Node.js version
|
|
568
|
+
|
|
569
|
+
- name: Install system dependencies
|
|
570
|
+
run: |
|
|
571
|
+
sudo apt-get update
|
|
572
|
+
sudo apt-get install -y xvfb libnss3 libatk1.0-0 libatk-bridge2.0-0 libcups2 libdrm2 libxkbcommon0 libxcomposite1 libxdamage1 libxrandr2 libgbm1 libpango-1.0-0 libcairo2 libasound2t64
|
|
573
|
+
|
|
574
|
+
- name: Setup project
|
|
575
|
+
run: npm install && npm run build # EDIT: your install/build commands
|
|
576
|
+
|
|
577
|
+
- name: Start server
|
|
578
|
+
run: npm start & # EDIT: command to start your web server
|
|
579
|
+
|
|
580
|
+
- name: Wait for server
|
|
581
|
+
run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
|
|
582
|
+
|
|
583
|
+
- name: Install bangonit
|
|
584
|
+
run: npm install -g bangonit
|
|
528
585
|
|
|
529
586
|
- name: Run all tests
|
|
530
587
|
run: |
|
|
531
588
|
xvfb-run --auto-servernum boi run \\
|
|
532
|
-
--timeout
|
|
533
|
-
--output bangonit-output.json --record
|
|
589
|
+
--timeout 300 \\
|
|
590
|
+
--output \${{ github.workspace }}/bangonit-output.json --record
|
|
591
|
+
|
|
592
|
+
# EDIT: Uncomment to upload recordings to S3 (see bangonit-smoke.yml for full example)
|
|
593
|
+
# - name: Upload recordings to S3
|
|
594
|
+
# if: always()
|
|
595
|
+
# run: |
|
|
596
|
+
# ...
|
|
597
|
+
|
|
598
|
+
- name: Comment test results on commit
|
|
599
|
+
if: always()
|
|
600
|
+
run: |
|
|
601
|
+
${commentScript}
|
|
602
|
+
|
|
603
|
+
printf "%b" "$BODY" | gh api repos/\${{ github.repository }}/commits/\${{ github.sha }}/comments \\
|
|
604
|
+
--method POST --field "body=@-"
|
|
605
|
+
env:
|
|
606
|
+
GH_TOKEN: \${{ github.token }}
|
|
534
607
|
|
|
535
608
|
- name: Upload test results
|
|
536
609
|
if: always()
|
|
@@ -538,7 +611,7 @@ ${stepsYaml}
|
|
|
538
611
|
with:
|
|
539
612
|
name: bangonit-full-results
|
|
540
613
|
path: |
|
|
541
|
-
bangonit-output.json
|
|
614
|
+
\${{ github.workspace }}/bangonit-output.json
|
|
542
615
|
recordings/
|
|
543
616
|
if-no-files-found: ignore
|
|
544
617
|
`;
|
|
@@ -550,10 +623,11 @@ ${stepsYaml}
|
|
|
550
623
|
const fullPath = path.join(outDir, "bangonit-full.yml");
|
|
551
624
|
fs.writeFileSync(fullPath, fullWorkflow);
|
|
552
625
|
console.log(` ${c.green}Created${c.reset} ${path.relative(process.cwd(), fullPath)}`);
|
|
553
|
-
console.log(`\n ${c.yellow}
|
|
554
|
-
console.log(`
|
|
555
|
-
console.log(
|
|
556
|
-
console.log(`
|
|
626
|
+
console.log(`\n ${c.yellow}Next steps:${c.reset}`);
|
|
627
|
+
console.log(` 1. Edit the workflow files — look for ${c.bold}EDIT${c.reset} comments`);
|
|
628
|
+
console.log(` 2. Add your ${c.bold}ANTHROPIC_API_KEY${c.reset} secret to GitHub`);
|
|
629
|
+
console.log(` ${c.dim}https://github.com/<owner>/<repo>/settings/secrets/actions${c.reset}`);
|
|
630
|
+
console.log(` 3. Commit and push to trigger your first run`);
|
|
557
631
|
}
|
|
558
632
|
p.close();
|
|
559
633
|
console.log("");
|
|
@@ -570,15 +644,22 @@ async function run(argv, config) {
|
|
|
570
644
|
const recordingsDir = config.recordings_dir
|
|
571
645
|
? path.resolve(process.cwd(), config.recordings_dir)
|
|
572
646
|
: path.join(process.cwd(), "recordings");
|
|
573
|
-
// Validate test plan files exist
|
|
647
|
+
// Validate test plan files exist and expand directories
|
|
648
|
+
const expandedFiles = [];
|
|
574
649
|
for (const file of argv.files) {
|
|
575
650
|
const absPath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
576
651
|
if (!fs.existsSync(absPath)) {
|
|
577
652
|
die(`Test plan file not found: ${file}`);
|
|
578
653
|
}
|
|
654
|
+
if (fs.statSync(absPath).isDirectory()) {
|
|
655
|
+
expandedFiles.push(...findTestPlans(absPath, argv.filter));
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
expandedFiles.push(file);
|
|
659
|
+
}
|
|
579
660
|
}
|
|
580
661
|
// Discover test plans if no files/plan specified
|
|
581
|
-
const files = [...
|
|
662
|
+
const files = [...expandedFiles];
|
|
582
663
|
if (files.length === 0 && !argv.plan && config.testplans) {
|
|
583
664
|
const testDirPath = path.resolve(process.cwd(), config.testplans);
|
|
584
665
|
const plans = findTestPlans(testDirPath, argv.filter);
|
|
@@ -604,13 +685,14 @@ async function run(argv, config) {
|
|
|
604
685
|
testPlanFiles: files,
|
|
605
686
|
headless: argv.headless,
|
|
606
687
|
exit: argv.exit,
|
|
688
|
+
keepOpen: argv.keepOpen,
|
|
607
689
|
json: argv.json,
|
|
608
690
|
console: argv.console,
|
|
609
691
|
record: argv.record,
|
|
610
692
|
retries: argv.retries ?? 0,
|
|
611
|
-
output: argv.output
|
|
693
|
+
output: argv.output ? path.resolve(process.cwd(), argv.output) : null,
|
|
612
694
|
plan: argv.plan || null,
|
|
613
|
-
prompt: argv.
|
|
695
|
+
prompt: argv.additionalSystemPrompt || null,
|
|
614
696
|
concurrency: argv.concurrency ?? 1,
|
|
615
697
|
timeout: argv.timeout ?? 0,
|
|
616
698
|
cwd: process.cwd(),
|
|
@@ -656,7 +738,7 @@ async function run(argv, config) {
|
|
|
656
738
|
// Save terminal state before Electron (which inherits stdin) can modify it.
|
|
657
739
|
// stty is not available on Windows.
|
|
658
740
|
let savedTtyState = null;
|
|
659
|
-
if (process.platform !== "win32") {
|
|
741
|
+
if (process.platform !== "win32" && process.stdin.isTTY) {
|
|
660
742
|
try {
|
|
661
743
|
savedTtyState = (0, child_process_1.execSync)("stty -g", { stdio: ["inherit", "pipe", "ignore"] })
|
|
662
744
|
.toString()
|
|
@@ -738,33 +820,7 @@ async function run(argv, config) {
|
|
|
738
820
|
const electronLog = fs.createWriteStream(path.join(LOGS_DIR, "electron.log"));
|
|
739
821
|
electronProc.stderr.pipe(electronLog);
|
|
740
822
|
electronProc.stderr.pipe(process.stderr, { end: false });
|
|
741
|
-
electronProc.on("exit",
|
|
742
|
-
// Upload recordings to S3 if configured
|
|
743
|
-
if (config.s3?.bucket && argv.record) {
|
|
744
|
-
if (fs.existsSync(recordingsDir)) {
|
|
745
|
-
const dirs = fs
|
|
746
|
-
.readdirSync(recordingsDir)
|
|
747
|
-
.filter((d) => fs.existsSync(path.join(recordingsDir, d, "index.html")));
|
|
748
|
-
const s3Prefix = config.s3.prefix || "bangonit";
|
|
749
|
-
for (const dir of dirs) {
|
|
750
|
-
const localPath = path.join(recordingsDir, dir);
|
|
751
|
-
try {
|
|
752
|
-
const url = await uploadToS3(localPath, {
|
|
753
|
-
bucket: config.s3.bucket,
|
|
754
|
-
prefix: `${s3Prefix}/${dir}`,
|
|
755
|
-
region: config.s3.region,
|
|
756
|
-
endpoint: config.s3.endpoint,
|
|
757
|
-
accessKey: config.s3.access_key,
|
|
758
|
-
secretKey: config.s3.secret_key,
|
|
759
|
-
});
|
|
760
|
-
console.log(`\n${c.green}Recording:${c.reset} ${c.cyan}${url}${c.reset}`);
|
|
761
|
-
}
|
|
762
|
-
catch (err) {
|
|
763
|
-
console.error(`\n${c.red}Failed to upload recording:${c.reset} ${err.message}`);
|
|
764
|
-
}
|
|
765
|
-
}
|
|
766
|
-
}
|
|
767
|
-
}
|
|
823
|
+
electronProc.on("exit", (code) => {
|
|
768
824
|
cleanup();
|
|
769
825
|
process.exit(code ?? 1);
|
|
770
826
|
});
|
|
@@ -782,7 +838,10 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
|
|
|
782
838
|
.option("filter", { alias: "t", type: "string", describe: "Filter test plans by name substring" })
|
|
783
839
|
.option("config", { type: "string", describe: "Path to config file (default: .bangonit/config.toml)" })
|
|
784
840
|
.option("plan", { type: "string", describe: "Inline test plan (instead of file)" })
|
|
785
|
-
.option("prompt", {
|
|
841
|
+
.option("additional-system-prompt", {
|
|
842
|
+
type: "string",
|
|
843
|
+
describe: "Additional system prompt text appended to test plan",
|
|
844
|
+
})
|
|
786
845
|
.option("record", { type: "boolean", default: false, describe: "Record session replay" })
|
|
787
846
|
.option("retries", { type: "number", describe: "Retry failed tests N times (overrides test plan frontmatter)" })
|
|
788
847
|
.option("headless", {
|
|
@@ -794,6 +853,11 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
|
|
|
794
853
|
type: "boolean",
|
|
795
854
|
default: ciDefaults.exit ?? false,
|
|
796
855
|
describe: "Exit immediately after tests complete",
|
|
856
|
+
})
|
|
857
|
+
.option("keep-open", {
|
|
858
|
+
type: "boolean",
|
|
859
|
+
default: false,
|
|
860
|
+
describe: "Keep the browser window open after tests pass (for inspection)",
|
|
797
861
|
})
|
|
798
862
|
.option("json", { type: "boolean", default: false, describe: "Stream NDJSON events to stdout" })
|
|
799
863
|
.option("console", { type: "boolean", default: false, describe: "Forward browser console logs to stdout" })
|
|
@@ -805,11 +869,12 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
|
|
|
805
869
|
files: argv.files,
|
|
806
870
|
filter: argv.filter,
|
|
807
871
|
plan: argv.plan,
|
|
808
|
-
|
|
872
|
+
additionalSystemPrompt: argv.additionalSystemPrompt,
|
|
809
873
|
record: argv.record,
|
|
810
874
|
retries: argv.retries,
|
|
811
875
|
headless: argv.headless,
|
|
812
876
|
exit: argv.exit,
|
|
877
|
+
keepOpen: argv.keepOpen,
|
|
813
878
|
json: argv.json,
|
|
814
879
|
console: argv.console,
|
|
815
880
|
output: argv.output,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bangonit",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.4",
|
|
4
4
|
"description": "AI-powered E2E testing tool",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bangonit": "bin/src/cli/bangonit.js",
|
|
@@ -59,10 +59,10 @@
|
|
|
59
59
|
},
|
|
60
60
|
"dependencies": {
|
|
61
61
|
"@iarna/toml": "^2.2.5",
|
|
62
|
+
"diff": "^8.0.3",
|
|
62
63
|
"dotenv": "^17.3.1",
|
|
63
64
|
"electron": "^40.8.0",
|
|
64
65
|
"is-ci": "^4.1.0",
|
|
65
|
-
"minio": "^8.0.7",
|
|
66
66
|
"yargs": "^18.0.0",
|
|
67
67
|
"zod": "^3.25.0"
|
|
68
68
|
},
|