bangonit 0.4.3 → 0.5.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 +2 -1
- package/app/desktopapp/dist/main/index.js +10 -4
- package/app/desktopapp/dist/main/ipc.js +9 -24
- package/app/desktopapp/dist/main/preload.js +0 -7
- package/app/desktopapp/dist/main/tabs.js +10 -5
- package/app/desktopapp/package.json +0 -1
- package/app/replay/dist/replay.js +2 -2
- package/app/webapp/.next/standalone/app/webapp/.next/BUILD_ID +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/app-build-manifest.json +11 -11
- package/app/webapp/.next/standalone/app/webapp/.next/app-path-routes-manifest.json +1 -1
- 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/screenshot/route.js +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/app-paths-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/679.js +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/server/chunks/708.js +3 -3
- 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/server-reference-manifest.json +1 -1
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
- package/app/webapp/.next/{static/chunks/main-app-106dd83f859b9dfa.js → standalone/app/webapp/.next/static/chunks/main-app-76384b941f0b51cb.js} +1 -1
- package/app/webapp/.next/standalone/app/webapp/package.json +2 -6
- package/app/webapp/.next/standalone/app/webapp/server.js +1 -1
- package/app/webapp/.next/standalone/package.json +18 -6
- package/app/webapp/.next/static/chunks/app/app/page-533a30559a8f39fa.js +1 -0
- package/app/webapp/.next/static/chunks/app/layout-40f50d9380154ecf.js +1 -0
- package/app/webapp/.next/{standalone/app/webapp/.next/static/chunks/main-app-106dd83f859b9dfa.js → static/chunks/main-app-76384b941f0b51cb.js} +1 -1
- package/app/webapp/package.json +2 -6
- package/app/webapp/skills/document-review.md +1 -0
- package/app/webapp/skills/gmail.md +2 -0
- package/app/webapp/src/app/globals.css +8 -3
- package/app/webapp/src/app/layout.tsx +2 -8
- package/app/webapp/src/shared/api/chat.ts +49 -25
- package/app/webapp/src/shared/api/screenshot.ts +11 -10
- package/app/webapp/src/shared/components/AppShell.tsx +80 -109
- package/app/webapp/src/shared/components/SessionView.tsx +335 -248
- package/app/webapp/src/shared/components/VirtualCursor.tsx +13 -14
- package/app/webapp/src/shared/lib/browser/cursor.ts +2 -7
- package/app/webapp/src/shared/lib/browser/index.ts +56 -36
- package/app/webapp/src/shared/lib/browser/mouse.ts +86 -21
- package/app/webapp/src/shared/lib/browser/navigate.ts +1 -4
- package/app/webapp/src/shared/lib/browser/recorder.ts +12 -5
- package/app/webapp/src/shared/lib/browser/screenshot.ts +4 -4
- package/app/webapp/src/shared/lib/browser/snapshot.ts +9 -5
- package/app/webapp/src/shared/lib/browser/tabs.ts +1 -1
- package/app/webapp/src/shared/lib/browser/types.ts +3 -2
- package/app/webapp/src/shared/lib/browser/wait.ts +1 -1
- package/app/webapp/src/shared/lib/recorder/session-recorder.ts +1 -1
- package/app/webapp/src/shared/types/global.d.ts +8 -19
- package/app/webapp/tailwind.config.js +1 -3
- package/bin/src/cli/bangonit.js +270 -177
- package/package.json +18 -6
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
- package/app/webapp/.next/standalone/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
- package/app/webapp/.next/static/chunks/app/app/page-03dbc2fc67c26b74.js +0 -1
- package/app/webapp/.next/static/chunks/app/layout-57acb80d8da0067a.js +0 -1
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
- /package/app/webapp/.next/standalone/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_ssgManifest.js +0 -0
- /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_buildManifest.js +0 -0
- /package/app/webapp/.next/static/{z2gRF0NKwztPLZ9d7ok06 → kz1a_SRPtSly3Fe8wHKDq}/_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,54 +182,12 @@ 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 });
|
|
233
|
-
rl.on("close", () => {
|
|
188
|
+
rl.on("close", () => {
|
|
189
|
+
closed = true;
|
|
190
|
+
});
|
|
234
191
|
return {
|
|
235
192
|
ask(question, defaultVal) {
|
|
236
193
|
if (closed)
|
|
@@ -245,7 +202,7 @@ function createPrompter() {
|
|
|
245
202
|
askChoice(question, choices, defaultVal) {
|
|
246
203
|
if (closed)
|
|
247
204
|
return Promise.resolve(defaultVal);
|
|
248
|
-
const choiceStr = choices.map((ch) => ch === defaultVal ? `${c.bold}${ch}${c.reset}${c.dim}` : ch).join("/");
|
|
205
|
+
const choiceStr = choices.map((ch) => (ch === defaultVal ? `${c.bold}${ch}${c.reset}${c.dim}` : ch)).join("/");
|
|
249
206
|
return new Promise((resolve) => {
|
|
250
207
|
rl.question(` ${c.cyan}?${c.reset} ${question} ${c.dim}(${choiceStr})${c.reset} `, (answer) => {
|
|
251
208
|
const val = answer.trim().toLowerCase() || defaultVal;
|
|
@@ -253,55 +210,27 @@ function createPrompter() {
|
|
|
253
210
|
});
|
|
254
211
|
});
|
|
255
212
|
},
|
|
256
|
-
close() {
|
|
213
|
+
close() {
|
|
214
|
+
rl.close();
|
|
215
|
+
},
|
|
257
216
|
};
|
|
258
217
|
}
|
|
259
218
|
// --- init command ---
|
|
260
|
-
// Common timezone offsets from UTC (standard time)
|
|
261
|
-
const TIMEZONE_OFFSETS = {
|
|
262
|
-
"us/eastern": -5, "us/central": -6, "us/mountain": -7, "us/pacific": -8,
|
|
263
|
-
"europe/london": 0, "europe/berlin": 1, "europe/paris": 1,
|
|
264
|
-
"asia/tokyo": 9, "asia/shanghai": 8, "asia/kolkata": 5,
|
|
265
|
-
"australia/sydney": 11,
|
|
266
|
-
};
|
|
267
|
-
function localHourToUtc(localHour, utcOffset) {
|
|
268
|
-
return ((localHour - utcOffset) % 24 + 24) % 24;
|
|
269
|
-
}
|
|
270
219
|
async function initProject() {
|
|
271
220
|
const p = createPrompter();
|
|
272
221
|
console.log(`\n ${c.bold}${c.magenta}Bang On It! init${c.reset}\n`);
|
|
273
222
|
const testplans = await p.ask("Test plans directory", "testplans");
|
|
274
|
-
const apiKey = await p.ask("Anthropic API key (empty to use env var)", "");
|
|
275
223
|
const recordingsDir = await p.ask("Recordings directory", "recordings");
|
|
276
|
-
const s3Bucket = await p.ask("S3 bucket for recordings (empty to skip)", "");
|
|
277
|
-
let s3Endpoint = "";
|
|
278
|
-
let s3Region = "";
|
|
279
|
-
let s3Prefix = "";
|
|
280
|
-
if (s3Bucket) {
|
|
281
|
-
s3Endpoint = await p.ask("S3 endpoint (empty for AWS, or e.g. nyc3.digitaloceanspaces.com)", "");
|
|
282
|
-
s3Region = await p.ask("S3 region", "us-east-1");
|
|
283
|
-
s3Prefix = await p.ask("S3 prefix", "bangonit");
|
|
284
|
-
}
|
|
285
224
|
// --- Config file ---
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
toml += `\n[s3]\nbucket = "${s3Bucket}"\n`;
|
|
296
|
-
if (s3Endpoint)
|
|
297
|
-
toml += `endpoint = "${s3Endpoint}"\n`;
|
|
298
|
-
if (s3Region && s3Region !== "us-east-1")
|
|
299
|
-
toml += `region = "${s3Region}"\n`;
|
|
300
|
-
if (s3Prefix && s3Prefix !== "bangonit")
|
|
301
|
-
toml += `prefix = "${s3Prefix}"\n`;
|
|
302
|
-
toml += `# access_key = "\${AWS_ACCESS_KEY_ID}"\n`;
|
|
303
|
-
toml += `# secret_key = "\${AWS_SECRET_ACCESS_KEY}"\n`;
|
|
304
|
-
}
|
|
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
|
+
`;
|
|
305
234
|
const bangDir = path.join(process.cwd(), ".bangonit");
|
|
306
235
|
fs.mkdirSync(bangDir, { recursive: true });
|
|
307
236
|
const configOutPath = path.join(bangDir, "config.toml");
|
|
@@ -431,64 +360,167 @@ Create new Bang On It! test plan files.
|
|
|
431
360
|
console.log(` ${c.green}Created${c.reset} .claude/skills/create-test/SKILL.md`);
|
|
432
361
|
// --- CI setup ---
|
|
433
362
|
console.log("");
|
|
434
|
-
const setupCi = await p.askChoice("Set up GitHub Actions
|
|
363
|
+
const setupCi = await p.askChoice("Set up GitHub Actions?", ["y", "n"], "y");
|
|
435
364
|
if (setupCi === "y") {
|
|
436
|
-
|
|
437
|
-
const
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
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
|
+
|
|
449
447
|
steps:
|
|
450
448
|
- uses: actions/checkout@v4
|
|
451
449
|
|
|
452
450
|
- uses: actions/setup-node@v4
|
|
453
451
|
with:
|
|
454
|
-
node-version: '
|
|
452
|
+
node-version: '20' # EDIT: your Node.js version
|
|
455
453
|
|
|
456
454
|
- name: Install system dependencies
|
|
457
455
|
run: |
|
|
458
456
|
sudo apt-get update
|
|
459
|
-
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
|
|
460
458
|
|
|
461
459
|
- name: Setup project
|
|
462
|
-
run:
|
|
460
|
+
run: npm install && npm run build # EDIT: your install/build commands
|
|
463
461
|
|
|
464
462
|
- name: Start server
|
|
465
|
-
run:
|
|
463
|
+
run: npm start & # EDIT: command to start your web server
|
|
466
464
|
|
|
467
465
|
- name: Wait for server
|
|
468
|
-
run: npx wait-on
|
|
466
|
+
run: npx wait-on http://localhost:3000 --timeout 30000 # EDIT: your app's URL
|
|
469
467
|
|
|
470
468
|
- name: Install bangonit
|
|
471
|
-
run: npm install -g bangonit
|
|
472
|
-
const smokeWorkflow = `name: "Bang On It! Smoke Tests"
|
|
473
|
-
|
|
474
|
-
on:
|
|
475
|
-
push:
|
|
476
|
-
branches: [main, master]
|
|
477
|
-
pull_request:
|
|
469
|
+
run: npm install -g bangonit
|
|
478
470
|
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
$
|
|
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 }}
|
|
486
480
|
|
|
487
481
|
- name: Run smoke tests
|
|
488
482
|
run: |
|
|
489
|
-
xvfb-run --auto-servernum boi run ${testplans}/smoke
|
|
490
|
-
--timeout
|
|
491
|
-
--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 }}
|
|
492
524
|
|
|
493
525
|
- name: Upload test results
|
|
494
526
|
if: always()
|
|
@@ -496,30 +528,82 @@ ${stepsYaml}
|
|
|
496
528
|
with:
|
|
497
529
|
name: bangonit-smoke-results
|
|
498
530
|
path: |
|
|
499
|
-
bangonit-output.json
|
|
531
|
+
\${{ github.workspace }}/bangonit-output.json
|
|
500
532
|
recordings/
|
|
501
533
|
if-no-files-found: ignore
|
|
502
534
|
`;
|
|
503
|
-
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"
|
|
504
542
|
|
|
505
543
|
on:
|
|
506
544
|
schedule:
|
|
507
|
-
- cron: '0
|
|
545
|
+
- cron: '0 23 * * *' # EDIT: daily at 6 PM US/Eastern (23:00 UTC)
|
|
508
546
|
workflow_dispatch:
|
|
509
547
|
|
|
548
|
+
permissions:
|
|
549
|
+
contents: write
|
|
550
|
+
|
|
510
551
|
jobs:
|
|
511
552
|
full:
|
|
512
|
-
runs-on:
|
|
513
|
-
timeout-minutes:
|
|
553
|
+
runs-on: ubuntu-latest
|
|
554
|
+
timeout-minutes: 30
|
|
514
555
|
env:
|
|
515
556
|
ANTHROPIC_API_KEY: \${{ secrets.ANTHROPIC_API_KEY }}
|
|
516
|
-
|
|
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: '20' # 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
|
|
517
585
|
|
|
518
586
|
- name: Run all tests
|
|
519
587
|
run: |
|
|
520
588
|
xvfb-run --auto-servernum boi run \\
|
|
521
|
-
--timeout
|
|
522
|
-
--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 }}
|
|
523
607
|
|
|
524
608
|
- name: Upload test results
|
|
525
609
|
if: always()
|
|
@@ -527,7 +611,7 @@ ${stepsYaml}
|
|
|
527
611
|
with:
|
|
528
612
|
name: bangonit-full-results
|
|
529
613
|
path: |
|
|
530
|
-
bangonit-output.json
|
|
614
|
+
\${{ github.workspace }}/bangonit-output.json
|
|
531
615
|
recordings/
|
|
532
616
|
if-no-files-found: ignore
|
|
533
617
|
`;
|
|
@@ -539,10 +623,11 @@ ${stepsYaml}
|
|
|
539
623
|
const fullPath = path.join(outDir, "bangonit-full.yml");
|
|
540
624
|
fs.writeFileSync(fullPath, fullWorkflow);
|
|
541
625
|
console.log(` ${c.green}Created${c.reset} ${path.relative(process.cwd(), fullPath)}`);
|
|
542
|
-
console.log(`\n ${c.yellow}
|
|
543
|
-
console.log(`
|
|
544
|
-
console.log(
|
|
545
|
-
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`);
|
|
546
631
|
}
|
|
547
632
|
p.close();
|
|
548
633
|
console.log("");
|
|
@@ -559,15 +644,22 @@ async function run(argv, config) {
|
|
|
559
644
|
const recordingsDir = config.recordings_dir
|
|
560
645
|
? path.resolve(process.cwd(), config.recordings_dir)
|
|
561
646
|
: path.join(process.cwd(), "recordings");
|
|
562
|
-
// Validate test plan files exist
|
|
647
|
+
// Validate test plan files exist and expand directories
|
|
648
|
+
const expandedFiles = [];
|
|
563
649
|
for (const file of argv.files) {
|
|
564
650
|
const absPath = path.isAbsolute(file) ? file : path.join(process.cwd(), file);
|
|
565
651
|
if (!fs.existsSync(absPath)) {
|
|
566
652
|
die(`Test plan file not found: ${file}`);
|
|
567
653
|
}
|
|
654
|
+
if (fs.statSync(absPath).isDirectory()) {
|
|
655
|
+
expandedFiles.push(...findTestPlans(absPath, argv.filter));
|
|
656
|
+
}
|
|
657
|
+
else {
|
|
658
|
+
expandedFiles.push(file);
|
|
659
|
+
}
|
|
568
660
|
}
|
|
569
661
|
// Discover test plans if no files/plan specified
|
|
570
|
-
const files = [...
|
|
662
|
+
const files = [...expandedFiles];
|
|
571
663
|
if (files.length === 0 && !argv.plan && config.testplans) {
|
|
572
664
|
const testDirPath = path.resolve(process.cwd(), config.testplans);
|
|
573
665
|
const plans = findTestPlans(testDirPath, argv.filter);
|
|
@@ -597,7 +689,7 @@ async function run(argv, config) {
|
|
|
597
689
|
console: argv.console,
|
|
598
690
|
record: argv.record,
|
|
599
691
|
retries: argv.retries ?? 0,
|
|
600
|
-
output: argv.output
|
|
692
|
+
output: argv.output ? path.resolve(process.cwd(), argv.output) : null,
|
|
601
693
|
plan: argv.plan || null,
|
|
602
694
|
prompt: argv.prompt || null,
|
|
603
695
|
concurrency: argv.concurrency ?? 1,
|
|
@@ -642,13 +734,18 @@ async function run(argv, config) {
|
|
|
642
734
|
die(`Webapp server crashed (exit code ${code}). Check logs/webapp.log for details.`);
|
|
643
735
|
}
|
|
644
736
|
});
|
|
645
|
-
// Save terminal state before Electron (which inherits stdin) can modify it
|
|
737
|
+
// Save terminal state before Electron (which inherits stdin) can modify it.
|
|
738
|
+
// stty is not available on Windows.
|
|
646
739
|
let savedTtyState = null;
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
740
|
+
if (process.platform !== "win32" && process.stdin.isTTY) {
|
|
741
|
+
try {
|
|
742
|
+
savedTtyState = (0, child_process_1.execSync)("stty -g", { stdio: ["inherit", "pipe", "ignore"] })
|
|
743
|
+
.toString()
|
|
744
|
+
.trim();
|
|
745
|
+
}
|
|
746
|
+
catch (e) {
|
|
747
|
+
console.error("[cli] stty save failed:", e.message);
|
|
748
|
+
}
|
|
652
749
|
}
|
|
653
750
|
let electronProc = null;
|
|
654
751
|
const cleanup = () => {
|
|
@@ -676,8 +773,14 @@ async function run(argv, config) {
|
|
|
676
773
|
}
|
|
677
774
|
};
|
|
678
775
|
process.on("exit", cleanup);
|
|
679
|
-
process.on("SIGINT", () => {
|
|
680
|
-
|
|
776
|
+
process.on("SIGINT", () => {
|
|
777
|
+
cleanup();
|
|
778
|
+
process.exit(1);
|
|
779
|
+
});
|
|
780
|
+
process.on("SIGTERM", () => {
|
|
781
|
+
cleanup();
|
|
782
|
+
process.exit(1);
|
|
783
|
+
});
|
|
681
784
|
await waitForPort(PORT);
|
|
682
785
|
const electronMain = path.join(DESKTOP_DIR, "dist", "main", "index.js");
|
|
683
786
|
if (!fs.existsSync(electronMain)) {
|
|
@@ -702,7 +805,8 @@ async function run(argv, config) {
|
|
|
702
805
|
die("Error: electron not found. Run `npm install` in app/desktopapp.");
|
|
703
806
|
}
|
|
704
807
|
}
|
|
705
|
-
|
|
808
|
+
const electronExtraArgs = process.env.CI ? ["--no-sandbox", "--disable-gpu"] : [];
|
|
809
|
+
electronProc = (0, child_process_1.spawn)(electronPath, [".", ...electronExtraArgs], {
|
|
706
810
|
cwd: DESKTOP_DIR,
|
|
707
811
|
env: {
|
|
708
812
|
...process.env,
|
|
@@ -710,33 +814,12 @@ async function run(argv, config) {
|
|
|
710
814
|
WEBAPP_URL: `http://localhost:${PORT}/app`,
|
|
711
815
|
NODE_ENV: process.env.NODE_ENV || "production",
|
|
712
816
|
},
|
|
713
|
-
stdio: ["inherit", "inherit", "
|
|
817
|
+
stdio: ["inherit", "inherit", "pipe"],
|
|
714
818
|
});
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
const dirs = fs.readdirSync(recordingsDir).filter((d) => fs.existsSync(path.join(recordingsDir, d, "index.html")));
|
|
720
|
-
const s3Prefix = config.s3.prefix || "bangonit";
|
|
721
|
-
for (const dir of dirs) {
|
|
722
|
-
const localPath = path.join(recordingsDir, dir);
|
|
723
|
-
try {
|
|
724
|
-
const url = await uploadToS3(localPath, {
|
|
725
|
-
bucket: config.s3.bucket,
|
|
726
|
-
prefix: `${s3Prefix}/${dir}`,
|
|
727
|
-
region: config.s3.region,
|
|
728
|
-
endpoint: config.s3.endpoint,
|
|
729
|
-
accessKey: config.s3.access_key,
|
|
730
|
-
secretKey: config.s3.secret_key,
|
|
731
|
-
});
|
|
732
|
-
console.log(`\n${c.green}Recording:${c.reset} ${c.cyan}${url}${c.reset}`);
|
|
733
|
-
}
|
|
734
|
-
catch (err) {
|
|
735
|
-
console.error(`\n${c.red}Failed to upload recording:${c.reset} ${err.message}`);
|
|
736
|
-
}
|
|
737
|
-
}
|
|
738
|
-
}
|
|
739
|
-
}
|
|
819
|
+
const electronLog = fs.createWriteStream(path.join(LOGS_DIR, "electron.log"));
|
|
820
|
+
electronProc.stderr.pipe(electronLog);
|
|
821
|
+
electronProc.stderr.pipe(process.stderr, { end: false });
|
|
822
|
+
electronProc.on("exit", (code) => {
|
|
740
823
|
cleanup();
|
|
741
824
|
process.exit(code ?? 1);
|
|
742
825
|
});
|
|
@@ -746,7 +829,9 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
|
|
|
746
829
|
(0, yargs_1.default)((0, helpers_1.hideBin)(process.argv))
|
|
747
830
|
.scriptName("boi")
|
|
748
831
|
.usage("Usage: $0 <command> [options]")
|
|
749
|
-
.command("init", "Set up Bang On It! (config, test plans, and optionally CI)", {}, () => {
|
|
832
|
+
.command("init", "Set up Bang On It! (config, test plans, and optionally CI)", {}, () => {
|
|
833
|
+
initProject();
|
|
834
|
+
})
|
|
750
835
|
.command(["run [files..]", "$0"], "Run test plans (or launch interactive UI)", (y) => y
|
|
751
836
|
.positional("files", { type: "string", array: true, default: [], describe: "Test plan files" })
|
|
752
837
|
.option("filter", { alias: "t", type: "string", describe: "Filter test plans by name substring" })
|
|
@@ -755,8 +840,16 @@ const ciDefaults = is_ci_1.default ? { headless: true, exit: true } : {};
|
|
|
755
840
|
.option("prompt", { type: "string", describe: "Additional instructions appended to test plan" })
|
|
756
841
|
.option("record", { type: "boolean", default: false, describe: "Record session replay" })
|
|
757
842
|
.option("retries", { type: "number", describe: "Retry failed tests N times (overrides test plan frontmatter)" })
|
|
758
|
-
.option("headless", {
|
|
759
|
-
|
|
843
|
+
.option("headless", {
|
|
844
|
+
type: "boolean",
|
|
845
|
+
default: ciDefaults.headless ?? false,
|
|
846
|
+
describe: "Run without showing the browser window",
|
|
847
|
+
})
|
|
848
|
+
.option("exit", {
|
|
849
|
+
type: "boolean",
|
|
850
|
+
default: ciDefaults.exit ?? false,
|
|
851
|
+
describe: "Exit immediately after tests complete",
|
|
852
|
+
})
|
|
760
853
|
.option("json", { type: "boolean", default: false, describe: "Stream NDJSON events to stdout" })
|
|
761
854
|
.option("console", { type: "boolean", default: false, describe: "Forward browser console logs to stdout" })
|
|
762
855
|
.option("output", { type: "string", describe: "Write JSON results to file" })
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bangonit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "AI-powered E2E testing tool",
|
|
5
5
|
"bin": {
|
|
6
6
|
"bangonit": "bin/src/cli/bangonit.js",
|
|
@@ -14,7 +14,16 @@
|
|
|
14
14
|
"build:webapp": "cd app/webapp && npm run build && cp -r .next/static .next/standalone/app/webapp/.next/static",
|
|
15
15
|
"build:electron": "cd app/desktopapp && npx tsc",
|
|
16
16
|
"build:replay": "cd app/replay && npx vite build",
|
|
17
|
-
"test": "node --test test/*.test.ts"
|
|
17
|
+
"test": "node --test test/*.test.ts",
|
|
18
|
+
"format": "prettier --write .",
|
|
19
|
+
"format:check": "prettier --check .",
|
|
20
|
+
"lint": "eslint .",
|
|
21
|
+
"lint:fix": "eslint --fix .",
|
|
22
|
+
"typecheck": "concurrently npm:typecheck:cli npm:typecheck:webapp npm:typecheck:desktop npm:typecheck:replay",
|
|
23
|
+
"typecheck:cli": "tsc --noEmit -p tsconfig.cli.json",
|
|
24
|
+
"typecheck:webapp": "cd app/webapp && npx tsc --noEmit",
|
|
25
|
+
"typecheck:desktop": "cd app/desktopapp && npx tsc --noEmit",
|
|
26
|
+
"typecheck:replay": "cd app/replay && npx tsc --noEmit"
|
|
18
27
|
},
|
|
19
28
|
"workspaces": [
|
|
20
29
|
"app/*"
|
|
@@ -52,16 +61,19 @@
|
|
|
52
61
|
"@iarna/toml": "^2.2.5",
|
|
53
62
|
"dotenv": "^17.3.1",
|
|
54
63
|
"electron": "^40.8.0",
|
|
55
|
-
"electron-store": "^8.1.0",
|
|
56
64
|
"is-ci": "^4.1.0",
|
|
57
|
-
"minio": "^8.0.7",
|
|
58
65
|
"yargs": "^18.0.0",
|
|
59
66
|
"zod": "^3.25.0"
|
|
60
67
|
},
|
|
61
68
|
"devDependencies": {
|
|
69
|
+
"@eslint/js": "^9.39.4",
|
|
62
70
|
"@types/is-ci": "^3.0.4",
|
|
63
71
|
"@types/yargs": "^17.0.35",
|
|
64
72
|
"concurrently": "^9.2.1",
|
|
65
|
-
"
|
|
73
|
+
"eslint": "^9.39.4",
|
|
74
|
+
"eslint-plugin-react-hooks": "^7.0.1",
|
|
75
|
+
"playwright": "^1.58.2",
|
|
76
|
+
"prettier": "^3.5.3",
|
|
77
|
+
"typescript-eslint": "^8.57.1"
|
|
66
78
|
}
|
|
67
|
-
}
|
|
79
|
+
}
|