qat-cli 0.3.1 → 0.3.3

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
@@ -955,12 +955,36 @@ Handlebars.registerHelper("propTestValue", (prop) => {
955
955
  };
956
956
  return map[prop.type] || "'test-value'";
957
957
  });
958
+ function resolveImportPath(testType, targetPath) {
959
+ if (!targetPath) return targetPath;
960
+ const testDirMap = {
961
+ unit: "tests/unit",
962
+ component: "tests/component",
963
+ e2e: "tests/e2e",
964
+ api: "tests/api",
965
+ visual: "tests/visual",
966
+ performance: "tests/e2e"
967
+ };
968
+ const testDir = testDirMap[testType];
969
+ if (!testDir) return targetPath;
970
+ if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
971
+ return targetPath;
972
+ }
973
+ const depth = testDir.split("/").length;
974
+ const prefix = "../".repeat(depth);
975
+ let cleanPath = targetPath.replace(/^\.\//, "");
976
+ if (!cleanPath.endsWith(".vue")) {
977
+ cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
978
+ }
979
+ return `${prefix}${cleanPath}`;
980
+ }
958
981
  function registerTemplate(type, templateContent) {
959
982
  customTemplates.set(type, templateContent);
960
983
  }
961
984
  function renderTemplate(type, context) {
962
985
  const templateContent = loadTemplate(type);
963
986
  const template = Handlebars.compile(templateContent);
987
+ const resolvedTarget = resolveImportPath(type, context.target);
964
988
  const fullContext = {
965
989
  vueVersion: 3,
966
990
  typescript: true,
@@ -981,6 +1005,8 @@ function renderTemplate(type, context) {
981
1005
  requiredProps: [],
982
1006
  optionalProps: [],
983
1007
  ...context,
1008
+ // 使用计算后的正确路径
1009
+ target: resolvedTarget,
984
1010
  framework: context.framework || "vue",
985
1011
  camelName: context.camelName || toCamelCase(context.name),
986
1012
  pascalName: context.pascalName || toPascalCase(context.name)
@@ -1623,12 +1649,23 @@ function generateHTMLReport(data) {
1623
1649
  // src/runners/vitest-runner.ts
1624
1650
  import { execFile } from "child_process";
1625
1651
  import path5 from "path";
1652
+ import fs6 from "fs";
1653
+ import os from "os";
1654
+ var isVerbose = () => process.env.QAT_VERBOSE === "true";
1655
+ function debug(label, ...args) {
1656
+ if (isVerbose()) {
1657
+ console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
1658
+ }
1659
+ }
1626
1660
  async function runVitest(options) {
1627
1661
  const startTime = Date.now();
1628
1662
  const args = buildVitestArgs(options);
1663
+ debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
1629
1664
  try {
1630
1665
  const result = await execVitest(args);
1631
1666
  const endTime = Date.now();
1667
+ 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`);
1668
+ debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
1632
1669
  return {
1633
1670
  type: options.type,
1634
1671
  status: result.success ? "passed" : "failed",
@@ -1671,14 +1708,14 @@ function buildVitestArgs(options) {
1671
1708
  if (options.files && options.files.length > 0) {
1672
1709
  args.push(...options.files);
1673
1710
  } else {
1674
- const includeMap = {
1675
- unit: ["tests/unit"],
1676
- component: ["tests/component"],
1677
- api: ["tests/api"]
1711
+ const pathMap = {
1712
+ unit: "tests/unit/**/*.test.ts",
1713
+ component: "tests/component/**/*.test.ts",
1714
+ api: "tests/api/**/*.test.ts"
1678
1715
  };
1679
- const includes = includeMap[options.type];
1680
- if (includes) {
1681
- args.push("--include", includes.map((d) => `${d}/**/*.test.ts`).join(","));
1716
+ const testPattern = pathMap[options.type];
1717
+ if (testPattern) {
1718
+ args.push(testPattern);
1682
1719
  }
1683
1720
  }
1684
1721
  if (options.coverage) {
@@ -1693,12 +1730,11 @@ function buildVitestArgs(options) {
1693
1730
  return args;
1694
1731
  }
1695
1732
  async function execVitest(args) {
1696
- const os = await import("os");
1697
- const fs9 = await import("fs");
1698
1733
  const tmpFile = path5.join(os.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
1699
1734
  const argsWithOutput = [...args, "--outputFile", tmpFile];
1700
1735
  return new Promise((resolve, reject) => {
1701
1736
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
1737
+ debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
1702
1738
  const child = execFile(npx, argsWithOutput, {
1703
1739
  cwd: process.cwd(),
1704
1740
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
@@ -1706,85 +1742,113 @@ async function execVitest(args) {
1706
1742
  shell: true
1707
1743
  }, (error, stdout, stderr) => {
1708
1744
  const rawOutput = stdout || stderr || "";
1745
+ const exitCode = error && "code" in error ? error.code : 0;
1746
+ debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
1747
+ debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
1709
1748
  let jsonResult = null;
1710
1749
  try {
1711
- if (fs9.existsSync(tmpFile)) {
1712
- jsonResult = fs9.readFileSync(tmpFile, "utf-8");
1713
- fs9.unlinkSync(tmpFile);
1750
+ if (fs6.existsSync(tmpFile)) {
1751
+ jsonResult = fs6.readFileSync(tmpFile, "utf-8");
1752
+ debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
1753
+ debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
1754
+ fs6.unlinkSync(tmpFile);
1755
+ } else {
1756
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
1714
1757
  }
1715
- } catch {
1758
+ } catch (e) {
1759
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1716
1760
  }
1717
1761
  if (jsonResult) {
1718
1762
  try {
1719
- const parsed = parseVitestJSONResult(jsonResult);
1720
- resolve({ ...parsed, rawOutput });
1763
+ const parsed = parseVitestJSON(jsonResult);
1764
+ debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1765
+ resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
1721
1766
  return;
1722
- } catch {
1767
+ } catch (e) {
1768
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1723
1769
  }
1724
1770
  }
1771
+ debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
1725
1772
  try {
1726
- const parsed = parseVitestJSONOutput(rawOutput);
1727
- resolve({ ...parsed, rawOutput });
1728
- } catch {
1729
- if (rawOutput) {
1730
- resolve({ ...parseVitestTextOutput(rawOutput, !!error), rawOutput });
1731
- } else if (error && error.message.includes("ENOENT")) {
1732
- reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
1733
- } else {
1734
- resolve({ success: !error, suites: [], rawOutput });
1735
- }
1773
+ const parsed = parseFromStdout(rawOutput);
1774
+ debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1775
+ resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
1776
+ return;
1777
+ } catch (e) {
1778
+ debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
1779
+ }
1780
+ debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
1781
+ if (rawOutput) {
1782
+ const parsed = parseVitestTextOutput(rawOutput, !!error);
1783
+ debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
1784
+ resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
1785
+ return;
1736
1786
  }
1787
+ if (error && error.message.includes("ENOENT")) {
1788
+ reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
1789
+ return;
1790
+ }
1791
+ debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
1792
+ resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
1737
1793
  });
1738
1794
  child.on("error", (err) => {
1739
1795
  try {
1740
- fs9.unlinkSync(tmpFile);
1796
+ fs6.unlinkSync(tmpFile);
1741
1797
  } catch {
1742
1798
  }
1743
1799
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
1744
1800
  });
1745
1801
  });
1746
1802
  }
1747
- function parseVitestJSONResult(jsonStr) {
1803
+ function parseVitestJSON(jsonStr) {
1748
1804
  const data = JSON.parse(jsonStr);
1749
1805
  const suites = [];
1806
+ debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
1750
1807
  if (data.testResults && Array.isArray(data.testResults)) {
1808
+ debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
1751
1809
  for (const fileResult of data.testResults) {
1810
+ const suiteTests = parseTestResults(fileResult);
1811
+ suites.push({
1812
+ name: path5.basename(fileResult.name || "unknown"),
1813
+ file: fileResult.name || "unknown",
1814
+ type: "unit",
1815
+ status: mapVitestStatus(fileResult.status),
1816
+ duration: fileResult.duration || 0,
1817
+ tests: suiteTests
1818
+ });
1819
+ }
1820
+ }
1821
+ if (suites.length === 0 && data.numTotalTests !== void 0) {
1822
+ debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
1823
+ suites.push({
1824
+ name: "Vitest Results",
1825
+ file: "unknown",
1826
+ type: "unit",
1827
+ status: data.numFailedTests > 0 ? "failed" : "passed",
1828
+ duration: 0,
1829
+ tests: buildTestsFromSummary(data)
1830
+ });
1831
+ }
1832
+ if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
1833
+ debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
1834
+ for (const suiteData of data.suites) {
1752
1835
  const suiteTests = [];
1753
- const assertions = fileResult.assertionResults || fileResult.tests || [];
1754
- for (const assertion of assertions) {
1836
+ for (const test of suiteData.tests || []) {
1755
1837
  suiteTests.push({
1756
- name: assertion.title || assertion.fullName || assertion.name || "unknown",
1757
- file: fileResult.name || "unknown",
1758
- status: mapVitestStatus(assertion.status),
1759
- duration: assertion.duration || 0,
1760
- error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
1838
+ name: test.name || test.title || "unknown",
1839
+ file: test.file || suiteData.file || "unknown",
1840
+ status: mapVitestStatus(test.status || test.result?.status),
1841
+ duration: test.duration || test.result?.duration || 0,
1842
+ error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
1761
1843
  retries: 0
1762
1844
  });
1763
1845
  }
1764
- if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
1765
- const counts = [
1766
- { n: fileResult.numPassingTests || 0, s: "passed" },
1767
- { n: fileResult.numFailingTests || 0, s: "failed" },
1768
- { n: fileResult.numPendingTests || 0, s: "skipped" }
1769
- ];
1770
- for (const { n, s } of counts) {
1771
- for (let i = 0; i < n; i++) {
1772
- suiteTests.push({
1773
- name: `${s} test ${i + 1}`,
1774
- file: fileResult.name || "unknown",
1775
- status: s,
1776
- duration: 0,
1777
- retries: 0
1778
- });
1779
- }
1780
- }
1781
- }
1782
1846
  suites.push({
1783
- name: path5.basename(fileResult.name || "unknown"),
1784
- file: fileResult.name || "unknown",
1847
+ name: suiteData.name || "unknown",
1848
+ file: suiteData.file || "unknown",
1785
1849
  type: "unit",
1786
- status: mapVitestStatus(fileResult.status),
1787
- duration: fileResult.duration || 0,
1850
+ status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
1851
+ duration: 0,
1788
1852
  tests: suiteTests
1789
1853
  });
1790
1854
  }
@@ -1793,58 +1857,101 @@ function parseVitestJSONResult(jsonStr) {
1793
1857
  if (data.coverageMap) {
1794
1858
  coverage = extractCoverage(data.coverageMap);
1795
1859
  }
1796
- const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
1860
+ if (!coverage && data.coverage && typeof data.coverage === "object") {
1861
+ const cov = data.coverage;
1862
+ const totals = cov.totals || cov;
1863
+ const getVal = (key) => {
1864
+ const v = totals[key];
1865
+ return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
1866
+ };
1867
+ coverage = {
1868
+ lines: getVal("lines"),
1869
+ statements: getVal("statements"),
1870
+ functions: getVal("functions"),
1871
+ branches: getVal("branches")
1872
+ };
1873
+ }
1874
+ const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
1797
1875
  return { success, suites, coverage };
1798
1876
  }
1799
- function parseVitestJSONOutput(output) {
1800
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
1801
- if (!jsonMatch) {
1802
- return parseVitestTextOutput(output, false);
1877
+ function parseTestResults(fileResult) {
1878
+ const tests = [];
1879
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
1880
+ for (const assertion of assertions) {
1881
+ tests.push({
1882
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
1883
+ file: fileResult.name || "unknown",
1884
+ status: mapVitestStatus(assertion.status),
1885
+ duration: assertion.duration || 0,
1886
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
1887
+ retries: 0
1888
+ });
1803
1889
  }
1804
- try {
1890
+ if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
1891
+ tests.push(...buildTestsFromSummary(fileResult));
1892
+ }
1893
+ return tests;
1894
+ }
1895
+ function buildTestsFromSummary(data) {
1896
+ const tests = [];
1897
+ const counts = [
1898
+ [data.numPassedTests || 0, "passed"],
1899
+ [data.numFailedTests || 0, "failed"],
1900
+ [data.numPendingTests || 0, "skipped"]
1901
+ ];
1902
+ for (const [n, s] of counts) {
1903
+ for (let i = 0; i < n; i++) {
1904
+ tests.push({
1905
+ name: `${s} test ${i + 1}`,
1906
+ file: data.name || "unknown",
1907
+ status: s,
1908
+ duration: 0,
1909
+ retries: 0
1910
+ });
1911
+ }
1912
+ }
1913
+ return tests;
1914
+ }
1915
+ function parseFromStdout(output) {
1916
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
1917
+ if (jsonMatch) {
1805
1918
  const data = JSON.parse(jsonMatch[0]);
1806
1919
  const suites = [];
1807
1920
  if (data.testResults && Array.isArray(data.testResults)) {
1808
1921
  for (const fileResult of data.testResults) {
1809
- const suite = {
1810
- name: path5.basename(fileResult.name || fileResult.assertionResults?.[0]?.ancestorTitles?.[0] || "unknown"),
1922
+ suites.push({
1923
+ name: path5.basename(fileResult.name || "unknown"),
1811
1924
  file: fileResult.name || "unknown",
1812
1925
  type: "unit",
1813
1926
  status: mapVitestStatus(fileResult.status),
1814
1927
  duration: fileResult.duration || 0,
1815
- tests: (fileResult.assertionResults || []).map((assertion) => ({
1816
- name: assertion.title || assertion.fullName || "unknown",
1817
- file: fileResult.name || "unknown",
1818
- status: mapVitestStatus(assertion.status),
1819
- duration: assertion.duration || 0,
1820
- error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
1821
- retries: 0
1822
- }))
1823
- };
1824
- suites.push(suite);
1928
+ tests: parseTestResults(fileResult)
1929
+ });
1825
1930
  }
1826
1931
  }
1827
- let coverage;
1828
- if (data.coverageMap) {
1829
- coverage = extractCoverage(data.coverageMap);
1830
- }
1831
1932
  const success = data.success !== false && suites.every((s) => s.status !== "failed");
1933
+ let coverage;
1934
+ if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
1832
1935
  return { success, suites, coverage };
1833
- } catch {
1834
- return parseVitestTextOutput(output, false);
1835
1936
  }
1937
+ const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
1938
+ if (anyJsonMatch) {
1939
+ return parseVitestJSON(anyJsonMatch[0]);
1940
+ }
1941
+ throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
1836
1942
  }
1837
1943
  function parseVitestTextOutput(output, hasError) {
1838
1944
  const suites = [];
1945
+ debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
1946
+ const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
1947
+ const lines = output.split("\n");
1839
1948
  let totalPassed = 0;
1840
1949
  let totalFailed = 0;
1841
- const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
1842
- const lines = output.split("\n");
1843
1950
  for (const line of lines) {
1844
1951
  const match = line.match(suiteRegex);
1845
1952
  if (match) {
1846
1953
  const file = match[1];
1847
- const testCount = parseInt(match[2], 10);
1954
+ const testCount = parseInt(match[4], 10);
1848
1955
  const isPassed = line.includes("\u2713");
1849
1956
  if (isPassed) totalPassed += testCount;
1850
1957
  else totalFailed += testCount;
@@ -1864,15 +1971,39 @@ function parseVitestTextOutput(output, hasError) {
1864
1971
  });
1865
1972
  }
1866
1973
  }
1974
+ if (suites.length === 0) {
1975
+ const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
1976
+ if (summaryMatch) {
1977
+ const count = parseInt(summaryMatch[1], 10);
1978
+ const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
1979
+ suites.push({
1980
+ name: "Vitest Summary",
1981
+ file: "unknown",
1982
+ type: "unit",
1983
+ status,
1984
+ duration: 0,
1985
+ tests: Array.from({ length: count }, (_, i) => ({
1986
+ name: `test ${i + 1}`,
1987
+ file: "unknown",
1988
+ status,
1989
+ duration: 0,
1990
+ retries: 0
1991
+ }))
1992
+ });
1993
+ }
1994
+ }
1995
+ debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
1867
1996
  return {
1868
1997
  success: !hasError || totalFailed === 0,
1869
1998
  suites
1870
1999
  };
1871
2000
  }
1872
2001
  function mapVitestStatus(status) {
2002
+ if (!status) return "pending";
1873
2003
  switch (status) {
1874
2004
  case "passed":
1875
2005
  case "pass":
2006
+ case "done":
1876
2007
  return "passed";
1877
2008
  case "failed":
1878
2009
  case "fail":
@@ -1880,6 +2011,7 @@ function mapVitestStatus(status) {
1880
2011
  case "skipped":
1881
2012
  case "skip":
1882
2013
  case "pending":
2014
+ case "todo":
1883
2015
  return "skipped";
1884
2016
  default:
1885
2017
  return "pending";
@@ -2566,8 +2698,13 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
2566
2698
  6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
2567
2699
  }
2568
2700
  buildGenerateTestUserPrompt(req) {
2701
+ const importPath = this.computeTestImportPath(req.type, req.target);
2569
2702
  let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
2570
2703
  \u76EE\u6807\u6587\u4EF6: ${req.target}
2704
+ \u6D4B\u8BD5\u6587\u4EF6\u5C06\u653E\u5728: ${this.getTestOutputDir(req.type)}/
2705
+ \u6B63\u786E\u7684 import \u8DEF\u5F84: ${importPath}
2706
+
2707
+ \u91CD\u8981\uFF1Aimport \u8BED\u53E5\u4E2D\u5FC5\u987B\u4F7F\u7528\u4E0A\u8FF0\u6B63\u786E\u7684\u76F8\u5BF9\u8DEF\u5F84 ${importPath}\uFF0C\u4E0D\u8981\u4F7F\u7528 ${req.target} \u6216\u5176\u4ED6\u8DEF\u5F84\uFF01
2571
2708
  `;
2572
2709
  if (req.analysis) {
2573
2710
  prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
@@ -2617,6 +2754,46 @@ ${req.context}
2617
2754
  }
2618
2755
  return prompt;
2619
2756
  }
2757
+ /**
2758
+ * 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
2759
+ */
2760
+ computeTestImportPath(testType, targetPath) {
2761
+ if (!targetPath) return targetPath;
2762
+ const testDirMap = {
2763
+ unit: "tests/unit",
2764
+ component: "tests/component",
2765
+ e2e: "tests/e2e",
2766
+ api: "tests/api",
2767
+ visual: "tests/visual",
2768
+ performance: "tests/e2e"
2769
+ };
2770
+ const testDir = testDirMap[testType];
2771
+ if (!testDir) return targetPath;
2772
+ if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
2773
+ return targetPath;
2774
+ }
2775
+ const depth = testDir.split("/").length;
2776
+ const prefix = "../".repeat(depth);
2777
+ let cleanPath = targetPath.replace(/^\.\//, "");
2778
+ if (!cleanPath.endsWith(".vue")) {
2779
+ cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
2780
+ }
2781
+ return `${prefix}${cleanPath}`;
2782
+ }
2783
+ /**
2784
+ * 获取测试输出目录
2785
+ */
2786
+ getTestOutputDir(testType) {
2787
+ const dirMap = {
2788
+ unit: "tests/unit",
2789
+ component: "tests/component",
2790
+ e2e: "tests/e2e",
2791
+ api: "tests/api",
2792
+ visual: "tests/visual",
2793
+ performance: "tests/e2e"
2794
+ };
2795
+ return dirMap[testType] || "tests/unit";
2796
+ }
2620
2797
  parseGenerateTestResponse(content) {
2621
2798
  const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
2622
2799
  const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
@@ -2645,10 +2822,12 @@ ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F
2645
2822
  SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
2646
2823
  }
2647
2824
  buildReviewTestUserPrompt(req) {
2825
+ const importPath = this.computeTestImportPath(req.testType, req.target);
2648
2826
  let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
2649
2827
 
2650
2828
  \u88AB\u6D4B\u6587\u4EF6: ${req.target}
2651
2829
  \u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
2830
+ \u6B63\u786E\u7684 import \u8DEF\u5F84\u5E94\u4E3A: ${importPath}\uFF08\u6D4B\u8BD5\u6587\u4EF6\u4F4D\u4E8E ${this.getTestOutputDir(req.testType)}/\uFF09
2652
2831
 
2653
2832
  --- \u6E90\u7801\u5185\u5BB9 ---
2654
2833
  \`\`\`typescript
@@ -2816,7 +2995,7 @@ async function testAIConnection(config) {
2816
2995
  }
2817
2996
 
2818
2997
  // src/services/mock-server.ts
2819
- import fs6 from "fs";
2998
+ import fs7 from "fs";
2820
2999
  import path7 from "path";
2821
3000
  var serverState = {
2822
3001
  running: false,
@@ -2829,18 +3008,18 @@ function getMockServerState() {
2829
3008
  }
2830
3009
  async function loadMockRoutes(routesDir) {
2831
3010
  const absDir = path7.resolve(process.cwd(), routesDir);
2832
- if (!fs6.existsSync(absDir)) {
3011
+ if (!fs7.existsSync(absDir)) {
2833
3012
  return [];
2834
3013
  }
2835
3014
  const routes = [];
2836
- const entries = fs6.readdirSync(absDir, { withFileTypes: true });
3015
+ const entries = fs7.readdirSync(absDir, { withFileTypes: true });
2837
3016
  for (const entry of entries) {
2838
3017
  if (!entry.isFile()) continue;
2839
3018
  const filePath = path7.join(absDir, entry.name);
2840
3019
  const ext = path7.extname(entry.name);
2841
3020
  try {
2842
3021
  if (ext === ".json") {
2843
- const content = fs6.readFileSync(filePath, "utf-8");
3022
+ const content = fs7.readFileSync(filePath, "utf-8");
2844
3023
  const parsed = JSON.parse(content);
2845
3024
  if (Array.isArray(parsed)) {
2846
3025
  routes.push(...parsed);
@@ -3032,11 +3211,11 @@ function generateMockRouteTemplate(name) {
3032
3211
  }
3033
3212
  function initMockRoutesDir(routesDir) {
3034
3213
  const absDir = path7.resolve(process.cwd(), routesDir);
3035
- if (!fs6.existsSync(absDir)) {
3036
- fs6.mkdirSync(absDir, { recursive: true });
3214
+ if (!fs7.existsSync(absDir)) {
3215
+ fs7.mkdirSync(absDir, { recursive: true });
3037
3216
  }
3038
3217
  const examplePath = path7.join(absDir, "example.json");
3039
- if (!fs6.existsSync(examplePath)) {
3218
+ if (!fs7.existsSync(examplePath)) {
3040
3219
  const exampleRoutes = [
3041
3220
  {
3042
3221
  method: "GET",
@@ -3060,24 +3239,24 @@ function initMockRoutesDir(routesDir) {
3060
3239
  }
3061
3240
  }
3062
3241
  ];
3063
- fs6.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
3242
+ fs7.writeFileSync(examplePath, JSON.stringify(exampleRoutes, null, 2), "utf-8");
3064
3243
  }
3065
3244
  }
3066
3245
 
3067
3246
  // src/services/visual.ts
3068
- import fs7 from "fs";
3247
+ import fs8 from "fs";
3069
3248
  import path8 from "path";
3070
3249
  import pixelmatch from "pixelmatch";
3071
3250
  import { PNG } from "pngjs";
3072
3251
  function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
3073
- if (!fs7.existsSync(baselinePath)) {
3252
+ if (!fs8.existsSync(baselinePath)) {
3074
3253
  throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
3075
3254
  }
3076
- if (!fs7.existsSync(currentPath)) {
3255
+ if (!fs8.existsSync(currentPath)) {
3077
3256
  throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
3078
3257
  }
3079
- const baseline = PNG.sync.read(fs7.readFileSync(baselinePath));
3080
- const current = PNG.sync.read(fs7.readFileSync(currentPath));
3258
+ const baseline = PNG.sync.read(fs8.readFileSync(baselinePath));
3259
+ const current = PNG.sync.read(fs8.readFileSync(currentPath));
3081
3260
  if (baseline.width !== current.width || baseline.height !== current.height) {
3082
3261
  return {
3083
3262
  passed: false,
@@ -3106,10 +3285,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
3106
3285
  let diffPath;
3107
3286
  if (diffPixels > 0) {
3108
3287
  const diffDir = path8.dirname(diffOutputPath);
3109
- if (!fs7.existsSync(diffDir)) {
3110
- fs7.mkdirSync(diffDir, { recursive: true });
3288
+ if (!fs8.existsSync(diffDir)) {
3289
+ fs8.mkdirSync(diffDir, { recursive: true });
3111
3290
  }
3112
- fs7.writeFileSync(diffOutputPath, PNG.sync.write(diff));
3291
+ fs8.writeFileSync(diffOutputPath, PNG.sync.write(diff));
3113
3292
  diffPath = diffOutputPath;
3114
3293
  }
3115
3294
  return {
@@ -3123,14 +3302,14 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
3123
3302
  };
3124
3303
  }
3125
3304
  function createBaseline(currentPath, baselinePath) {
3126
- if (!fs7.existsSync(currentPath)) {
3305
+ if (!fs8.existsSync(currentPath)) {
3127
3306
  throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
3128
3307
  }
3129
3308
  const baselineDir = path8.dirname(baselinePath);
3130
- if (!fs7.existsSync(baselineDir)) {
3131
- fs7.mkdirSync(baselineDir, { recursive: true });
3309
+ if (!fs8.existsSync(baselineDir)) {
3310
+ fs8.mkdirSync(baselineDir, { recursive: true });
3132
3311
  }
3133
- fs7.copyFileSync(currentPath, baselinePath);
3312
+ fs8.copyFileSync(currentPath, baselinePath);
3134
3313
  return baselinePath;
3135
3314
  }
3136
3315
  function updateBaseline(currentPath, baselinePath) {
@@ -3138,69 +3317,69 @@ function updateBaseline(currentPath, baselinePath) {
3138
3317
  }
3139
3318
  function updateAllBaselines(currentDir, baselineDir) {
3140
3319
  const updated = [];
3141
- if (!fs7.existsSync(currentDir)) {
3320
+ if (!fs8.existsSync(currentDir)) {
3142
3321
  return updated;
3143
3322
  }
3144
- const files = fs7.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3323
+ const files = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3145
3324
  for (const file of files) {
3146
3325
  const currentPath = path8.join(currentDir, file);
3147
3326
  const baselinePath = path8.join(baselineDir, file);
3148
3327
  const baselineDirAbs = path8.dirname(baselinePath);
3149
- if (!fs7.existsSync(baselineDirAbs)) {
3150
- fs7.mkdirSync(baselineDirAbs, { recursive: true });
3328
+ if (!fs8.existsSync(baselineDirAbs)) {
3329
+ fs8.mkdirSync(baselineDirAbs, { recursive: true });
3151
3330
  }
3152
- fs7.copyFileSync(currentPath, baselinePath);
3331
+ fs8.copyFileSync(currentPath, baselinePath);
3153
3332
  updated.push(file);
3154
3333
  }
3155
3334
  return updated;
3156
3335
  }
3157
3336
  function cleanBaselines(baselineDir) {
3158
- if (!fs7.existsSync(baselineDir)) {
3337
+ if (!fs8.existsSync(baselineDir)) {
3159
3338
  return 0;
3160
3339
  }
3161
- const files = fs7.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
3340
+ const files = fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
3162
3341
  let count = 0;
3163
3342
  for (const file of files) {
3164
- fs7.unlinkSync(path8.join(baselineDir, file));
3343
+ fs8.unlinkSync(path8.join(baselineDir, file));
3165
3344
  count++;
3166
3345
  }
3167
3346
  return count;
3168
3347
  }
3169
3348
  function cleanDiffs(diffDir) {
3170
- if (!fs7.existsSync(diffDir)) {
3349
+ if (!fs8.existsSync(diffDir)) {
3171
3350
  return 0;
3172
3351
  }
3173
- const files = fs7.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
3352
+ const files = fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
3174
3353
  let count = 0;
3175
3354
  for (const file of files) {
3176
- fs7.unlinkSync(path8.join(diffDir, file));
3355
+ fs8.unlinkSync(path8.join(diffDir, file));
3177
3356
  count++;
3178
3357
  }
3179
3358
  return count;
3180
3359
  }
3181
3360
  function listBaselines(baselineDir) {
3182
- if (!fs7.existsSync(baselineDir)) {
3361
+ if (!fs8.existsSync(baselineDir)) {
3183
3362
  return [];
3184
3363
  }
3185
- return fs7.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
3364
+ return fs8.readdirSync(baselineDir).filter((f) => f.endsWith(".png")).sort();
3186
3365
  }
3187
3366
  function listDiffs(diffDir) {
3188
- if (!fs7.existsSync(diffDir)) {
3367
+ if (!fs8.existsSync(diffDir)) {
3189
3368
  return [];
3190
3369
  }
3191
- return fs7.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
3370
+ return fs8.readdirSync(diffDir).filter((f) => f.endsWith(".png")).sort();
3192
3371
  }
3193
3372
  function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
3194
3373
  const results = [];
3195
- if (!fs7.existsSync(currentDir)) {
3374
+ if (!fs8.existsSync(currentDir)) {
3196
3375
  return results;
3197
3376
  }
3198
- const currentFiles = fs7.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3377
+ const currentFiles = fs8.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
3199
3378
  for (const file of currentFiles) {
3200
3379
  const currentPath = path8.join(currentDir, file);
3201
3380
  const baselinePath = path8.join(baselineDir, file);
3202
3381
  const diffPath = path8.join(diffDir, file);
3203
- if (!fs7.existsSync(baselinePath)) {
3382
+ if (!fs8.existsSync(baselinePath)) {
3204
3383
  createBaseline(currentPath, baselinePath);
3205
3384
  results.push({
3206
3385
  passed: true,
@@ -3232,14 +3411,14 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
3232
3411
  }
3233
3412
 
3234
3413
  // src/services/source-analyzer.ts
3235
- import fs8 from "fs";
3414
+ import fs9 from "fs";
3236
3415
  import path9 from "path";
3237
3416
  function analyzeFile(filePath) {
3238
3417
  const absolutePath = path9.resolve(process.cwd(), filePath);
3239
- if (!fs8.existsSync(absolutePath)) {
3418
+ if (!fs9.existsSync(absolutePath)) {
3240
3419
  return { filePath, exports: [], apiCalls: [] };
3241
3420
  }
3242
- const content = fs8.readFileSync(absolutePath, "utf-8");
3421
+ const content = fs9.readFileSync(absolutePath, "utf-8");
3243
3422
  const ext = path9.extname(filePath);
3244
3423
  const result = {
3245
3424
  filePath,
@@ -3594,13 +3773,13 @@ function cleanTemplateUrl(url) {
3594
3773
  }
3595
3774
  function scanAPICalls(srcDir) {
3596
3775
  const absDir = path9.resolve(process.cwd(), srcDir);
3597
- if (!fs8.existsSync(absDir)) {
3776
+ if (!fs9.existsSync(absDir)) {
3598
3777
  return [];
3599
3778
  }
3600
3779
  const allCalls = [];
3601
3780
  const seen = /* @__PURE__ */ new Set();
3602
3781
  function walk(dir) {
3603
- const entries = fs8.readdirSync(dir, { withFileTypes: true });
3782
+ const entries = fs9.readdirSync(dir, { withFileTypes: true });
3604
3783
  for (const entry of entries) {
3605
3784
  if (entry.name.startsWith(".") || entry.name === "node_modules" || entry.name === "dist") continue;
3606
3785
  const fullPath = path9.join(dir, entry.name);
@@ -3608,7 +3787,7 @@ function scanAPICalls(srcDir) {
3608
3787
  walk(fullPath);
3609
3788
  } else if (entry.isFile() && /\.(ts|js|vue|mjs)$/.test(entry.name)) {
3610
3789
  try {
3611
- const content = fs8.readFileSync(fullPath, "utf-8");
3790
+ const content = fs9.readFileSync(fullPath, "utf-8");
3612
3791
  const calls = extractAPICalls(content, path9.relative(process.cwd(), fullPath));
3613
3792
  for (const call of calls) {
3614
3793
  const key = `${call.method}:${call.url}`;