@muggleai/works 5.20.0-staging.135 → 5.20.0-staging.137

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.
@@ -354,15 +354,37 @@ function parentIsAlive(pid) {
354
354
  }
355
355
  }
356
356
 
357
- // src/cli/pr-section/render.ts
357
+ // src/cli/pr-section/constants.ts
358
358
  var DASHBOARD_URL_BASE = "https://www.muggle-ai.com/muggleTestV0/dashboard/projects";
359
359
  var DETAIL_IMAGE_WIDTH = 720;
360
- function countTests(report) {
361
- const total = report.tests.length;
362
- const passed = report.tests.filter((t) => t.status === "passed").length;
363
- const failed = report.tests.filter((t) => t.status === "failed").length;
364
- const inconclusive = report.tests.filter((t) => t.status === "inconclusive").length;
365
- return { total, passed, failed, inconclusive };
360
+ var STEP_LIST_ELISION_THRESHOLD = 10;
361
+ var STEP_LIST_HEAD_COUNT = 6;
362
+ var STEP_LIST_TAIL_COUNT = 3;
363
+
364
+ // src/cli/pr-section/render.ts
365
+ function resolveLatestRuns(tests) {
366
+ const byCaseId = /* @__PURE__ */ new Map();
367
+ const order = [];
368
+ for (const test of tests) {
369
+ const existing = byCaseId.get(test.testCaseId);
370
+ if (existing) {
371
+ existing.test = test;
372
+ existing.attempts += 1;
373
+ continue;
374
+ }
375
+ byCaseId.set(test.testCaseId, { test, attempts: 1 });
376
+ order.push(test.testCaseId);
377
+ }
378
+ return order.map((id) => byCaseId.get(id));
379
+ }
380
+ function countTests(latestRuns) {
381
+ const statuses = latestRuns.map((entry) => entry.test.status);
382
+ return {
383
+ total: statuses.length,
384
+ passed: statuses.filter((s) => s === "passed").length,
385
+ failed: statuses.filter((s) => s === "failed").length,
386
+ inconclusive: statuses.filter((s) => s === "inconclusive").length
387
+ };
366
388
  }
367
389
  function statusEmoji(test) {
368
390
  if (test.status === "passed") return "\u2705";
@@ -370,23 +392,30 @@ function statusEmoji(test) {
370
392
  return "\u26A0\uFE0F";
371
393
  }
372
394
  function computeVerdict(report) {
373
- if (report.tests.length === 0) return "none";
374
- if (report.tests.some((t) => t.status === "failed")) return "fail";
375
- if (report.tests.some((t) => t.status === "inconclusive")) return "inconclusive";
395
+ const latestRuns = resolveLatestRuns(report.tests);
396
+ if (latestRuns.length === 0) return "none";
397
+ if (latestRuns.some((entry) => entry.test.status === "failed")) return "fail";
398
+ if (latestRuns.some((entry) => entry.test.status === "inconclusive")) return "inconclusive";
376
399
  return "pass";
377
400
  }
378
- function renderVerdictLine(verdict) {
401
+ function verdictLabel(verdict) {
379
402
  switch (verdict) {
380
403
  case "pass":
381
- return "**Verdict: \u2705 PASS**";
404
+ return "\u2705 PASS";
382
405
  case "fail":
383
- return "**Verdict: \u274C FAIL**";
406
+ return "\u274C FAIL";
384
407
  case "inconclusive":
385
- return "**Verdict: \u26A0\uFE0F INCONCLUSIVE**";
408
+ return "\u26A0\uFE0F INCONCLUSIVE";
386
409
  case "none":
387
410
  return null;
388
411
  }
389
412
  }
413
+ function renderSummaryHeadline(verdict, counts) {
414
+ const { total, passed, failed, inconclusive } = counts;
415
+ const tally = `${total} ${total === 1 ? "test" : "tests"} ran \xB7 ${passed} passed / ${failed} failed / ${inconclusive} inconclusive`;
416
+ const label = verdictLabel(verdict);
417
+ return label ? `**${label}** \u2014 ${tally}` : `**${tally}**`;
418
+ }
390
419
  function endingScreenshot(test) {
391
420
  if (test.steps.length === 0) {
392
421
  return null;
@@ -400,9 +429,13 @@ function endingScreenshot(test) {
400
429
  }
401
430
  return test.steps[test.steps.length - 1];
402
431
  }
432
+ function failureOrdinal(test, failureStepIndex) {
433
+ const position = test.steps.findIndex((s) => s.stepIndex === failureStepIndex);
434
+ return position === -1 ? failureStepIndex : position + 1;
435
+ }
403
436
  function defaultEndingCaption(test) {
404
437
  if (test.status === "failed") {
405
- return `Failure at step ${test.failureStepIndex}`;
438
+ return `Failure at step ${failureOrdinal(test, test.failureStepIndex)}`;
406
439
  }
407
440
  if (test.status === "inconclusive") {
408
441
  return "Last frame before run was cut short";
@@ -431,88 +464,93 @@ function fullSizeImage(url, alt) {
431
464
  function safeInlineCode(s) {
432
465
  return s.replace(/`/g, "\u2018");
433
466
  }
434
- function buildTestNumbering(report) {
435
- const map = /* @__PURE__ */ new Map();
436
- report.tests.forEach((t, i) => {
437
- map.set(t, i + 1);
438
- });
439
- return map;
440
- }
441
467
  function renderOverview(report) {
442
- const { total, passed, failed, inconclusive } = countTests(report);
443
- const verdict = computeVerdict(report);
444
- const verdictLine = renderVerdictLine(verdict);
445
- const lines = ["## E2E Acceptance Results", ""];
446
- if (verdictLine) {
447
- lines.push(verdictLine, "");
448
- }
449
- lines.push(`**${total} tests ran \u2014 ${passed} passed / ${failed} failed / ${inconclusive} inconclusive**`);
450
- if (total === 0) {
468
+ const latestRuns = resolveLatestRuns(report.tests);
469
+ const counts = countTests(latestRuns);
470
+ const lines = [
471
+ "## E2E Acceptance Results",
472
+ "",
473
+ "### Summary",
474
+ "",
475
+ renderSummaryHeadline(computeVerdict(report), counts)
476
+ ];
477
+ if (counts.total === 0) {
451
478
  lines.push("", "_No tests were executed._");
452
479
  return lines.join("\n");
453
480
  }
454
- lines.push("", "**Tests run:**");
455
- const numbering = buildTestNumbering(report);
456
- const anyGrouped = report.tests.some((t) => Boolean(t.useCaseName));
481
+ lines.push("");
482
+ const numbering = buildTestNumbering(latestRuns);
483
+ const anyGrouped = latestRuns.some((entry) => Boolean(entry.test.useCaseName));
457
484
  if (!anyGrouped) {
458
- for (const t of report.tests) {
459
- lines.push(`- **${numbering.get(t)}.** ${statusEmoji(t)} ${t.name}`);
485
+ for (const entry of latestRuns) {
486
+ lines.push(renderOverviewBullet(entry, numbering, ""));
460
487
  }
461
488
  return lines.join("\n");
462
489
  }
463
- const groupOrder = [];
464
- const groups = /* @__PURE__ */ new Map();
465
- const flat = [];
466
- const seenGroups = /* @__PURE__ */ new Set();
467
- for (const t of report.tests) {
468
- if (t.useCaseName) {
469
- if (!groups.has(t.useCaseName)) {
470
- groups.set(t.useCaseName, []);
471
- groupOrder.push(t.useCaseName);
472
- }
473
- groups.get(t.useCaseName).push(t);
474
- if (!seenGroups.has(t.useCaseName)) {
475
- seenGroups.add(t.useCaseName);
476
- flat.push({ type: "group", key: t.useCaseName });
477
- }
478
- } else {
479
- flat.push({ type: "test", test: t });
480
- }
481
- }
482
- for (const entry of flat) {
490
+ for (const entry of buildGroupedLayout(latestRuns)) {
483
491
  if (entry.type === "test") {
484
- lines.push(`- **${numbering.get(entry.test)}.** ${statusEmoji(entry.test)} ${entry.test.name}`);
492
+ lines.push(renderOverviewBullet(entry.entry, numbering, ""));
485
493
  } else {
486
494
  lines.push(`- **${entry.key}**`);
487
- for (const t of groups.get(entry.key)) {
488
- lines.push(` - **${numbering.get(t)}.** ${statusEmoji(t)} ${t.name}`);
495
+ for (const member of entry.members) {
496
+ lines.push(renderOverviewBullet(member, numbering, " "));
489
497
  }
490
498
  }
491
499
  }
492
500
  return lines.join("\n");
493
501
  }
494
- function renderTestDetails(test, projectId, testNumber, dashboardBaseUrl = DASHBOARD_URL_BASE) {
495
- const summary = renderSummaryLine(test, testNumber);
496
- const frameBlock = renderEndingFrame(test);
497
- const resultLines = renderResultSummary(test, projectId, dashboardBaseUrl);
502
+ function buildTestNumbering(latestRuns) {
503
+ const map = /* @__PURE__ */ new Map();
504
+ latestRuns.forEach((entry, i) => {
505
+ map.set(entry.test.testCaseId, i + 1);
506
+ });
507
+ return map;
508
+ }
509
+ function renderOverviewBullet(entry, numbering, indent) {
510
+ const rerunNote = entry.attempts > 1 ? ` _(${entry.attempts} runs)_` : "";
511
+ return `${indent}- ${statusEmoji(entry.test)} **${numbering.get(entry.test.testCaseId)}.** ${entry.test.name}${rerunNote}`;
512
+ }
513
+ function buildGroupedLayout(latestRuns) {
514
+ const groups = /* @__PURE__ */ new Map();
515
+ const layout = [];
516
+ for (const entry of latestRuns) {
517
+ const useCaseName = entry.test.useCaseName;
518
+ if (!useCaseName) {
519
+ layout.push({ type: "test", entry });
520
+ continue;
521
+ }
522
+ const existing = groups.get(useCaseName);
523
+ if (existing) {
524
+ existing.push(entry);
525
+ continue;
526
+ }
527
+ const members = [entry];
528
+ groups.set(useCaseName, members);
529
+ layout.push({ type: "group", key: useCaseName, members });
530
+ }
531
+ return layout;
532
+ }
533
+ function renderTestDetails(entry, projectId, testNumber, dashboardBaseUrl = DASHBOARD_URL_BASE) {
534
+ const { test } = entry;
498
535
  const body = ["", "<br>", ""];
536
+ body.push(...renderResultSummary(entry, projectId, dashboardBaseUrl));
537
+ const stepList = renderStepList(test.steps);
538
+ if (stepList) {
539
+ body.push("", ...stepList);
540
+ }
541
+ const frameBlock = renderEndingFrame(test);
499
542
  if (frameBlock) {
500
- body.push(...frameBlock, "");
543
+ body.push("", ...frameBlock);
501
544
  }
502
- body.push(...resultLines);
503
545
  return `<details>
504
- <summary>${summary}</summary>
546
+ <summary>${renderSummaryLine(test, testNumber)}</summary>
505
547
  ${body.join("\n")}
506
548
 
507
549
  </details>`;
508
550
  }
509
551
  function renderSummaryLine(test, testNumber) {
510
- const base = `<b>${test.name}</b> ${statusEmoji(test)}`;
511
- const head = "<i>\u25B6 click to expand</i> ";
512
- if (test.description) {
513
- return `<b>${testNumber}. </b>${head}${base} \u2014 ${test.description}`;
514
- }
515
- return `<b>${testNumber}. </b>${head}${base}`;
552
+ const base = `${statusEmoji(test)} <b>${testNumber}. ${test.name}</b>`;
553
+ return test.description ? `${base} \u2014 ${test.description}` : base;
516
554
  }
517
555
  function renderEndingFrame(test) {
518
556
  const frame = endingFrame(test);
@@ -525,22 +563,42 @@ function renderEndingFrame(test) {
525
563
  fullSizeImage(frame.url, test.name)
526
564
  ];
527
565
  }
528
- function renderResultSummary(test, projectId, dashboardBaseUrl) {
566
+ function renderStepList(steps) {
567
+ if (steps.length === 0) {
568
+ return null;
569
+ }
570
+ const entry = (step, position) => `${position}. ${safeInlineCode(step.action)}`;
571
+ if (steps.length <= STEP_LIST_ELISION_THRESHOLD) {
572
+ return steps.map((step, i) => entry(step, i + 1));
573
+ }
574
+ const head = steps.slice(0, STEP_LIST_HEAD_COUNT).map((step, i) => entry(step, i + 1));
575
+ const tailStart = steps.length - STEP_LIST_TAIL_COUNT;
576
+ const tail = steps.slice(tailStart).map((step, i) => entry(step, tailStart + i + 1));
577
+ const hidden = tailStart - STEP_LIST_HEAD_COUNT;
578
+ return [...head, "", `_\u2026 ${hidden} more steps \u2026_`, "", ...tail];
579
+ }
580
+ function renderResultSummary(entry, projectId, dashboardBaseUrl) {
581
+ const { test } = entry;
529
582
  const dashboardUrl = `${dashboardBaseUrl}/${projectId}/scripts?modal=script-details&testCaseId=${encodeURIComponent(test.testCaseId)}`;
530
583
  const lines = [];
531
584
  if (test.status === "passed") {
532
585
  lines.push(`**Result:** \u2705 PASSED`);
533
586
  } else if (test.status === "failed") {
534
- lines.push(`**Result:** \u274C FAILED at step ${test.failureStepIndex}`);
587
+ lines.push(`**Result:** \u274C FAILED at step ${failureOrdinal(test, test.failureStepIndex)}`);
535
588
  lines.push(`**Error:** \`${safeInlineCode(test.error)}\``);
536
589
  } else {
537
590
  lines.push(`**Result:** \u26A0\uFE0F INCONCLUSIVE`);
538
591
  lines.push(`**Reason:** \`${safeInlineCode(test.reason)}\``);
539
592
  }
540
- lines.push(`**Steps:** ${test.steps.length}`);
541
- lines.push(`<a href="${dashboardUrl}" target="_blank" rel="noopener noreferrer">View steps on Muggle AI \u2192</a>`);
593
+ if (entry.attempts > 1) {
594
+ lines.push(`**Runs:** ${entry.attempts} \u2014 showing the latest`);
595
+ }
596
+ lines.push(`**Steps:** ${test.steps.length} \xB7 <a href="${dashboardUrl}" target="_blank" rel="noopener noreferrer">View steps on Muggle AI \u2192</a>`);
542
597
  return lines;
543
598
  }
599
+ function renderDetailBlocks(report, dashboardBaseUrl) {
600
+ return resolveLatestRuns(report.tests).map((entry, i) => renderTestDetails(entry, report.projectId, i + 1, dashboardBaseUrl ?? DASHBOARD_URL_BASE)).join("\n\n");
601
+ }
544
602
  function renderBody(report, opts) {
545
603
  const overview = renderOverview(report);
546
604
  if (report.tests.length === 0) {
@@ -555,26 +613,28 @@ function renderBody(report, opts) {
555
613
  "_Full per-test details in the comment below \u2014 the PR description was too large to inline them._"
556
614
  ].join("\n");
557
615
  }
558
- const detailBlocks = report.tests.map((t, i) => renderTestDetails(t, report.projectId, i + 1, opts.dashboardBaseUrl ?? DASHBOARD_URL_BASE));
559
616
  return [
560
617
  overview,
561
618
  "",
562
619
  "---",
563
620
  "",
564
- detailBlocks.join("\n\n")
621
+ "### Test details",
622
+ "",
623
+ renderDetailBlocks(report, opts.dashboardBaseUrl)
565
624
  ].join("\n");
566
625
  }
567
626
  function renderComment(report, opts = {}) {
568
627
  if (report.tests.length === 0) {
569
628
  return "";
570
629
  }
571
- const detailBlocks = report.tests.map((t, i) => renderTestDetails(t, report.projectId, i + 1, opts.dashboardBaseUrl ?? DASHBOARD_URL_BASE));
572
630
  return [
573
631
  "## E2E acceptance evidence (overflow)",
574
632
  "",
575
633
  "_This comment was posted because the full per-test details did not fit in the PR description._",
576
634
  "",
577
- detailBlocks.join("\n\n")
635
+ "### Test details",
636
+ "",
637
+ renderDetailBlocks(report, opts.dashboardBaseUrl)
578
638
  ].join("\n");
579
639
  }
580
640
 
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { runCli } from './chunk-4M3P45WX.js';
2
+ import { runCli } from './chunk-BA6EYJRB.js';
3
3
  import './chunk-ZA5KHVX5.js';
4
4
 
5
5
  // src/cli/main.ts
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
- export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-4M3P45WX.js';
1
+ export { src_exports as commands, createUnifiedMcpServer, server_exports as server } from './chunk-BA6EYJRB.js';
2
2
  export { createChildLogger, e2e_exports as e2e, getConfig, getLocalQaTools, getLogger, getQaTools, local_exports as localQa, mcp_exports as mcp, e2e_exports as qa, src_exports as shared } from './chunk-ZA5KHVX5.js';
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
4
- "version": "5.20.0-staging.135",
4
+ "version": "5.20.0-staging.137",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
5
- "version": "5.20.0-staging.135",
5
+ "version": "5.20.0-staging.137",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "release": "5.18.0",
3
- "buildId": "run-135-1",
4
- "commitSha": "605057934e0d2ad35b957e511584bfac7cd7d400",
5
- "buildTime": "2026-09-24T17:28:38Z",
3
+ "buildId": "run-137-1",
4
+ "commitSha": "0ac93fb98732cc7c350e2269b19b552438f0fced",
5
+ "buildTime": "2026-09-24T18:14:39Z",
6
6
  "serviceName": "muggle-ai-works-mcp"
7
7
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@muggleai/works",
3
3
  "mcpName": "io.github.multiplex-ai/muggle",
4
- "version": "5.20.0-staging.135",
4
+ "version": "5.20.0-staging.137",
5
5
  "description": "Ship quality products with AI-powered E2E acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
6
6
  "type": "module",
7
7
  "main": "dist/index.js",
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "muggle",
3
3
  "description": "Run real-browser end-to-end (E2E) acceptance tests on your web app from any AI coding agent. Generate test scripts from plain English, replay them on localhost, capture screenshots, and validate user flows like signup, checkout, and dashboards. Works across Claude Code, Cursor, Codex, and Windsurf.",
4
- "version": "5.20.0-staging.135",
4
+ "version": "5.20.0-staging.137",
5
5
  "author": {
6
6
  "name": "Muggle AI",
7
7
  "email": "support@muggle-ai.com"
@@ -2,7 +2,7 @@
2
2
  "name": "muggle",
3
3
  "displayName": "Muggle AI",
4
4
  "description": "Ship quality products with AI-powered end-to-end (E2E) acceptance testing that validates your web app like a real user — from Claude Code and Cursor to PR.",
5
- "version": "5.20.0-staging.135",
5
+ "version": "5.20.0-staging.137",
6
6
  "author": {
7
7
  "name": "Muggle AI",
8
8
  "email": "support@muggle-ai.com"