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.
Files changed (64) hide show
  1. package/README.md +13 -2
  2. package/app/desktopapp/dist/main/index.js +14 -10
  3. package/app/desktopapp/dist/shared/args.js +1 -0
  4. package/app/replay/dist/replay.js +1 -1
  5. package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
  6. package/app/webapp/.next/standalone/app/webapp/.next/app-build-manifest.json +7 -7
  7. package/app/webapp/.next/standalone/app/webapp/.next/build-manifest.json +3 -3
  8. package/app/webapp/.next/standalone/app/webapp/.next/prerender-manifest.json +1 -1
  9. package/app/webapp/.next/standalone/app/webapp/.next/required-server-files.json +1 -1
  10. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page.js +1 -1
  11. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  12. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.html +1 -1
  13. package/app/webapp/.next/standalone/app/webapp/.next/server/app/_not-found.rsc +1 -1
  14. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js +1 -1
  15. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/chat/route.js.nft.json +1 -1
  16. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js +1 -1
  17. package/app/webapp/.next/standalone/app/webapp/.next/server/app/api/screenshot/route.js.nft.json +1 -1
  18. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page.js +6 -6
  19. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app/page_client-reference-manifest.js +1 -1
  20. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.html +1 -1
  21. package/app/webapp/.next/standalone/app/webapp/.next/server/app/app.rsc +2 -2
  22. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.html +1 -1
  23. package/app/webapp/.next/standalone/app/webapp/.next/server/app/index.rsc +1 -1
  24. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page.js +1 -1
  25. package/app/webapp/.next/standalone/app/webapp/.next/server/app/page_client-reference-manifest.js +1 -1
  26. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/{708.js → 151.js} +9 -7
  27. package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/679.js +1 -1
  28. package/app/webapp/.next/standalone/app/webapp/.next/server/middleware-build-manifest.js +1 -1
  29. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/404.html +1 -1
  30. package/app/webapp/.next/standalone/app/webapp/.next/server/pages/500.html +1 -1
  31. package/app/webapp/.next/standalone/app/webapp/.next/server/pages-manifest.json +1 -1
  32. package/app/webapp/.next/standalone/app/webapp/.next/server/server-reference-manifest.json +1 -1
  33. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/793-be37f231ca512496.js +36 -0
  34. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-4c6dfbf12230aab7.js +1 -0
  35. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +1 -0
  36. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/{main-app-76384b941f0b51cb.js → main-app-106dd83f859b9dfa.js} +1 -1
  37. package/app/webapp/.next/standalone/app/webapp/package.json +1 -0
  38. package/app/webapp/.next/standalone/app/webapp/server.js +1 -1
  39. package/app/webapp/.next/standalone/package.json +2 -2
  40. package/app/webapp/.next/static/chunks/793-be37f231ca512496.js +36 -0
  41. package/app/webapp/.next/static/chunks/app/app/page-4c6dfbf12230aab7.js +1 -0
  42. package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +1 -0
  43. package/app/webapp/.next/static/chunks/{main-app-76384b941f0b51cb.js → main-app-106dd83f859b9dfa.js} +1 -1
  44. package/app/webapp/package.json +1 -0
  45. package/app/webapp/src/shared/api/chat.ts +13 -2
  46. package/app/webapp/src/shared/components/AppShell.tsx +15 -5
  47. package/app/webapp/src/shared/components/SessionView.tsx +10 -2
  48. package/app/webapp/src/shared/lib/browser/index.ts +2 -2
  49. package/app/webapp/src/shared/lib/browser/screenshot.ts +2 -2
  50. package/app/webapp/src/shared/lib/browser/snapshot.ts +107 -7
  51. package/app/webapp/src/shared/lib/browser/types.ts +1 -0
  52. package/bin/app/desktopapp/src/shared/args.js +1 -0
  53. package/bin/src/cli/bangonit.js +259 -194
  54. package/package.json +2 -2
  55. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/631-0edca788fa58159b.js +0 -36
  56. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +0 -1
  57. package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +0 -1
  58. package/app/webapp/.next/static/chunks/631-0edca788fa58159b.js +0 -36
  59. package/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +0 -1
  60. package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +0 -1
  61. /package/app/webapp/.next/standalone/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_buildManifest.js +0 -0
  62. /package/app/webapp/.next/standalone/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_ssgManifest.js +0 -0
  63. /package/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_buildManifest.js +0 -0
  64. /package/app/webapp/.next/static/{oyP-lwTb7W3NCCryjxA6T → 96Rcgc5VRl40T_EbDikOw}/_ssgManifest.js +0 -0
@@ -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
- let toml = `testplans = "${testplans}"\n`;
298
- toml += `recordings_dir = "${recordingsDir}"\n`;
299
- if (apiKey) {
300
- toml += `anthropic_api_key = "${apiKey}"\n`;
301
- }
302
- else {
303
- toml += `# anthropic_api_key = "\${ANTHROPIC_API_KEY}"\n`;
304
- }
305
- if (s3Bucket) {
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 CI?", ["y", "n"], "y");
363
+ const setupCi = await p.askChoice("Set up GitHub Actions?", ["y", "n"], "y");
446
364
  if (setupCi === "y") {
447
- console.log("");
448
- const baseImage = await p.ask("GitHub Actions runner", "ubuntu-latest");
449
- const nodeVersion = await p.ask("Node.js version", "20");
450
- const setupCommand = await p.ask("Setup command", "npm install && npm run build");
451
- const startCommand = await p.ask("Command to start your web server", "npm start &");
452
- const waitUrl = await p.ask("URL to wait for before testing", "http://localhost:3000");
453
- const timeout = await p.ask("Test timeout in seconds", "300");
454
- const tzNames = Object.keys(TIMEZONE_OFFSETS).join(", ");
455
- const tz = await p.ask(`Timezone for full run (${tzNames})`, "us/eastern");
456
- const utcOffset = TIMEZONE_OFFSETS[tz.toLowerCase()] ?? -5;
457
- const fullUtcHour = localHourToUtc(18, utcOffset);
458
- const timeoutMinutes = Math.ceil(parseInt(timeout) / 60) + 5;
459
- const stepsYaml = `
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: '${nodeVersion}'
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 libasound2
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: ${setupCommand}
460
+ run: npm install && npm run build # EDIT: your install/build commands
474
461
 
475
462
  - name: Start server
476
- run: ${startCommand}
463
+ run: npm start & # EDIT: command to start your web server
477
464
 
478
465
  - name: Wait for server
479
- run: npx wait-on ${waitUrl} --timeout 30000
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
- jobs:
491
- smoke:
492
- runs-on: ${baseImage}
493
- timeout-minutes: ${timeoutMinutes}
494
- env:
495
- ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
496
- ${stepsYaml}
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 ${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 = `name: "Bang On It! Full Tests"
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 ${fullUtcHour} * * *'
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: ${baseImage}
524
- timeout-minutes: ${timeoutMinutes * 3}
553
+ runs-on: ubuntu-latest
554
+ timeout-minutes: 30
525
555
  env:
526
556
  ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
527
- ${stepsYaml}
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 ${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}Required GitHub secret:${c.reset}`);
554
- console.log(` ${c.dim} ANTHROPIC_API_KEY${c.reset} — your Anthropic API key`);
555
- console.log(`\n ${c.dim}Smoke tests run on every push/PR.${c.reset}`);
556
- console.log(` ${c.dim}Full tests run all test plans daily at 6pm ${tz} (${fullUtcHour}:00 UTC).${c.reset}`);
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 before launching anything
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 = [...argv.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 || null,
693
+ output: argv.output ? path.resolve(process.cwd(), argv.output) : null,
612
694
  plan: argv.plan || null,
613
- prompt: argv.prompt || null,
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", async (code) => {
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", { type: "string", describe: "Additional instructions appended to test plan" })
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
- prompt: argv.prompt,
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.0",
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
  },