qat-cli 0.2.98 → 0.3.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/dist/index.js CHANGED
@@ -1374,6 +1374,7 @@ function aggregateResults(results) {
1374
1374
  pending: 0
1375
1375
  };
1376
1376
  const byType = {};
1377
+ let coverage;
1377
1378
  for (const result of results) {
1378
1379
  const typeKey = result.type;
1379
1380
  if (!byType[typeKey]) {
@@ -1397,6 +1398,16 @@ function aggregateResults(results) {
1397
1398
  }
1398
1399
  }
1399
1400
  }
1401
+ if (result.coverage) {
1402
+ if (!coverage) {
1403
+ coverage = { ...result.coverage };
1404
+ } else {
1405
+ coverage.lines = Math.max(coverage.lines, result.coverage.lines);
1406
+ coverage.statements = Math.max(coverage.statements, result.coverage.statements);
1407
+ coverage.functions = Math.max(coverage.functions, result.coverage.functions);
1408
+ coverage.branches = Math.max(coverage.branches, result.coverage.branches);
1409
+ }
1410
+ }
1400
1411
  }
1401
1412
  const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
1402
1413
  return {
@@ -1404,7 +1415,8 @@ function aggregateResults(results) {
1404
1415
  duration: totalDuration,
1405
1416
  results,
1406
1417
  summary,
1407
- byType
1418
+ byType,
1419
+ coverage: coverage?.lines ? coverage : void 0
1408
1420
  };
1409
1421
  }
1410
1422
  function formatDuration(ms) {
@@ -1424,56 +1436,149 @@ function formatTimestamp(ts) {
1424
1436
  second: "2-digit"
1425
1437
  });
1426
1438
  }
1427
- function renderSuiteHTML(suite) {
1428
- const statusClass = suite.status === "passed" ? "passed" : suite.status === "failed" ? "failed" : "skipped";
1429
- const testsHTML = suite.tests.map((test) => {
1430
- const testStatusClass = test.status;
1431
- const errorHTML = test.error ? `<div class="error-message"><strong>${escapeHTML(test.error.message)}</strong>${test.error.stack ? `
1432
- ${escapeHTML(test.error.stack)}` : ""}${test.error.expected && test.error.actual ? `
1439
+ function pct(value) {
1440
+ return `${(value * 100).toFixed(1)}%`;
1441
+ }
1442
+ function renderCoverageMD(coverage) {
1443
+ return `
1444
+ ### \u8986\u76D6\u7387
1433
1445
 
1434
- Expected: ${escapeHTML(test.error.expected)}
1435
- Actual: ${escapeHTML(test.error.actual)}` : ""}</div>` : "";
1436
- return `<div class="test-item">
1437
- <span class="status-dot ${testStatusClass}"></span>
1438
- <span class="test-name">${escapeHTML(test.name)}</span>
1439
- <span class="duration">${formatDuration(test.duration)}</span>
1440
- ${test.retries > 0 ? `<span class="retries">\u91CD\u8BD5 ${test.retries} \u6B21</span>` : ""}
1441
- </div>
1442
- ${errorHTML}`;
1443
- }).join("");
1444
- return `<div class="suite">
1445
- <div class="suite-header" onclick="this.parentElement.classList.toggle('collapsed')">
1446
- <div>
1447
- <span class="status-dot ${statusClass}"></span>
1448
- <strong>${escapeHTML(suite.name)}</strong>
1449
- <span class="suite-file">${escapeHTML(suite.file)}</span>
1450
- </div>
1451
- <div>
1452
- <span class="duration">${formatDuration(suite.duration)}</span>
1453
- <span class="toggle-icon">\u25BC</span>
1454
- </div>
1455
- </div>
1456
- <div class="suite-body">${testsHTML}</div>
1457
- </div>`;
1458
- }
1459
- function escapeHTML(str) {
1460
- return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
1446
+ | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
1447
+ |------|--------|------|
1448
+ | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
1449
+ | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
1450
+ | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
1451
+ | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
1452
+ `;
1453
+ }
1454
+ function renderProgressBar(value) {
1455
+ const filled = Math.round(value * 10);
1456
+ const empty = 10 - filled;
1457
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
1458
+ }
1459
+ var TYPE_LABELS = {
1460
+ unit: "\u5355\u5143\u6D4B\u8BD5",
1461
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5",
1462
+ e2e: "E2E \u6D4B\u8BD5",
1463
+ api: "API \u6D4B\u8BD5",
1464
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
1465
+ performance: "\u6027\u80FD\u6D4B\u8BD5"
1466
+ };
1467
+ function generateMDReport(data) {
1468
+ const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
1469
+ const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
1470
+ const lines = [];
1471
+ lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
1472
+ lines.push("");
1473
+ lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
1474
+ lines.push("");
1475
+ lines.push(`## \u603B\u89C8`);
1476
+ lines.push("");
1477
+ lines.push(`| \u6307\u6807 | \u6570\u503C |`);
1478
+ lines.push(`|------|------|`);
1479
+ lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
1480
+ lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
1481
+ lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
1482
+ if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
1483
+ if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
1484
+ if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
1485
+ lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
1486
+ lines.push("");
1487
+ if (Object.keys(data.byType).length > 0) {
1488
+ lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
1489
+ lines.push("");
1490
+ lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
1491
+ lines.push(`|------|------|------|------|------|--------|`);
1492
+ for (const [type, stats] of Object.entries(data.byType)) {
1493
+ const label = TYPE_LABELS[type] || type;
1494
+ const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
1495
+ lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
1496
+ }
1497
+ lines.push("");
1498
+ }
1499
+ if (data.coverage) {
1500
+ lines.push(renderCoverageMD(data.coverage));
1501
+ lines.push("");
1502
+ }
1503
+ lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
1504
+ lines.push("");
1505
+ for (const result of data.results) {
1506
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
1507
+ const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
1508
+ lines.push(`### ${statusIcon} ${typeLabel}`);
1509
+ lines.push("");
1510
+ if (result.suites.length === 0) {
1511
+ lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
1512
+ lines.push("");
1513
+ continue;
1514
+ }
1515
+ for (const suite of result.suites) {
1516
+ const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
1517
+ lines.push(`#### ${suiteIcon} ${suite.name}`);
1518
+ lines.push("");
1519
+ lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
1520
+ lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
1521
+ lines.push("");
1522
+ if (suite.tests.length > 0) {
1523
+ lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
1524
+ lines.push(`|------|----------|------|`);
1525
+ for (const test of suite.tests) {
1526
+ const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
1527
+ const name = test.error ? `**${test.name}**` : test.name;
1528
+ lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
1529
+ }
1530
+ lines.push("");
1531
+ }
1532
+ const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
1533
+ if (failedTests.length > 0) {
1534
+ lines.push(`<details>`);
1535
+ lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
1536
+ lines.push("");
1537
+ for (const test of failedTests) {
1538
+ lines.push(`**${test.name}**`);
1539
+ lines.push("```");
1540
+ lines.push(test.error.message);
1541
+ if (test.error.stack) {
1542
+ lines.push(test.error.stack);
1543
+ }
1544
+ if (test.error.expected && test.error.actual) {
1545
+ lines.push(`Expected: ${test.error.expected}`);
1546
+ lines.push(`Actual: ${test.error.actual}`);
1547
+ }
1548
+ lines.push("```");
1549
+ lines.push("");
1550
+ }
1551
+ lines.push(`</details>`);
1552
+ lines.push("");
1553
+ }
1554
+ }
1555
+ }
1556
+ lines.push("---");
1557
+ lines.push("");
1558
+ lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
1559
+ return lines.join("\n");
1560
+ }
1561
+ function writeReportToDisk(data, outputDir) {
1562
+ const md = generateMDReport(data);
1563
+ const dir = path4.resolve(outputDir);
1564
+ if (!fs5.existsSync(dir)) {
1565
+ fs5.mkdirSync(dir, { recursive: true });
1566
+ }
1567
+ const mdPath = path4.join(dir, "report.md");
1568
+ fs5.writeFileSync(mdPath, md, "utf-8");
1569
+ const jsonPath = path4.join(dir, "report.json");
1570
+ fs5.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
1571
+ return mdPath;
1461
1572
  }
1462
1573
  function generateHTMLReport(data) {
1463
1574
  const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
1464
- const suitesHTML = data.results.flatMap((r) => r.suites).map(renderSuiteHTML).join("\n");
1465
- const byTypeHTML = Object.entries(data.byType).map(([type, stats]) => {
1466
- const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) : "0";
1467
- return `<div class="type-card">
1468
- <div class="type-name">${type}</div>
1469
- <div class="type-stats">
1470
- <span class="passed">${stats.passed} \u901A\u8FC7</span>
1471
- <span class="failed">${stats.failed} \u5931\u8D25</span>
1472
- <span class="skipped">${stats.skipped} \u8DF3\u8FC7</span>
1473
- </div>
1474
- <div class="type-rate">${rate}%</div>
1475
- </div>`;
1476
- }).join("\n");
1575
+ const md = generateMDReport(data);
1576
+ const html = md.replace(/^### (.+)$/gm, "<h3>$1</h3>").replace(/^## (.+)$/gm, "<h2>$1</h2>").replace(/^# (.+)$/gm, "<h1>$1</h1>").replace(/^> (.+)$/gm, "<blockquote>$1</blockquote>").replace(/\*\*(.+?)\*\*/g, "<strong>$1</strong>").replace(/`([^`]+)`/g, "<code>$1</code>").replace(/^---$/gm, "<hr>").replace(/\|(.+)\|/g, (match) => {
1577
+ const cells = match.split("|").filter(Boolean).map((c) => c.trim());
1578
+ if (cells.every((c) => c.startsWith("-") || c === "")) return "";
1579
+ const tds = cells.map((c) => `<td>${c}</td>`).join("");
1580
+ return `<tr>${tds}</tr>`;
1581
+ }).replace(/<tr>/g, "<table><tr>").replace(/<\/tr>(?!\s*<table>)/g, "</tr></table>").replace(/<\/table>\s*<table>/g, "").replace(/^✅/gm, '<span style="color:#22c55e">\u2705</span>').replace(/^❌/gm, '<span style="color:#ef4444">\u274C</span>').replace(/^⚠️/gm, '<span style="color:#f59e0b">\u26A0\uFE0F</span>');
1477
1582
  return `<!DOCTYPE html>
1478
1583
  <html lang="zh-CN">
1479
1584
  <head>
@@ -1481,162 +1586,60 @@ function generateHTMLReport(data) {
1481
1586
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
1482
1587
  <title>QAT \u6D4B\u8BD5\u62A5\u544A - ${formatTimestamp(data.timestamp)}</title>
1483
1588
  <style>
1484
- :root {
1485
- --color-passed: #22c55e;
1486
- --color-failed: #ef4444;
1487
- --color-skipped: #f59e0b;
1488
- --color-info: #3b82f6;
1489
- --bg-primary: #ffffff;
1490
- --bg-secondary: #f8fafc;
1491
- --text-primary: #1e293b;
1492
- --text-secondary: #64748b;
1493
- --border-color: #e2e8f0;
1494
- }
1495
- * { margin: 0; padding: 0; box-sizing: border-box; }
1496
1589
  body {
1497
1590
  font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
1498
- background: var(--bg-secondary);
1499
- color: var(--text-primary);
1500
- line-height: 1.6;
1501
- }
1502
- .container { max-width: 1200px; margin: 0 auto; padding: 20px; }
1503
- header {
1504
- background: white;
1505
- border-bottom: 1px solid var(--border-color);
1506
- padding: 20px;
1507
- margin-bottom: 20px;
1508
- border-radius: 8px;
1509
- }
1510
- header h1 { font-size: 24px; font-weight: 600; }
1511
- header .meta { color: var(--text-secondary); font-size: 14px; margin-top: 4px; }
1512
- .summary { display: flex; gap: 16px; margin: 20px 0; flex-wrap: wrap; }
1513
- .card {
1514
- padding: 16px 24px; border-radius: 8px; color: white;
1515
- font-weight: 600; min-width: 120px; text-align: center;
1516
- }
1517
- .card.passed { background: var(--color-passed); }
1518
- .card.failed { background: var(--color-failed); }
1519
- .card.skipped { background: var(--color-skipped); }
1520
- .card.total { background: var(--color-info); }
1521
- .card .card-value { font-size: 28px; }
1522
- .card .card-label { font-size: 13px; opacity: 0.9; }
1523
- .pass-rate {
1524
- font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
1525
- color: ${parseFloat(passRate) >= 80 ? "var(--color-passed)" : parseFloat(passRate) >= 50 ? "var(--color-skipped)" : "var(--color-failed)"};
1526
- }
1527
- .pass-rate-label { text-align: center; color: var(--text-secondary); margin-bottom: 20px; }
1528
- .by-type { display: flex; gap: 12px; flex-wrap: wrap; margin: 20px 0; }
1529
- .type-card {
1530
- background: white; border: 1px solid var(--border-color); border-radius: 8px;
1531
- padding: 12px 16px; min-width: 180px;
1532
- }
1533
- .type-name { font-weight: 600; text-transform: capitalize; margin-bottom: 4px; }
1534
- .type-stats { font-size: 13px; }
1535
- .type-stats span { margin-right: 8px; }
1536
- .type-stats .passed { color: var(--color-passed); }
1537
- .type-stats .failed { color: var(--color-failed); }
1538
- .type-stats .skipped { color: var(--color-skipped); }
1539
- .type-rate { font-size: 20px; font-weight: 700; margin-top: 4px; }
1540
- .suite {
1541
- background: white; border: 1px solid var(--border-color);
1542
- border-radius: 8px; margin-bottom: 12px; overflow: hidden;
1543
- }
1544
- .suite-header {
1545
- padding: 12px 16px; display: flex; justify-content: space-between;
1546
- align-items: center; cursor: pointer; user-select: none;
1547
- }
1548
- .suite-header:hover { background: var(--bg-secondary); }
1549
- .suite-header div { display: flex; align-items: center; gap: 8px; }
1550
- .suite.collapsed .suite-body { display: none; }
1551
- .suite-file { color: var(--text-secondary); font-size: 12px; }
1552
- .toggle-icon { font-size: 12px; color: var(--text-secondary); transition: transform 0.2s; }
1553
- .suite.collapsed .toggle-icon { transform: rotate(-90deg); }
1554
- .suite-body { border-top: 1px solid var(--border-color); padding: 8px 16px; }
1555
- .test-item {
1556
- padding: 8px 0; display: flex; align-items: center; gap: 8px;
1557
- }
1558
- .test-item + .test-item { border-top: 1px solid var(--border-color); }
1559
- .status-dot {
1560
- width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;
1561
- }
1562
- .status-dot.passed { background: var(--color-passed); }
1563
- .status-dot.failed { background: var(--color-failed); }
1564
- .status-dot.skipped { background: var(--color-skipped); }
1565
- .status-dot.pending { background: var(--text-secondary); }
1566
- .test-name { flex: 1; }
1567
- .duration { color: var(--text-secondary); font-size: 13px; }
1568
- .retries { color: var(--color-skipped); font-size: 12px; }
1569
- .error-message {
1570
- background: #fef2f2; color: #991b1b; padding: 12px;
1571
- border-radius: 4px; font-family: 'Fira Code', monospace;
1572
- font-size: 13px; margin: 8px 0 8px 16px; white-space: pre-wrap;
1573
- word-break: break-all;
1574
- }
1575
- footer {
1576
- text-align: center; padding: 20px; color: var(--text-secondary);
1577
- font-size: 13px; margin-top: 40px;
1578
- }
1591
+ max-width: 960px; margin: 0 auto; padding: 20px;
1592
+ background: #f8fafc; color: #1e293b; line-height: 1.6;
1593
+ }
1594
+ h1 { border-bottom: 2px solid #3b82f6; padding-bottom: 8px; }
1595
+ h2 { margin-top: 24px; color: #1e40af; }
1596
+ h3 { margin-top: 16px; color: #334155; }
1597
+ h4 { margin-top: 12px; color: #475569; }
1598
+ blockquote { color: #64748b; border-left: 3px solid #cbd5e1; padding-left: 12px; margin: 8px 0; }
1599
+ table { border-collapse: collapse; width: 100%; margin: 12px 0; }
1600
+ td, th { border: 1px solid #e2e8f0; padding: 8px 12px; text-align: left; }
1601
+ tr:nth-child(even) { background: #f1f5f9; }
1602
+ code { background: #e2e8f0; padding: 2px 6px; border-radius: 3px; font-size: 0.9em; }
1603
+ pre { background: #1e293b; color: #e2e8f0; padding: 16px; border-radius: 8px; overflow-x: auto; }
1604
+ pre code { background: none; padding: 0; }
1605
+ hr { border: none; border-top: 1px solid #e2e8f0; margin: 24px 0; }
1606
+ details { margin: 8px 0; }
1607
+ summary { cursor: pointer; color: #ef4444; font-weight: 600; }
1608
+ strong { color: #1e293b; }
1609
+ .pass-rate { font-size: 48px; font-weight: 700; text-align: center; margin: 20px 0;
1610
+ color: ${parseFloat(passRate) >= 80 ? "#22c55e" : parseFloat(passRate) >= 50 ? "#f59e0b" : "#ef4444"};
1611
+ }
1612
+ .pass-rate-label { text-align: center; color: #64748b; margin-bottom: 20px; }
1579
1613
  </style>
1580
1614
  </head>
1581
1615
  <body>
1582
- <div class="container">
1583
- <header>
1584
- <h1>QAT \u6D4B\u8BD5\u62A5\u544A</h1>
1585
- <div class="meta">\u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}</div>
1586
- </header>
1587
-
1588
- <div class="pass-rate">${passRate}%</div>
1589
- <div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
1590
-
1591
- <div class="summary">
1592
- <div class="card total">
1593
- <div class="card-value">${data.summary.total}</div>
1594
- <div class="card-label">\u603B\u8BA1</div>
1595
- </div>
1596
- <div class="card passed">
1597
- <div class="card-value">${data.summary.passed}</div>
1598
- <div class="card-label">\u901A\u8FC7</div>
1599
- </div>
1600
- <div class="card failed">
1601
- <div class="card-value">${data.summary.failed}</div>
1602
- <div class="card-label">\u5931\u8D25</div>
1603
- </div>
1604
- <div class="card skipped">
1605
- <div class="card-value">${data.summary.skipped}</div>
1606
- <div class="card-label">\u8DF3\u8FC7</div>
1607
- </div>
1608
- </div>
1609
-
1610
- ${Object.keys(data.byType).length > 0 ? `<h2 style="margin-top:30px;margin-bottom:10px;">\u6309\u7C7B\u578B\u7EDF\u8BA1</h2><div class="by-type">${byTypeHTML}</div>` : ""}
1611
-
1612
- <h2 style="margin-top:30px;margin-bottom:10px;">\u6D4B\u8BD5\u8BE6\u60C5</h2>
1613
- ${suitesHTML || '<p style="color:var(--text-secondary)">\u6682\u65E0\u6D4B\u8BD5\u7ED3\u679C</p>'}
1614
-
1615
- <footer>\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210</footer>
1616
- </div>
1616
+ <div class="pass-rate">${passRate}%</div>
1617
+ <div class="pass-rate-label">\u6D4B\u8BD5\u901A\u8FC7\u7387</div>
1618
+ ${html}
1617
1619
  </body>
1618
1620
  </html>`;
1619
1621
  }
1620
- function writeReportToDisk(data, outputDir) {
1621
- const html = generateHTMLReport(data);
1622
- const dir = path4.resolve(outputDir);
1623
- if (!fs5.existsSync(dir)) {
1624
- fs5.mkdirSync(dir, { recursive: true });
1625
- }
1626
- const indexPath = path4.join(dir, "index.html");
1627
- fs5.writeFileSync(indexPath, html, "utf-8");
1628
- return indexPath;
1629
- }
1630
1622
 
1631
1623
  // src/runners/vitest-runner.ts
1632
1624
  import { execFile } from "child_process";
1633
1625
  import path5 from "path";
1626
+ import fs6 from "fs";
1627
+ import os from "os";
1628
+ var isVerbose = () => process.env.QAT_VERBOSE === "true";
1629
+ function debug(label, ...args) {
1630
+ if (isVerbose()) {
1631
+ console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
1632
+ }
1633
+ }
1634
1634
  async function runVitest(options) {
1635
1635
  const startTime = Date.now();
1636
1636
  const args = buildVitestArgs(options);
1637
+ debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
1637
1638
  try {
1638
1639
  const result = await execVitest(args);
1639
1640
  const endTime = Date.now();
1641
+ debug("vitest", `\u89E3\u6790\u7ED3\u679C: ${result.suites.length} \u4E2A\u5957\u4EF6, ${result.suites.reduce((s, su) => s + su.tests.length, 0)} \u4E2A\u7528\u4F8B`);
1642
+ debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
1640
1643
  return {
1641
1644
  type: options.type,
1642
1645
  status: result.success ? "passed" : "failed",
@@ -1701,83 +1704,228 @@ function buildVitestArgs(options) {
1701
1704
  return args;
1702
1705
  }
1703
1706
  async function execVitest(args) {
1707
+ const tmpFile = path5.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
1708
+ const argsWithOutput = [...args, "--outputFile", tmpFile];
1704
1709
  return new Promise((resolve, reject) => {
1705
1710
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
1706
- const child = execFile(npx, args, {
1711
+ debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
1712
+ const child = execFile(npx, argsWithOutput, {
1707
1713
  cwd: process.cwd(),
1708
- env: { ...process.env, FORCE_COLOR: "0" },
1714
+ env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
1709
1715
  maxBuffer: 50 * 1024 * 1024,
1710
- // 50MB
1711
1716
  shell: true
1712
1717
  }, (error, stdout, stderr) => {
1713
- const output = stdout || stderr || "";
1718
+ const rawOutput = stdout || stderr || "";
1719
+ const exitCode = error && "code" in error ? error.code : 0;
1720
+ debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
1721
+ debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
1722
+ let jsonResult = null;
1714
1723
  try {
1715
- const parsed = parseVitestJSONOutput(output);
1716
- resolve(parsed);
1717
- } catch {
1718
- if (output) {
1719
- resolve(parseVitestTextOutput(output, !!error));
1720
- } else if (error && error.message.includes("ENOENT")) {
1721
- reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
1724
+ if (fs6.existsSync(tmpFile)) {
1725
+ jsonResult = fs6.readFileSync(tmpFile, "utf-8");
1726
+ debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
1727
+ debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
1728
+ fs6.unlinkSync(tmpFile);
1722
1729
  } else {
1723
- resolve({ success: !error, suites: [] });
1730
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
1724
1731
  }
1732
+ } catch (e) {
1733
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1725
1734
  }
1735
+ if (jsonResult) {
1736
+ try {
1737
+ const parsed = parseVitestJSON(jsonResult);
1738
+ debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1739
+ resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
1740
+ return;
1741
+ } catch (e) {
1742
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1743
+ }
1744
+ }
1745
+ debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
1746
+ try {
1747
+ const parsed = parseFromStdout(rawOutput);
1748
+ debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1749
+ resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
1750
+ return;
1751
+ } catch (e) {
1752
+ debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1753
+ }
1754
+ debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
1755
+ if (rawOutput) {
1756
+ const parsed = parseVitestTextOutput(rawOutput, !!error);
1757
+ debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1758
+ resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
1759
+ return;
1760
+ }
1761
+ if (error && error.message.includes("ENOENT")) {
1762
+ reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
1763
+ return;
1764
+ }
1765
+ debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
1766
+ resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
1726
1767
  });
1727
1768
  child.on("error", (err) => {
1769
+ try {
1770
+ fs6.unlinkSync(tmpFile);
1771
+ } catch {
1772
+ }
1728
1773
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
1729
1774
  });
1730
1775
  });
1731
1776
  }
1732
- function parseVitestJSONOutput(output) {
1733
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
1734
- if (!jsonMatch) {
1735
- return parseVitestTextOutput(output, false);
1777
+ function parseVitestJSON(jsonStr) {
1778
+ const data = JSON.parse(jsonStr);
1779
+ const suites = [];
1780
+ debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
1781
+ if (data.testResults && Array.isArray(data.testResults)) {
1782
+ debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
1783
+ for (const fileResult of data.testResults) {
1784
+ const suiteTests = parseTestResults(fileResult);
1785
+ suites.push({
1786
+ name: path5.basename(fileResult.name || "unknown"),
1787
+ file: fileResult.name || "unknown",
1788
+ type: "unit",
1789
+ status: mapVitestStatus(fileResult.status),
1790
+ duration: fileResult.duration || 0,
1791
+ tests: suiteTests
1792
+ });
1793
+ }
1736
1794
  }
1737
- try {
1795
+ if (suites.length === 0 && data.numTotalTests !== void 0) {
1796
+ debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
1797
+ suites.push({
1798
+ name: "Vitest Results",
1799
+ file: "unknown",
1800
+ type: "unit",
1801
+ status: data.numFailedTests > 0 ? "failed" : "passed",
1802
+ duration: 0,
1803
+ tests: buildTestsFromSummary(data)
1804
+ });
1805
+ }
1806
+ if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
1807
+ debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
1808
+ for (const suiteData of data.suites) {
1809
+ const suiteTests = [];
1810
+ for (const test of suiteData.tests || []) {
1811
+ suiteTests.push({
1812
+ name: test.name || test.title || "unknown",
1813
+ file: test.file || suiteData.file || "unknown",
1814
+ status: mapVitestStatus(test.status || test.result?.status),
1815
+ duration: test.duration || test.result?.duration || 0,
1816
+ error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
1817
+ retries: 0
1818
+ });
1819
+ }
1820
+ suites.push({
1821
+ name: suiteData.name || "unknown",
1822
+ file: suiteData.file || "unknown",
1823
+ type: "unit",
1824
+ status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
1825
+ duration: 0,
1826
+ tests: suiteTests
1827
+ });
1828
+ }
1829
+ }
1830
+ let coverage;
1831
+ if (data.coverageMap) {
1832
+ coverage = extractCoverage(data.coverageMap);
1833
+ }
1834
+ if (!coverage && data.coverage && typeof data.coverage === "object") {
1835
+ const cov = data.coverage;
1836
+ const totals = cov.totals || cov;
1837
+ const getVal = (key) => {
1838
+ const v = totals[key];
1839
+ return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
1840
+ };
1841
+ coverage = {
1842
+ lines: getVal("lines"),
1843
+ statements: getVal("statements"),
1844
+ functions: getVal("functions"),
1845
+ branches: getVal("branches")
1846
+ };
1847
+ }
1848
+ const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
1849
+ return { success, suites, coverage };
1850
+ }
1851
+ function parseTestResults(fileResult) {
1852
+ const tests = [];
1853
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
1854
+ for (const assertion of assertions) {
1855
+ tests.push({
1856
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
1857
+ file: fileResult.name || "unknown",
1858
+ status: mapVitestStatus(assertion.status),
1859
+ duration: assertion.duration || 0,
1860
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
1861
+ retries: 0
1862
+ });
1863
+ }
1864
+ if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
1865
+ tests.push(...buildTestsFromSummary(fileResult));
1866
+ }
1867
+ return tests;
1868
+ }
1869
+ function buildTestsFromSummary(data) {
1870
+ const tests = [];
1871
+ const counts = [
1872
+ [data.numPassedTests || 0, "passed"],
1873
+ [data.numFailedTests || 0, "failed"],
1874
+ [data.numPendingTests || 0, "skipped"]
1875
+ ];
1876
+ for (const [n, s] of counts) {
1877
+ for (let i = 0; i < n; i++) {
1878
+ tests.push({
1879
+ name: `${s} test ${i + 1}`,
1880
+ file: data.name || "unknown",
1881
+ status: s,
1882
+ duration: 0,
1883
+ retries: 0
1884
+ });
1885
+ }
1886
+ }
1887
+ return tests;
1888
+ }
1889
+ function parseFromStdout(output) {
1890
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
1891
+ if (jsonMatch) {
1738
1892
  const data = JSON.parse(jsonMatch[0]);
1739
1893
  const suites = [];
1740
1894
  if (data.testResults && Array.isArray(data.testResults)) {
1741
1895
  for (const fileResult of data.testResults) {
1742
- const suite = {
1743
- name: path5.basename(fileResult.name || fileResult.assertionResults?.[0]?.ancestorTitles?.[0] || "unknown"),
1896
+ suites.push({
1897
+ name: path5.basename(fileResult.name || "unknown"),
1744
1898
  file: fileResult.name || "unknown",
1745
1899
  type: "unit",
1746
1900
  status: mapVitestStatus(fileResult.status),
1747
1901
  duration: fileResult.duration || 0,
1748
- tests: (fileResult.assertionResults || []).map((assertion) => ({
1749
- name: assertion.title || assertion.fullName || "unknown",
1750
- file: fileResult.name || "unknown",
1751
- status: mapVitestStatus(assertion.status),
1752
- duration: assertion.duration || 0,
1753
- error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
1754
- retries: 0
1755
- }))
1756
- };
1757
- suites.push(suite);
1902
+ tests: parseTestResults(fileResult)
1903
+ });
1758
1904
  }
1759
1905
  }
1760
- let coverage;
1761
- if (data.coverageMap) {
1762
- coverage = extractCoverage(data.coverageMap);
1763
- }
1764
1906
  const success = data.success !== false && suites.every((s) => s.status !== "failed");
1907
+ let coverage;
1908
+ if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
1765
1909
  return { success, suites, coverage };
1766
- } catch {
1767
- return parseVitestTextOutput(output, false);
1768
1910
  }
1911
+ const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
1912
+ if (anyJsonMatch) {
1913
+ return parseVitestJSON(anyJsonMatch[0]);
1914
+ }
1915
+ throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
1769
1916
  }
1770
1917
  function parseVitestTextOutput(output, hasError) {
1771
1918
  const suites = [];
1919
+ debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
1920
+ const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
1921
+ const lines = output.split("\n");
1772
1922
  let totalPassed = 0;
1773
1923
  let totalFailed = 0;
1774
- const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
1775
- const lines = output.split("\n");
1776
1924
  for (const line of lines) {
1777
1925
  const match = line.match(suiteRegex);
1778
1926
  if (match) {
1779
1927
  const file = match[1];
1780
- const testCount = parseInt(match[2], 10);
1928
+ const testCount = parseInt(match[4], 10);
1781
1929
  const isPassed = line.includes("\u2713");
1782
1930
  if (isPassed) totalPassed += testCount;
1783
1931
  else totalFailed += testCount;
@@ -1797,15 +1945,39 @@ function parseVitestTextOutput(output, hasError) {
1797
1945
  });
1798
1946
  }
1799
1947
  }
1948
+ if (suites.length === 0) {
1949
+ const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
1950
+ if (summaryMatch) {
1951
+ const count = parseInt(summaryMatch[1], 10);
1952
+ const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
1953
+ suites.push({
1954
+ name: "Vitest Summary",
1955
+ file: "unknown",
1956
+ type: "unit",
1957
+ status,
1958
+ duration: 0,
1959
+ tests: Array.from({ length: count }, (_, i) => ({
1960
+ name: `test ${i + 1}`,
1961
+ file: "unknown",
1962
+ status,
1963
+ duration: 0,
1964
+ retries: 0
1965
+ }))
1966
+ });
1967
+ }
1968
+ }
1969
+ debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
1800
1970
  return {
1801
1971
  success: !hasError || totalFailed === 0,
1802
1972
  suites
1803
1973
  };
1804
1974
  }
1805
1975
  function mapVitestStatus(status) {
1976
+ if (!status) return "pending";
1806
1977
  switch (status) {
1807
1978
  case "passed":
1808
1979
  case "pass":
1980
+ case "done":
1809
1981
  return "passed";
1810
1982
  case "failed":
1811
1983
  case "fail":
@@ -1813,6 +1985,7 @@ function mapVitestStatus(status) {
1813
1985
  case "skipped":
1814
1986
  case "skip":
1815
1987
  case "pending":
1988
+ case "todo":
1816
1989
  return "skipped";
1817
1990
  default:
1818
1991
  return "pending";
@@ -2749,7 +2922,7 @@ async function testAIConnection(config) {
2749
2922
  }
2750
2923
 
2751
2924
  // src/services/mock-server.ts
2752
- import fs6 from "fs";
2925
+ import fs7 from "fs";
2753
2926
  import path7 from "path";
2754
2927
  var serverState = {
2755
2928
  running: false,
@@ -2762,18 +2935,18 @@ function getMockServerState() {
2762
2935
  }
2763
2936
  async function loadMockRoutes(routesDir) {
2764
2937
  const absDir = path7.resolve(process.cwd(), routesDir);
2765
- if (!fs6.existsSync(absDir)) {
2938
+ if (!fs7.existsSync(absDir)) {
2766
2939
  return [];
2767
2940
  }
2768
2941
  const routes = [];
2769
- const entries = fs6.readdirSync(absDir, { withFileTypes: true });
2942
+ const entries = fs7.readdirSync(absDir, { withFileTypes: true });
2770
2943
  for (const entry of entries) {
2771
2944
  if (!entry.isFile()) continue;
2772
2945
  const filePath = path7.join(absDir, entry.name);
2773
2946
  const ext = path7.extname(entry.name);
2774
2947
  try {
2775
2948
  if (ext === ".json") {
2776
- const content = fs6.readFileSync(filePath, "utf-8");
2949
+ const content = fs7.readFileSync(filePath, "utf-8");
2777
2950
  const parsed = JSON.parse(content);
2778
2951
  if (Array.isArray(parsed)) {
2779
2952
  routes.push(...parsed);
@@ -2965,11 +3138,11 @@ function generateMockRouteTemplate(name) {
2965
3138
  }
2966
3139
  function initMockRoutesDir(routesDir) {
2967
3140
  const absDir = path7.resolve(process.cwd(), routesDir);
2968
- if (!fs6.existsSync(absDir)) {
2969
- fs6.mkdirSync(absDir, { recursive: true });
3141
+ if (!fs7.existsSync(absDir)) {
3142
+ fs7.mkdirSync(absDir, { recursive: true });
2970
3143
  }
2971
3144
  const examplePath = path7.join(absDir, "example.json");
2972
- if (!fs6.existsSync(examplePath)) {
3145
+ if (!fs7.existsSync(examplePath)) {
2973
3146
  const exampleRoutes = [
2974
3147
  {
2975
3148
  method: "GET",
@@ -2993,24 +3166,24 @@ function initMockRoutesDir(routesDir) {
2993
3166
  }
2994
3167
  }
2995
3168
  ];
2996
- fs6.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
3169
+ fs7.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
2997
3170
  }
2998
3171
  }
2999
3172
 
3000
3173
  // src/services/visual.ts
3001
- import fs7 from "fs";
3174
+ import fs8 from "fs";
3002
3175
  import path8 from "path";
3003
3176
  import pixelmatch from "pixelmatch";
3004
3177
  import { PNG } from "pngjs";
3005
3178
  function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
3006
- if (!fs7.existsSync(baselinePath)) {
3179
+ if (!fs8.existsSync(baselinePath)) {
3007
3180
  throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
3008
3181
  }
3009
- if (!fs7.existsSync(currentPath)) {
3182
+ if (!fs8.existsSync(currentPath)) {
3010
3183
  throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
3011
3184
  }
3012
- const baseline = PNG.sync.read(fs7.readFileSync(baselinePath));
3013
- const current = PNG.sync.read(fs7.readFileSync(currentPath));
3185
+ const baseline = PNG.sync.read(fs8.readFileSync(baselinePath));
3186
+ const current = PNG.sync.read(fs8.readFileSync(currentPath));
3014
3187
  if (baseline.width !== current.width || baseline.height !== current.height) {
3015
3188
  return {
3016
3189
  passed: false,
@@ -3039,10 +3212,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
3039
3212
  let diffPath;
3040
3213
  if (diffPixels > 0) {
3041
3214
  const diffDir = path8.dirname(diffOutputPath);
3042
- if (!fs7.existsSync(diffDir)) {
3043
- fs7.mkdirSync(diffDir, { recursive: true });
3215
+ if (!fs8.existsSync(diffDir)) {
3216
+ fs8.mkdirSync(diffDir, { recursive: true });
3044
3217
  }
3045
- fs7.writeFileSync(diffOutputPath, PNG.sync.write(diff));
3218
+ fs8.writeFileSync(diffOutputPath, PNG.sync.write(diff));
3046
3219
  diffPath = diffOutputPath;
3047
3220
  }
3048
3221
  return {
@@ -3056,14 +3229,14 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
3056
3229
  };
3057
3230
  }
3058
3231
  function createBaseline(currentPath, baselinePath) {
3059
- if (!fs7.existsSync(currentPath)) {
3232
+ if (!fs8.existsSync(currentPath)) {
3060
3233
  throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
3061
3234
  }
3062
3235
  const baselineDir = path8.dirname(baselinePath);
3063
- if (!fs7.existsSync(baselineDir)) {
3064
- fs7.mkdirSync(baselineDir, { recursive: true });
3236
+ if (!fs8.existsSync(baselineDir)) {
3237
+ fs8.mkdirSync(baselineDir, { recursive: true });
3065
3238
  }
3066
- fs7.copyFileSync(currentPath, baselinePath);
3239
+ fs8.copyFileSync(currentPath, baselinePath);
3067
3240
  return baselinePath;
3068
3241
  }
3069
3242
  function updateBaseline(currentPath, baselinePath) {
@@ -3071,69 +3244,69 @@ function updateBaseline(currentPath, baselinePath) {
3071
3244
  }
3072
3245
  function updateAllBaselines(currentDir, baselineDir) {
3073
3246
  const updated = [];
3074
- if (!fs7.existsSync(currentDir)) {
3247
+ if (!fs8.existsSync(currentDir)) {
3075
3248
  return updated;
3076
3249
  }
3077
- const files = fs7.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3250
+ const files = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3078
3251
  for (const file of files) {
3079
3252
  const currentPath = path8.join(currentDir, file);
3080
3253
  const baselinePath = path8.join(baselineDir, file);
3081
3254
  const baselineDirAbs = path8.dirname(baselinePath);
3082
- if (!fs7.existsSync(baselineDirAbs)) {
3083
- fs7.mkdirSync(baselineDirAbs, { recursive: true });
3255
+ if (!fs8.existsSync(baselineDirAbs)) {
3256
+ fs8.mkdirSync(baselineDirAbs, { recursive: true });
3084
3257
  }
3085
- fs7.copyFileSync(currentPath, baselinePath);
3258
+ fs8.copyFileSync(currentPath, baselinePath);
3086
3259
  updated.push(file);
3087
3260
  }
3088
3261
  return updated;
3089
3262
  }
3090
3263
  function cleanBaselines(baselineDir) {
3091
- if (!fs7.existsSync(baselineDir)) {
3264
+ if (!fs8.existsSync(baselineDir)) {
3092
3265
  return 0;
3093
3266
  }
3094
- const files = fs7.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
3267
+ const files = fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
3095
3268
  let count = 0;
3096
3269
  for (const file of files) {
3097
- fs7.unlinkSync(path8.join(baselineDir, file));
3270
+ fs8.unlinkSync(path8.join(baselineDir, file));
3098
3271
  count++;
3099
3272
  }
3100
3273
  return count;
3101
3274
  }
3102
3275
  function cleanDiffs(diffDir) {
3103
- if (!fs7.existsSync(diffDir)) {
3276
+ if (!fs8.existsSync(diffDir)) {
3104
3277
  return 0;
3105
3278
  }
3106
- const files = fs7.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
3279
+ const files = fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
3107
3280
  let count = 0;
3108
3281
  for (const file of files) {
3109
- fs7.unlinkSync(path8.join(diffDir, file));
3282
+ fs8.unlinkSync(path8.join(diffDir, file));
3110
3283
  count++;
3111
3284
  }
3112
3285
  return count;
3113
3286
  }
3114
3287
  function listBaselines(baselineDir) {
3115
- if (!fs7.existsSync(baselineDir)) {
3288
+ if (!fs8.existsSync(baselineDir)) {
3116
3289
  return [];
3117
3290
  }
3118
- return fs7.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
3291
+ return fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
3119
3292
  }
3120
3293
  function listDiffs(diffDir) {
3121
- if (!fs7.existsSync(diffDir)) {
3294
+ if (!fs8.existsSync(diffDir)) {
3122
3295
  return [];
3123
3296
  }
3124
- return fs7.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
3297
+ return fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
3125
3298
  }
3126
3299
  function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
3127
3300
  const results = [];
3128
- if (!fs7.existsSync(currentDir)) {
3301
+ if (!fs8.existsSync(currentDir)) {
3129
3302
  return results;
3130
3303
  }
3131
- const currentFiles = fs7.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3304
+ const currentFiles = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3132
3305
  for (const file of currentFiles) {
3133
3306
  const currentPath = path8.join(currentDir, file);
3134
3307
  const baselinePath = path8.join(baselineDir, file);
3135
3308
  const diffPath = path8.join(diffDir, file);
3136
- if (!fs7.existsSync(baselinePath)) {
3309
+ if (!fs8.existsSync(baselinePath)) {
3137
3310
  createBaseline(currentPath, baselinePath);
3138
3311
  results.push({
3139
3312
  passed: true,
@@ -3165,14 +3338,14 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
3165
3338
  }
3166
3339
 
3167
3340
  // src/services/source-analyzer.ts
3168
- import fs8 from "fs";
3341
+ import fs9 from "fs";
3169
3342
  import path9 from "path";
3170
3343
  function analyzeFile(filePath) {
3171
3344
  const absolutePath = path9.resolve(process.cwd(), filePath);
3172
- if (!fs8.existsSync(absolutePath)) {
3345
+ if (!fs9.existsSync(absolutePath)) {
3173
3346
  return { filePath, exports: [], apiCalls: [] };
3174
3347
  }
3175
- const content = fs8.readFileSync(absolutePath, "utf-8");
3348
+ const content = fs9.readFileSync(absolutePath, "utf-8");
3176
3349
  const ext = path9.extname(filePath);
3177
3350
  const result = {
3178
3351
  filePath,
@@ -3527,13 +3700,13 @@ function cleanTemplateUrl(url) {
3527
3700
  }
3528
3701
  function scanAPICalls(srcDir) {
3529
3702
  const absDir = path9.resolve(process.cwd(), srcDir);
3530
- if (!fs8.existsSync(absDir)) {
3703
+ if (!fs9.existsSync(absDir)) {
3531
3704
  return [];
3532
3705
  }
3533
3706
  const allCalls = [];
3534
3707
  const seen = /* @__PURE__ */ new Set();
3535
3708
  function walk(dir) {
3536
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
3709
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
3537
3710
  for (const entry of entries) {
3538
3711
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
3539
3712
  const fullPath = path9.join(dir, entry.name);
@@ -3541,7 +3714,7 @@ function scanAPICalls(srcDir) {
3541
3714
  walk(fullPath);
3542
3715
  } else if (entry.isFile() && /\.(ts|js|vue|mjs)$/.test(entry.name)) {
3543
3716
  try {
3544
- const content = fs8.readFileSync(fullPath, "utf-8");
3717
+ const content = fs9.readFileSync(fullPath, "utf-8");
3545
3718
  const calls = extractAPICalls(content, path9.relative(process.cwd(), fullPath));
3546
3719
  for (const call of calls) {
3547
3720
  const key = `${call.method}:${call.url}`;