qat-cli 0.3.4 → 0.3.6

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/cli.js CHANGED
@@ -524,71 +524,6 @@ function validateConfig(config) {
524
524
  }
525
525
  return merged;
526
526
  }
527
- function generateConfigFile(overrides = {}) {
528
- const config = validateConfig(overrides);
529
- return `// @ts-check
530
- /**
531
- * QAT \u914D\u7F6E\u6587\u4EF6 - Quick Auto Testing
532
- * \u4FEE\u6539\u540E\u65E0\u9700\u91CD\u542F\uFF0C\u4E0B\u6B21\u8FD0\u884C qat \u547D\u4EE4\u65F6\u81EA\u52A8\u751F\u6548
533
- *
534
- * AI \u6A21\u578B\u914D\u7F6E\u8BF7\u8FD0\u884C: qat change
535
- * AI \u72B6\u6001\u67E5\u770B\u8BF7\u8FD0\u884C: qat status
536
- */
537
- import { defineConfig } from 'qat-cli';
538
-
539
- export default defineConfig({
540
- project: {
541
- framework: '${config.project.framework}',
542
- vite: ${config.project.vite ?? true},
543
- srcDir: '${config.project.srcDir}',
544
- },
545
- vitest: {
546
- enabled: ${config.vitest.enabled},
547
- coverage: ${config.vitest.coverage},
548
- globals: ${config.vitest.globals},
549
- environment: '${config.vitest.environment}',
550
- },
551
- playwright: {
552
- enabled: ${config.playwright.enabled},
553
- browsers: [${config.playwright.browsers.map((b) => `'${b}'`).join(", ")}],
554
- baseURL: '${config.playwright.baseURL}',
555
- screenshot: '${config.playwright.screenshot}',
556
- },
557
- visual: {
558
- enabled: ${config.visual.enabled},
559
- threshold: ${config.visual.threshold},
560
- baselineDir: '${config.visual.baselineDir}',
561
- diffDir: '${config.visual.diffDir}',
562
- },
563
- lighthouse: {
564
- enabled: ${config.lighthouse.enabled},
565
- urls: [${config.lighthouse.urls.map((u) => `'${u}'`).join(", ")}],
566
- runs: ${config.lighthouse.runs},
567
- thresholds: {${Object.entries(config.lighthouse.thresholds).map(([k, v]) => `
568
- ${k}: ${v},`).join("")}
569
- },
570
- },
571
- mock: {
572
- enabled: ${config.mock.enabled},
573
- port: ${config.mock.port},
574
- routesDir: '${config.mock.routesDir}',
575
- },
576
- report: {
577
- outputDir: '${config.report.outputDir}',
578
- open: ${config.report.open},
579
- },
580
- });
581
- `;
582
- }
583
- async function writeConfigFile(cwd, overrides = {}, force = false) {
584
- const configPath = path2.join(cwd, "qat.config.js");
585
- if (fs2.existsSync(configPath) && !force) {
586
- throw new Error(`\u914D\u7F6E\u6587\u4EF6\u5DF2\u5B58\u5728: ${configPath}\uFF0C\u4F7F\u7528 --force \u8986\u76D6`);
587
- }
588
- const content = generateConfigFile(overrides);
589
- fs2.writeFileSync(configPath, content, "utf-8");
590
- return configPath;
591
- }
592
527
  function isFileNotFoundError(error) {
593
528
  if (error instanceof Error) {
594
529
  return error.message.includes("Cannot find") || error.message.includes("ENOENT") || error.message.includes("\u65E0\u6CD5\u52A0\u8F7D\u914D\u7F6E\u6587\u4EF6");
@@ -1088,9 +1023,15 @@ ${errorDetails}`;
1088
1023
  /**
1089
1024
  * 压缩源码:保留签名和关键逻辑,剔除注释、空行、样式块
1090
1025
  * 目标:在保留准确性的前提下减少 token 消耗
1026
+ * @param code 源码内容
1027
+ * @param maxLength 最大长度
1028
+ * @param importPathRewrites import 路径重写映射(原路径→正确路径)
1091
1029
  */
1092
- compressSourceCode(code, maxLength = 3e3) {
1030
+ compressSourceCode(code, maxLength = 3e3, importPathRewrites) {
1093
1031
  let compressed = code;
1032
+ if (importPathRewrites && importPathRewrites.size > 0) {
1033
+ compressed = this.rewriteImportPaths(compressed, importPathRewrites);
1034
+ }
1094
1035
  compressed = compressed.replace(/<style[^>]*>[\s\S]*?<\/style>/gi, "");
1095
1036
  compressed = compressed.replace(/<template[^>]*>([\s\S]*?)<\/template>/gi, (_match, content) => {
1096
1037
  return `<template>${content.replace(/\s*(?:class|style)\s*=\s*["'][^"']*["']/gi, "")}</template>`;
@@ -1128,7 +1069,8 @@ ${errorDetails}`;
1128
1069
  }
1129
1070
  buildGenerateTestUserPrompt(req) {
1130
1071
  const importPath = this.computeTestImportPath(req.type, req.target);
1131
- let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002import\u8DEF\u5F84:${importPath}
1072
+ let prompt = `\u4E3A${req.target}\u751F\u6210${req.type}\u6D4B\u8BD5\u3002
1073
+ \u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
1132
1074
  `;
1133
1075
  if (req.analysis) {
1134
1076
  const parts = [];
@@ -1150,13 +1092,19 @@ ${errorDetails}`;
1150
1092
  if (req.analysis.computed?.length) {
1151
1093
  parts.push(`Computed:${req.analysis.computed.join(",")}`);
1152
1094
  }
1095
+ if (req.analysis.importSignatures?.length) {
1096
+ parts.push(`\u4F9D\u8D56\u7B7E\u540D:${req.analysis.importSignatures.map(
1097
+ (imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
1098
+ ).join(";")}`);
1099
+ }
1153
1100
  prompt += parts.join("\n") + "\n";
1154
1101
  }
1155
1102
  if (req.context) {
1103
+ const importPathRewrites = this.buildImportPathRewrites(req.type, req.target);
1156
1104
  prompt += `
1157
1105
  \u6E90\u7801:
1158
1106
  \`\`\`
1159
- ${this.compressSourceCode(req.context)}
1107
+ ${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
1160
1108
  \`\`\`
1161
1109
  `;
1162
1110
  }
@@ -1191,6 +1139,58 @@ ${this.compressSourceCode(req.context)}
1191
1139
  }
1192
1140
  return `${prefix}${cleanPath}`;
1193
1141
  }
1142
+ /**
1143
+ * 构建 import 路径重写映射
1144
+ * 将源码中可能引用自身的各种写法,映射到测试文件中正确的导入路径
1145
+ */
1146
+ buildImportPathRewrites(testType, targetPath) {
1147
+ const rewrites = /* @__PURE__ */ new Map();
1148
+ if (!targetPath) return rewrites;
1149
+ const correctImportPath = this.computeTestImportPath(testType, targetPath);
1150
+ const variations = /* @__PURE__ */ new Set();
1151
+ variations.add(targetPath);
1152
+ variations.add(targetPath.replace(/^\.\//, ""));
1153
+ const withoutExt = targetPath.replace(/\.(ts|js|tsx|jsx)$/, "");
1154
+ variations.add(withoutExt);
1155
+ const srcDirMatch = targetPath.match(/^(?:\.\/)?(src\/.+)$/);
1156
+ if (srcDirMatch) {
1157
+ variations.add(`@/${srcDirMatch[1]}`);
1158
+ variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1159
+ variations.add(`#/${srcDirMatch[1]}`);
1160
+ variations.add(`#/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
1161
+ }
1162
+ const pathParts = targetPath.replace(/^\.\//, "").split("/");
1163
+ const fileName = pathParts[pathParts.length - 1];
1164
+ const fileNameNoExt = fileName.replace(/\.(ts|js|tsx|jsx|vue)$/, "");
1165
+ variations.add(`./${fileName}`);
1166
+ variations.add(`./${fileNameNoExt}`);
1167
+ for (let i = 1; i < pathParts.length; i++) {
1168
+ const relPath = "../".repeat(i) + pathParts.slice(i).join("/");
1169
+ variations.add(relPath);
1170
+ variations.add(relPath.replace(/\.(ts|js|tsx|jsx)$/, ""));
1171
+ }
1172
+ for (const variant of variations) {
1173
+ if (variant && variant !== correctImportPath) {
1174
+ rewrites.set(variant, correctImportPath);
1175
+ }
1176
+ }
1177
+ return rewrites;
1178
+ }
1179
+ /**
1180
+ * 重写源码中的 import 路径
1181
+ * 将 from 'oldPath' / from "oldPath" 替换为正确的路径
1182
+ */
1183
+ rewriteImportPaths(code, rewrites) {
1184
+ let result = code;
1185
+ for (const [oldPath, newPath] of rewrites) {
1186
+ const escaped = oldPath.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
1187
+ const singleQuoteRegex = new RegExp(`(from\\s+')${escaped}(')`, "g");
1188
+ const doubleQuoteRegex = new RegExp(`(from\\s+")${escaped}(")`, "g");
1189
+ result = result.replace(singleQuoteRegex, `$1${newPath}$2`);
1190
+ result = result.replace(doubleQuoteRegex, `$1${newPath}$2`);
1191
+ }
1192
+ return result;
1193
+ }
1194
1194
  parseGenerateTestResponse(content) {
1195
1195
  const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
1196
1196
  const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
@@ -1213,12 +1213,14 @@ SUGGESTIONS:- \u5EFA\u8BAE(\u6BCF\u884C\u4E00\u4E2A)`;
1213
1213
  }
1214
1214
  buildReviewTestUserPrompt(req) {
1215
1215
  const importPath = this.computeTestImportPath(req.testType, req.target);
1216
- let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType} import\u8DEF\u5F84:${importPath}
1216
+ let prompt = `\u5BA1\u67E5\u6D4B\u8BD5\u4EE3\u7801\u3002\u88AB\u6D4B:${req.target} \u7C7B\u578B:${req.testType}
1217
+ \u3010\u5F3A\u5236\u3011import\u88AB\u6D4B\u6A21\u5757\u5FC5\u987B\u7528: ${importPath}
1217
1218
  `;
1219
+ const importPathRewrites = this.buildImportPathRewrites(req.testType, req.target);
1218
1220
  prompt += `
1219
1221
  \u6E90\u7801:
1220
1222
  \`\`\`
1221
- ${this.compressSourceCode(req.sourceCode, 2e3)}
1223
+ ${this.compressSourceCode(req.sourceCode, 2e3, importPathRewrites)}
1222
1224
  \`\`\`
1223
1225
  `;
1224
1226
  prompt += `
@@ -1237,6 +1239,11 @@ ${req.testCode}
1237
1239
  if (req.analysis.emits?.length) {
1238
1240
  parts.push(`Emits:${req.analysis.emits.map((e) => `${e.name}(${e.params?.join(",") || ""})`).join(",")}`);
1239
1241
  }
1242
+ if (req.analysis.importSignatures?.length) {
1243
+ parts.push(`\u4F9D\u8D56:${req.analysis.importSignatures.map(
1244
+ (imp) => `${imp.source}{${Object.entries(imp.signatures).map(([k, v]) => `${k}:${v}`).join(",")}}`
1245
+ ).join(";")}`);
1246
+ }
1240
1247
  if (parts.length > 0) {
1241
1248
  prompt += `
1242
1249
  \u5206\u6790:${parts.join("|")}`;
@@ -1428,15 +1435,18 @@ import path4 from "path";
1428
1435
  function analyzeFile(filePath) {
1429
1436
  const absolutePath = path4.resolve(process.cwd(), filePath);
1430
1437
  if (!fs4.existsSync(absolutePath)) {
1431
- return { filePath, exports: [], apiCalls: [] };
1438
+ return { filePath, exports: [], imports: [], importSignatures: [], apiCalls: [] };
1432
1439
  }
1433
1440
  const content = fs4.readFileSync(absolutePath, "utf-8");
1434
1441
  const ext = path4.extname(filePath);
1435
1442
  const result = {
1436
1443
  filePath,
1437
1444
  exports: extractExports(content, ext),
1445
+ imports: extractImports(content),
1446
+ importSignatures: [],
1438
1447
  apiCalls: extractAPICalls(content, filePath)
1439
1448
  };
1449
+ result.importSignatures = analyzeImportSignatures(result.imports, path4.dirname(absolutePath));
1440
1450
  if (ext === ".vue") {
1441
1451
  result.vueAnalysis = analyzeVueComponent(content, path4.basename(filePath, ".vue"));
1442
1452
  const scriptMatch = content.match(/<script[^>]*>([\s\S]*?)<\/script>/);
@@ -1856,6 +1866,90 @@ function generateMockRoutesFromAPICalls(apiCalls) {
1856
1866
  }
1857
1867
  return routes;
1858
1868
  }
1869
+ function extractImports(content) {
1870
+ const imports = [];
1871
+ const namedImportRegex = /import\s*\{([^}]+)\}\s*from\s*['"]([^'"]+)['"]/g;
1872
+ let match;
1873
+ while ((match = namedImportRegex.exec(content)) !== null) {
1874
+ const names = match[1].split(",").map((n) => {
1875
+ const parts = n.trim().split(/\s+as\s+/);
1876
+ return parts[0].trim();
1877
+ }).filter(Boolean);
1878
+ imports.push({ names, source: match[2], isDefault: false, isNamespace: false });
1879
+ }
1880
+ const defaultImportRegex = /import\s+(\w+)\s+from\s*['"]([^'"]+)['"]/g;
1881
+ while ((match = defaultImportRegex.exec(content)) !== null) {
1882
+ const fullLine = content.slice(Math.max(0, match.index - 5), match.index + match[0].length);
1883
+ if (fullLine.includes("{")) continue;
1884
+ imports.push({ names: [match[1]], source: match[2], isDefault: true, isNamespace: false });
1885
+ }
1886
+ const namespaceImportRegex = /import\s*\*\s*as\s+(\w+)\s*from\s*['"]([^'"]+)['"]/g;
1887
+ while ((match = namespaceImportRegex.exec(content)) !== null) {
1888
+ imports.push({ names: [match[1]], source: match[2], isDefault: false, isNamespace: true });
1889
+ }
1890
+ return imports;
1891
+ }
1892
+ function analyzeImportSignatures(imports, baseDir) {
1893
+ const signatures = [];
1894
+ for (const imp of imports) {
1895
+ if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("#/") && !imp.source.startsWith("~")) {
1896
+ continue;
1897
+ }
1898
+ const resolvedPath = resolveImportPath(imp.source, baseDir);
1899
+ if (!resolvedPath || !fs4.existsSync(resolvedPath)) continue;
1900
+ try {
1901
+ const content = fs4.readFileSync(resolvedPath, "utf-8");
1902
+ const ext = path4.extname(resolvedPath);
1903
+ const exports = extractExports(content, ext);
1904
+ const sigMap = {};
1905
+ for (const name of imp.names) {
1906
+ const exp = exports.find((e) => e.name === name);
1907
+ if (exp) {
1908
+ const params = exp.params.length > 0 ? `(${exp.params.join(", ")})` : "";
1909
+ const ret = exp.returnType ? `: ${exp.returnType}` : "";
1910
+ const async = exp.isAsync ? "async " : "";
1911
+ sigMap[name] = `${async}${exp.kind}${params}${ret}`;
1912
+ } else if (imp.isDefault) {
1913
+ const defaultExp = exports.find((e) => e.kind === "default");
1914
+ if (defaultExp) {
1915
+ sigMap[name] = `default[${defaultExp.name || "anonymous"}]`;
1916
+ }
1917
+ } else if (imp.isNamespace) {
1918
+ sigMap[name] = `{${exports.map((e) => e.name).join(", ")}}`;
1919
+ }
1920
+ }
1921
+ if (ext === ".vue") {
1922
+ const vueAnalysis = analyzeVueComponent(content, path4.basename(resolvedPath, ".vue"));
1923
+ if (vueAnalysis.props.length > 0) {
1924
+ sigMap["__props__"] = vueAnalysis.props.map((p) => `${p.name}:${p.type}${p.required ? "!" : "?"}`).join(",");
1925
+ }
1926
+ }
1927
+ if (Object.keys(sigMap).length > 0) {
1928
+ signatures.push({ source: imp.source, signatures: sigMap });
1929
+ }
1930
+ } catch {
1931
+ }
1932
+ }
1933
+ return signatures;
1934
+ }
1935
+ function resolveImportPath(importSource, baseDir) {
1936
+ let resolved;
1937
+ if (importSource.startsWith("@/") || importSource.startsWith("#/") || importSource.startsWith("~")) {
1938
+ const relativePath = importSource.replace(/^[@#~]\//, "src/");
1939
+ resolved = path4.resolve(process.cwd(), relativePath);
1940
+ } else if (importSource.startsWith(".")) {
1941
+ resolved = path4.resolve(baseDir, importSource);
1942
+ } else {
1943
+ return null;
1944
+ }
1945
+ const extensions = [".ts", ".tsx", ".js", ".jsx", ".vue", "/index.ts", "/index.js"];
1946
+ if (fs4.existsSync(resolved)) return resolved;
1947
+ for (const ext of extensions) {
1948
+ const withExt = resolved + ext;
1949
+ if (fs4.existsSync(withExt)) return withExt;
1950
+ }
1951
+ return null;
1952
+ }
1859
1953
 
1860
1954
  // src/services/global-config.ts
1861
1955
  import fs5 from "fs";
@@ -1960,41 +2054,13 @@ async function executeInit(options) {
1960
2054
  }
1961
2055
  }
1962
2056
  const config = buildProjectConfig(projectInfo);
1963
- let configPath;
1964
2057
  const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
1965
2058
  const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
1966
2059
  const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
1967
- if (configExists && !options.force) {
1968
- const { overwrite } = await inquirer.prompt([
1969
- {
1970
- type: "confirm",
1971
- name: "overwrite",
1972
- message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
1973
- default: true
1974
- }
1975
- ]);
1976
- if (!overwrite) {
1977
- console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
1978
- configPath = existingConfigPath;
1979
- } else {
1980
- const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
1981
- try {
1982
- configPath = await writeConfigFile(process.cwd(), config, true);
1983
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
1984
- } catch (error) {
1985
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
1986
- throw error;
1987
- }
1988
- }
2060
+ if (!configExists) {
2061
+ console.log(chalk3.gray(" \u672A\u627E\u5230\u914D\u7F6E\u6587\u4EF6\uFF0C\u4F7F\u7528\u9ED8\u8BA4\u914D\u7F6E\uFF08\u9700\u8981\u81EA\u5B9A\u4E49\u65F6\u53EF\u521B\u5EFA qat.config.js\uFF09"));
1989
2062
  } else {
1990
- const fileSpinner = ora("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
1991
- try {
1992
- configPath = await writeConfigFile(process.cwd(), config, options.force);
1993
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
1994
- } catch (error) {
1995
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
1996
- throw error;
1997
- }
2063
+ console.log(chalk3.green(` \u2713 \u5DF2\u6709\u914D\u7F6E\u6587\u4EF6: ${path6.basename(fs6.existsSync(existingConfigPath) ? existingConfigPath : existingTsPath)}`));
1998
2064
  }
1999
2065
  const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
2000
2066
  const createdDirs = createTestDirectories(config);
@@ -2023,7 +2089,7 @@ async function executeInit(options) {
2023
2089
  console.log(chalk3.cyan(`
2024
2090
  \u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
2025
2091
  }
2026
- displayResult(configPath, createdDirs, totalFiles, projectInfo);
2092
+ displayResult(createdDirs, totalFiles, projectInfo);
2027
2093
  }
2028
2094
  async function promptAIConfig() {
2029
2095
  const answers = await inquirer.prompt([
@@ -2160,12 +2226,8 @@ function createTestDirectories(config) {
2160
2226
  }
2161
2227
  return dirs;
2162
2228
  }
2163
- function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo) {
2164
- const relativeConfig = path6.relative(process.cwd(), configPath);
2229
+ function displayResult(createdDirs, totalTestableFiles, projectInfo) {
2165
2230
  console.log(chalk3.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
2166
- console.log(chalk3.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
2167
- console.log(chalk3.gray(` ${relativeConfig}`));
2168
- console.log();
2169
2231
  if (createdDirs.length > 0) {
2170
2232
  console.log(chalk3.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
2171
2233
  for (const dir of createdDirs) {
@@ -2173,6 +2235,9 @@ function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo)
2173
2235
  }
2174
2236
  console.log();
2175
2237
  }
2238
+ console.log(chalk3.white(" \u914D\u7F6E:"));
2239
+ console.log(chalk3.gray(" \u9ED8\u8BA4\u914D\u7F6E\u5DF2\u5185\u7F6E\uFF0C\u9700\u8981\u81EA\u5B9A\u4E49\u65F6\u521B\u5EFA qat.config.js"));
2240
+ console.log();
2176
2241
  console.log(chalk3.cyan(" \u4E0B\u4E00\u6B65:"));
2177
2242
  if (totalTestableFiles > 0) {
2178
2243
  console.log(chalk3.gray(` 1. qat create \u9009\u62E9\u6587\u4EF6\u5E76\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (\u53D1\u73B0 ${totalTestableFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6)`));
@@ -2193,7 +2258,7 @@ import chalk5 from "chalk";
2193
2258
  import inquirer2 from "inquirer";
2194
2259
  import ora3 from "ora";
2195
2260
  import fs8 from "fs";
2196
- import path8 from "path";
2261
+ import path9 from "path";
2197
2262
 
2198
2263
  // src/services/template.ts
2199
2264
  import fs7 from "fs";
@@ -2242,7 +2307,7 @@ Handlebars.registerHelper("propTestValue", (prop) => {
2242
2307
  };
2243
2308
  return map[prop.type] || "'test-value'";
2244
2309
  });
2245
- function resolveImportPath(testType, targetPath) {
2310
+ function resolveImportPath2(testType, targetPath) {
2246
2311
  if (!targetPath) return targetPath;
2247
2312
  const testDirMap = {
2248
2313
  unit: "tests/unit",
@@ -2268,7 +2333,7 @@ function resolveImportPath(testType, targetPath) {
2268
2333
  function renderTemplate(type, context) {
2269
2334
  const templateContent = loadTemplate(type);
2270
2335
  const template = Handlebars.compile(templateContent);
2271
- const resolvedTarget = resolveImportPath(type, context.target);
2336
+ const resolvedTarget = resolveImportPath2(type, context.target);
2272
2337
  const fullContext = {
2273
2338
  vueVersion: 3,
2274
2339
  typescript: true,
@@ -2652,10 +2717,11 @@ function toPascalCase(str) {
2652
2717
  // src/services/test-reviewer.ts
2653
2718
  import chalk4 from "chalk";
2654
2719
  import ora2 from "ora";
2720
+ import path8 from "path";
2655
2721
  var MAX_RETRIES = 3;
2656
2722
  var REVIEW_THRESHOLD = 0.6;
2657
2723
  async function generateWithReview(params) {
2658
- const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
2724
+ const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt, onProgress } = params;
2659
2725
  const generatorProvider = createAIProvider(aiConfig);
2660
2726
  const reviewerProvider = createAIProvider(aiConfig);
2661
2727
  if (!generatorProvider.capabilities.generateTest) {
@@ -2667,9 +2733,12 @@ async function generateWithReview(params) {
2667
2733
  let lastReview = null;
2668
2734
  let approved = false;
2669
2735
  let attempts = 0;
2736
+ const targetName = path8.basename(targetPath);
2670
2737
  for (let i = 0; i < MAX_RETRIES; i++) {
2671
2738
  attempts = i + 1;
2672
2739
  onAttempt?.(attempts, MAX_RETRIES);
2740
+ const genLabel = attempts > 1 ? `\u27F3 \u91CD\u65B0\u751F\u6210 (${attempts}/${MAX_RETRIES})` : "\u751F\u6210\u6D4B\u8BD5\u4EE3\u7801";
2741
+ onProgress?.(`${genLabel} \u2190 ${targetName}`);
2673
2742
  if (process.env.QAT_DEBUG === "true") {
2674
2743
  console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u751F\u6210\u8005 AI \u7B2C${attempts}\u6B21\u5C1D\u8BD5 \u2500\u2500\u2500`));
2675
2744
  }
@@ -2693,6 +2762,7 @@ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
2693
2762
  currentCode = generateResponse.code;
2694
2763
  currentDescription = generateResponse.description;
2695
2764
  currentConfidence = generateResponse.confidence;
2765
+ onProgress?.(`\u5BA1\u8BA1\u5BA1\u67E5 \u2190 ${targetName}`);
2696
2766
  if (process.env.QAT_DEBUG === "true") {
2697
2767
  console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u5BA1\u8BA1\u5458 AI \u5BA1\u67E5 \u2500\u2500\u2500`));
2698
2768
  }
@@ -2846,59 +2916,103 @@ async function executeCreate(options) {
2846
2916
  ]);
2847
2917
  useAI = ai;
2848
2918
  }
2919
+ const tasks = [];
2920
+ for (const testType of types) {
2921
+ for (const testTarget of targets) {
2922
+ tasks.push({ testType, target: testTarget });
2923
+ }
2924
+ }
2849
2925
  const createdFiles = [];
2850
2926
  let skippedCount = 0;
2851
2927
  const reviewReportEntries = [];
2852
- for (const testType of types) {
2853
- for (const testTarget of targets) {
2854
- const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[testType]} - ${path8.basename(testTarget)}...`).start();
2855
- try {
2856
- let content;
2857
- if (useAI && testTarget) {
2858
- const aiResult = await generateWithAI(testType, name, testTarget, config);
2859
- content = aiResult.code;
2860
- if (aiResult.reviewEntry) {
2861
- reviewReportEntries.push(aiResult.reviewEntry);
2862
- }
2928
+ if (useAI && tasks.length > 0) {
2929
+ const spinner = ora3(`\u5E76\u884C\u751F\u6210 ${tasks.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B...`).start();
2930
+ let completed = 0;
2931
+ let approvedCount = 0;
2932
+ let failedCount = 0;
2933
+ const results = await Promise.allSettled(
2934
+ tasks.map(async (task) => {
2935
+ const result = await generateWithAI(task.testType, name, task.target, config, (text) => {
2936
+ completed++;
2937
+ spinner.text = `\u5E76\u884C\u751F\u6210\u4E2D (${completed}/${tasks.length}) ${text}`;
2938
+ });
2939
+ return { ...task, ...result };
2940
+ })
2941
+ );
2942
+ spinner.stop();
2943
+ for (let i = 0; i < results.length; i++) {
2944
+ const r = results[i];
2945
+ const task = tasks[i];
2946
+ if (r.status === "rejected") {
2947
+ console.log(chalk5.red(` \u2717 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`));
2948
+ failedCount++;
2949
+ continue;
2950
+ }
2951
+ const { code, reviewEntry } = r.value;
2952
+ const outputDir = getTestOutputDir(task.testType);
2953
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
2954
+ if (fs8.existsSync(filePath)) {
2955
+ console.log(chalk5.yellow(` \u26A0 \u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`));
2956
+ skippedCount++;
2957
+ continue;
2958
+ }
2959
+ if (!fs8.existsSync(outputDir)) {
2960
+ fs8.mkdirSync(outputDir, { recursive: true });
2961
+ }
2962
+ fs8.writeFileSync(filePath, code, "utf-8");
2963
+ createdFiles.push({ type: task.testType, filePath });
2964
+ if (reviewEntry) {
2965
+ reviewReportEntries.push(reviewEntry);
2966
+ if (reviewEntry.approved) {
2967
+ approvedCount++;
2863
2968
  } else {
2864
- const analysis = testTarget ? analyzeFile(testTarget) : void 0;
2865
- content = renderTemplate(testType, {
2866
- name,
2867
- target: testTarget || `./${config.project.srcDir}`,
2868
- framework: projectInfo.framework,
2869
- vueVersion: projectInfo.vueVersion,
2870
- typescript: projectInfo.typescript,
2871
- uiLibrary: projectInfo.uiLibrary,
2872
- extraImports: projectInfo.componentTestSetup?.extraImports,
2873
- globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2874
- globalStubs: projectInfo.componentTestSetup?.globalStubs,
2875
- mountOptions: projectInfo.componentTestSetup?.mountOptions,
2876
- // 注入源码分析结果
2877
- exports: analysis?.exports.map((e) => ({
2878
- name: e.name,
2879
- kind: e.kind,
2880
- params: e.params,
2881
- isAsync: e.isAsync,
2882
- returnType: e.returnType
2883
- })),
2884
- props: analysis?.vueAnalysis?.props.map((p) => ({
2885
- name: p.name,
2886
- type: p.type,
2887
- required: p.required,
2888
- defaultValue: p.defaultValue
2889
- })),
2890
- emits: analysis?.vueAnalysis?.emits.map((e) => ({
2891
- name: e.name,
2892
- params: e.params
2893
- })),
2894
- methods: analysis?.vueAnalysis?.methods,
2895
- computed: analysis?.vueAnalysis?.computed
2896
- });
2969
+ failedCount++;
2897
2970
  }
2898
- const outputDir = getTestOutputDir(testType);
2899
- const filePath = path8.join(outputDir, `${name}.${testType === "e2e" ? "spec" : "test"}.ts`);
2971
+ }
2972
+ }
2973
+ console.log(chalk5.cyan(`
2974
+ \u5E76\u884C\u751F\u6210\u5B8C\u6210: ${createdFiles.length} \u6210\u529F, ${skippedCount} \u8DF3\u8FC7, ${failedCount} \u5BA1\u8BA1\u672A\u901A\u8FC7`));
2975
+ } else {
2976
+ for (const task of tasks) {
2977
+ const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}...`).start();
2978
+ try {
2979
+ const analysis = task.target ? analyzeFile(task.target) : void 0;
2980
+ const content = renderTemplate(task.testType, {
2981
+ name,
2982
+ target: task.target || `./${config.project.srcDir}`,
2983
+ framework: projectInfo.framework,
2984
+ vueVersion: projectInfo.vueVersion,
2985
+ typescript: projectInfo.typescript,
2986
+ uiLibrary: projectInfo.uiLibrary,
2987
+ extraImports: projectInfo.componentTestSetup?.extraImports,
2988
+ globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2989
+ globalStubs: projectInfo.componentTestSetup?.globalStubs,
2990
+ mountOptions: projectInfo.componentTestSetup?.mountOptions,
2991
+ // 注入源码分析结果
2992
+ exports: analysis?.exports.map((e) => ({
2993
+ name: e.name,
2994
+ kind: e.kind,
2995
+ params: e.params,
2996
+ isAsync: e.isAsync,
2997
+ returnType: e.returnType
2998
+ })),
2999
+ props: analysis?.vueAnalysis?.props.map((p) => ({
3000
+ name: p.name,
3001
+ type: p.type,
3002
+ required: p.required,
3003
+ defaultValue: p.defaultValue
3004
+ })),
3005
+ emits: analysis?.vueAnalysis?.emits.map((e) => ({
3006
+ name: e.name,
3007
+ params: e.params
3008
+ })),
3009
+ methods: analysis?.vueAnalysis?.methods,
3010
+ computed: analysis?.vueAnalysis?.computed
3011
+ });
3012
+ const outputDir = getTestOutputDir(task.testType);
3013
+ const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
2900
3014
  if (fs8.existsSync(filePath)) {
2901
- spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path8.relative(process.cwd(), filePath)}`);
3015
+ spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`);
2902
3016
  skippedCount++;
2903
3017
  continue;
2904
3018
  }
@@ -2906,10 +3020,10 @@ async function executeCreate(options) {
2906
3020
  fs8.mkdirSync(outputDir, { recursive: true });
2907
3021
  }
2908
3022
  fs8.writeFileSync(filePath, content, "utf-8");
2909
- spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${path8.basename(testTarget)}`);
2910
- createdFiles.push({ type: testType, filePath });
3023
+ spinner.succeed(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}`);
3024
+ createdFiles.push({ type: task.testType, filePath });
2911
3025
  } catch (error) {
2912
- spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${path8.basename(testTarget)} \u521B\u5EFA\u5931\u8D25`);
3026
+ spinner.fail(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)} \u521B\u5EFA\u5931\u8D25`);
2913
3027
  console.log(chalk5.gray(` ${error instanceof Error ? error.message : String(error)}`));
2914
3028
  }
2915
3029
  }
@@ -2918,6 +3032,34 @@ async function executeCreate(options) {
2918
3032
  if (reviewReportEntries.length > 0) {
2919
3033
  printReviewReport(reviewReportEntries);
2920
3034
  }
3035
+ const failedEntries = reviewReportEntries.filter((e) => !e.approved);
3036
+ if (failedEntries.length > 0 && useAI) {
3037
+ const { retry } = await inquirer2.prompt([
3038
+ {
3039
+ type: "confirm",
3040
+ name: "retry",
3041
+ message: `${failedEntries.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B\u5BA1\u8BA1\u672A\u901A\u8FC7\uFF0C\u662F\u5426\u91CD\u65B0\u751F\u6210\uFF1F`,
3042
+ default: false
3043
+ }
3044
+ ]);
3045
+ if (retry) {
3046
+ console.log(chalk5.cyan("\n \u91CD\u65B0\u751F\u6210\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\u7528\u4F8B...\n"));
3047
+ for (const entry of failedEntries) {
3048
+ const spinner = ora3(`\u91CD\u65B0\u751F\u6210 ${path9.basename(entry.target)}...`).start();
3049
+ try {
3050
+ const aiResult = await generateWithAI(entry.testType, name, entry.target, config, (text) => {
3051
+ spinner.text = text;
3052
+ });
3053
+ const outputDir = getTestOutputDir(entry.testType);
3054
+ const filePath = path9.join(outputDir, `${name}.${entry.testType === "e2e" ? "spec" : "test"}.ts`);
3055
+ fs8.writeFileSync(filePath, aiResult.code, "utf-8");
3056
+ spinner.succeed(`${path9.basename(entry.target)} \u5DF2\u91CD\u65B0\u751F\u6210${aiResult.reviewEntry?.approved ? chalk5.green(" (\u5BA1\u8BA1\u901A\u8FC7)") : chalk5.yellow(" (\u4ECD\u9700\u5BA1\u67E5)")}`);
3057
+ } catch (error) {
3058
+ spinner.fail(`${path9.basename(entry.target)} \u91CD\u65B0\u751F\u6210\u5931\u8D25`);
3059
+ }
3060
+ }
3061
+ }
3062
+ }
2921
3063
  }
2922
3064
  async function selectTargets(types, projectInfo, srcDir) {
2923
3065
  const allTargets = [];
@@ -2976,22 +3118,22 @@ async function selectTargets(types, projectInfo, srcDir) {
2976
3118
  }
2977
3119
  function generateDefaultName(type, target) {
2978
3120
  if (!target) return `${type}-test`;
2979
- const basename = path8.basename(target, path8.extname(target));
3121
+ const basename = path9.basename(target, path9.extname(target));
2980
3122
  const cleaned = basename.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2981
3123
  return cleaned || `${type}-test`;
2982
3124
  }
2983
- async function generateWithAI(type, name, target, config) {
3125
+ async function generateWithAI(type, name, target, config, onProgress) {
2984
3126
  const provider = getAIProvider(config.ai);
2985
3127
  if (!provider.capabilities.generateTest) {
2986
3128
  throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
2987
3129
  }
2988
3130
  let sourceCode = "";
2989
- const fullPath = path8.resolve(process.cwd(), target);
3131
+ const fullPath = path9.resolve(process.cwd(), target);
2990
3132
  if (fs8.existsSync(fullPath)) {
2991
3133
  sourceCode = fs8.readFileSync(fullPath, "utf-8");
2992
3134
  }
2993
3135
  const analysis = analyzeFile(target);
2994
- const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis ? {
3136
+ const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis || analysis.importSignatures.length > 0 ? {
2995
3137
  exports: analysis.exports.map((e) => ({
2996
3138
  name: e.name,
2997
3139
  kind: e.kind,
@@ -3008,7 +3150,8 @@ async function generateWithAI(type, name, target, config) {
3008
3150
  params: e.params
3009
3151
  })),
3010
3152
  methods: analysis.vueAnalysis?.methods,
3011
- computed: analysis.vueAnalysis?.computed
3153
+ computed: analysis.vueAnalysis?.computed,
3154
+ importSignatures: analysis.importSignatures.length > 0 ? analysis.importSignatures : void 0
3012
3155
  } : void 0;
3013
3156
  const reviewResult = await generateWithReview({
3014
3157
  testType: type,
@@ -3016,7 +3159,8 @@ async function generateWithAI(type, name, target, config) {
3016
3159
  sourceCode,
3017
3160
  analysis: analysisSummary,
3018
3161
  aiConfig: config.ai,
3019
- framework: "vue"
3162
+ framework: "vue",
3163
+ onProgress
3020
3164
  });
3021
3165
  const headerComment = [
3022
3166
  `// AI Generated Test - ${name}`,
@@ -3060,7 +3204,7 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
3060
3204
  }
3061
3205
  console.log();
3062
3206
  for (const { type, filePath } of createdFiles) {
3063
- const relativePath = path8.relative(process.cwd(), filePath);
3207
+ const relativePath = path9.relative(process.cwd(), filePath);
3064
3208
  console.log(chalk5.white(` ${chalk5.cyan(TEST_TYPE_LABELS[type])} ${chalk5.gray(relativePath)}`));
3065
3209
  }
3066
3210
  if (usedAI) {
@@ -3085,11 +3229,11 @@ import chalk6 from "chalk";
3085
3229
  import inquirer3 from "inquirer";
3086
3230
  import ora4 from "ora";
3087
3231
  import fs11 from "fs";
3088
- import path12 from "path";
3232
+ import path13 from "path";
3089
3233
 
3090
3234
  // src/runners/vitest-runner.ts
3091
3235
  import { execFile } from "child_process";
3092
- import path9 from "path";
3236
+ import path10 from "path";
3093
3237
  import fs9 from "fs";
3094
3238
  import os2 from "os";
3095
3239
  var isVerbose = () => process.env.QAT_VERBOSE === "true";
@@ -3171,7 +3315,7 @@ function buildVitestArgs(options) {
3171
3315
  return args;
3172
3316
  }
3173
3317
  async function execVitest(args) {
3174
- const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
3318
+ const tmpFile = path10.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
3175
3319
  const argsWithOutput = [...args, "--outputFile", tmpFile];
3176
3320
  return new Promise((resolve, reject) => {
3177
3321
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
@@ -3250,7 +3394,7 @@ function parseVitestJSON(jsonStr) {
3250
3394
  for (const fileResult of data.testResults) {
3251
3395
  const suiteTests = parseTestResults(fileResult);
3252
3396
  suites.push({
3253
- name: path9.basename(fileResult.name || "unknown"),
3397
+ name: path10.basename(fileResult.name || "unknown"),
3254
3398
  file: fileResult.name || "unknown",
3255
3399
  type: "unit",
3256
3400
  status: mapVitestStatus(fileResult.status),
@@ -3361,7 +3505,7 @@ function parseFromStdout(output) {
3361
3505
  if (data.testResults && Array.isArray(data.testResults)) {
3362
3506
  for (const fileResult of data.testResults) {
3363
3507
  suites.push({
3364
- name: path9.basename(fileResult.name || "unknown"),
3508
+ name: path10.basename(fileResult.name || "unknown"),
3365
3509
  file: fileResult.name || "unknown",
3366
3510
  type: "unit",
3367
3511
  status: mapVitestStatus(fileResult.status),
@@ -3397,7 +3541,7 @@ function parseVitestTextOutput(output, hasError) {
3397
3541
  if (isPassed) totalPassed += testCount;
3398
3542
  else totalFailed += testCount;
3399
3543
  suites.push({
3400
- name: path9.basename(file),
3544
+ name: path10.basename(file),
3401
3545
  file,
3402
3546
  type: "unit",
3403
3547
  status: isPassed ? "passed" : "failed",
@@ -3498,7 +3642,7 @@ function extractCoverage(coverageMap) {
3498
3642
 
3499
3643
  // src/runners/playwright-runner.ts
3500
3644
  import { execFile as execFile2 } from "child_process";
3501
- import path10 from "path";
3645
+ import path11 from "path";
3502
3646
  async function runPlaywright(options) {
3503
3647
  const startTime = Date.now();
3504
3648
  const args = buildPlaywrightArgs(options);
@@ -3698,7 +3842,7 @@ function parsePlaywrightTextOutput(output, hasError) {
3698
3842
  let existingSuite = suites.find((s) => s.file === file);
3699
3843
  if (!existingSuite) {
3700
3844
  existingSuite = {
3701
- name: path10.basename(file),
3845
+ name: path11.basename(file),
3702
3846
  file,
3703
3847
  type: "e2e",
3704
3848
  status: "passed",
@@ -3933,7 +4077,7 @@ function calculateAverageMetrics(results) {
3933
4077
 
3934
4078
  // src/services/reporter.ts
3935
4079
  import fs10 from "fs";
3936
- import path11 from "path";
4080
+ import path12 from "path";
3937
4081
  function aggregateResults(results) {
3938
4082
  const summary = {
3939
4083
  total: 0,
@@ -4129,13 +4273,13 @@ function generateMDReport(data) {
4129
4273
  }
4130
4274
  function writeReportToDisk(data, outputDir) {
4131
4275
  const md = generateMDReport(data);
4132
- const dir = path11.resolve(outputDir);
4276
+ const dir = path12.resolve(outputDir);
4133
4277
  if (!fs10.existsSync(dir)) {
4134
4278
  fs10.mkdirSync(dir, { recursive: true });
4135
4279
  }
4136
- const mdPath = path11.join(dir, "report.md");
4280
+ const mdPath = path12.join(dir, "report.md");
4137
4281
  fs10.writeFileSync(mdPath, md, "utf-8");
4138
- const jsonPath = path11.join(dir, "report.json");
4282
+ const jsonPath = path12.join(dir, "report.json");
4139
4283
  fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
4140
4284
  return mdPath;
4141
4285
  }
@@ -4356,7 +4500,7 @@ async function executeRun(options) {
4356
4500
  const reportData = aggregateResults(results);
4357
4501
  const outputDir = config.report.outputDir || "qat-report";
4358
4502
  const reportPath = writeReportToDisk(reportData, outputDir);
4359
- const relativePath = path12.relative(process.cwd(), reportPath);
4503
+ const relativePath = path13.relative(process.cwd(), reportPath);
4360
4504
  console.log(chalk6.gray(`
4361
4505
  \u62A5\u544A\u5DF2\u751F\u6210: ${chalk6.cyan(relativePath)}`));
4362
4506
  console.log();
@@ -4743,8 +4887,8 @@ async function checkDevServer(config) {
4743
4887
  }
4744
4888
  async function startDevServer(config) {
4745
4889
  const { spawn } = await import("child_process");
4746
- const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4747
- const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4890
+ const hasPnpm = fs11.existsSync(path13.join(process.cwd(), "pnpm-lock.yaml"));
4891
+ const hasYarn = fs11.existsSync(path13.join(process.cwd(), "yarn.lock"));
4748
4892
  const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4749
4893
  console.log(chalk6.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
4750
4894
  const child = spawn(pkgCmd, ["run", "dev"], {
@@ -4773,13 +4917,13 @@ async function startDevServer(config) {
4773
4917
  }
4774
4918
  function saveRunResults(results) {
4775
4919
  if (results.length === 0) return;
4776
- const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
4920
+ const resultsPath = path13.join(process.cwd(), RESULTS_DIR);
4777
4921
  if (!fs11.existsSync(resultsPath)) {
4778
4922
  fs11.mkdirSync(resultsPath, { recursive: true });
4779
4923
  }
4780
4924
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4781
4925
  const fileName = `result-${timestamp}.json`;
4782
- const filePath = path12.join(resultsPath, fileName);
4926
+ const filePath = path13.join(resultsPath, fileName);
4783
4927
  const data = {
4784
4928
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4785
4929
  results
@@ -4789,13 +4933,13 @@ function saveRunResults(results) {
4789
4933
  const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4790
4934
  while (files.length > 20) {
4791
4935
  const oldest = files.shift();
4792
- fs11.unlinkSync(path12.join(resultsPath, oldest));
4936
+ fs11.unlinkSync(path13.join(resultsPath, oldest));
4793
4937
  }
4794
4938
  } catch {
4795
4939
  }
4796
4940
  }
4797
4941
  function checkTestDependencies(types) {
4798
- const pkgPath = path12.join(process.cwd(), "package.json");
4942
+ const pkgPath = path13.join(process.cwd(), "package.json");
4799
4943
  let allDeps = {};
4800
4944
  if (fs11.existsSync(pkgPath)) {
4801
4945
  try {
@@ -4821,8 +4965,8 @@ function checkTestDependencies(types) {
4821
4965
  return missingDeps;
4822
4966
  }
4823
4967
  async function installTestDependencies(missingDeps) {
4824
- const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4825
- const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4968
+ const hasPnpm = fs11.existsSync(path13.join(process.cwd(), "pnpm-lock.yaml"));
4969
+ const hasYarn = fs11.existsSync(path13.join(process.cwd(), "yarn.lock"));
4826
4970
  const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4827
4971
  const { execFile: execFile5 } = await import("child_process");
4828
4972
  let allSuccess = true;
@@ -4990,7 +5134,7 @@ function showStatus() {
4990
5134
  import chalk8 from "chalk";
4991
5135
  import ora6 from "ora";
4992
5136
  import fs12 from "fs";
4993
- import path13 from "path";
5137
+ import path14 from "path";
4994
5138
  var RESULTS_DIR2 = ".qat-results";
4995
5139
  function registerReportCommand(program2) {
4996
5140
  program2.command("report").description("\u751F\u6210\u6D4B\u8BD5\u62A5\u544A - \u805A\u5408\u6240\u6709\u6D4B\u8BD5\u7ED3\u679C\u5E76\u8F93\u51FAHTML").option("-o, --output <dir>", "\u62A5\u544A\u8F93\u51FA\u76EE\u5F55").option("--open", "\u751F\u6210\u540E\u81EA\u52A8\u6253\u5F00\u62A5\u544A", false).action(async (options) => {
@@ -5026,13 +5170,13 @@ async function executeReport(options) {
5026
5170
  }
5027
5171
  function collectResults() {
5028
5172
  const results = [];
5029
- const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
5173
+ const resultsPath = path14.join(process.cwd(), RESULTS_DIR2);
5030
5174
  if (!fs12.existsSync(resultsPath)) {
5031
5175
  return results;
5032
5176
  }
5033
5177
  const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
5034
5178
  if (files.length > 0) {
5035
- const latestFile = path13.join(resultsPath, files[0]);
5179
+ const latestFile = path14.join(resultsPath, files[0]);
5036
5180
  try {
5037
5181
  const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
5038
5182
  if (Array.isArray(data)) {
@@ -5046,22 +5190,22 @@ function collectResults() {
5046
5190
  return results;
5047
5191
  }
5048
5192
  function saveResultToHistory(reportData) {
5049
- const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
5193
+ const resultsPath = path14.join(process.cwd(), RESULTS_DIR2);
5050
5194
  if (!fs12.existsSync(resultsPath)) {
5051
5195
  fs12.mkdirSync(resultsPath, { recursive: true });
5052
5196
  }
5053
5197
  const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
5054
5198
  const fileName = `result-${timestamp}.json`;
5055
- const filePath = path13.join(resultsPath, fileName);
5199
+ const filePath = path14.join(resultsPath, fileName);
5056
5200
  fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
5057
5201
  const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
5058
5202
  while (files.length > 20) {
5059
5203
  const oldest = files.shift();
5060
- fs12.unlinkSync(path13.join(resultsPath, oldest));
5204
+ fs12.unlinkSync(path14.join(resultsPath, oldest));
5061
5205
  }
5062
5206
  }
5063
5207
  function displayReportResult(reportPath, data) {
5064
- const relativePath = path13.relative(process.cwd(), reportPath);
5208
+ const relativePath = path14.relative(process.cwd(), reportPath);
5065
5209
  const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
5066
5210
  console.log();
5067
5211
  console.log(chalk8.green(" \u2713 \u6D4B\u8BD5\u62A5\u544A\u5DF2\u751F\u6210"));
@@ -5119,7 +5263,7 @@ import ora7 from "ora";
5119
5263
 
5120
5264
  // src/services/visual.ts
5121
5265
  import fs13 from "fs";
5122
- import path14 from "path";
5266
+ import path15 from "path";
5123
5267
  import pixelmatch from "pixelmatch";
5124
5268
  import { PNG } from "pngjs";
5125
5269
  function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
@@ -5158,7 +5302,7 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
5158
5302
  const passed = diffRatio <= threshold;
5159
5303
  let diffPath;
5160
5304
  if (diffPixels > 0) {
5161
- const diffDir = path14.dirname(diffOutputPath);
5305
+ const diffDir = path15.dirname(diffOutputPath);
5162
5306
  if (!fs13.existsSync(diffDir)) {
5163
5307
  fs13.mkdirSync(diffDir, { recursive: true });
5164
5308
  }
@@ -5179,7 +5323,7 @@ function createBaseline(currentPath, baselinePath) {
5179
5323
  if (!fs13.existsSync(currentPath)) {
5180
5324
  throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
5181
5325
  }
5182
- const baselineDir = path14.dirname(baselinePath);
5326
+ const baselineDir = path15.dirname(baselinePath);
5183
5327
  if (!fs13.existsSync(baselineDir)) {
5184
5328
  fs13.mkdirSync(baselineDir, { recursive: true });
5185
5329
  }
@@ -5193,9 +5337,9 @@ function updateAllBaselines(currentDir, baselineDir) {
5193
5337
  }
5194
5338
  const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5195
5339
  for (const file of files) {
5196
- const currentPath = path14.join(currentDir, file);
5197
- const baselinePath = path14.join(baselineDir, file);
5198
- const baselineDirAbs = path14.dirname(baselinePath);
5340
+ const currentPath = path15.join(currentDir, file);
5341
+ const baselinePath = path15.join(baselineDir, file);
5342
+ const baselineDirAbs = path15.dirname(baselinePath);
5199
5343
  if (!fs13.existsSync(baselineDirAbs)) {
5200
5344
  fs13.mkdirSync(baselineDirAbs, { recursive: true });
5201
5345
  }
@@ -5211,7 +5355,7 @@ function cleanBaselines(baselineDir) {
5211
5355
  const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5212
5356
  let count = 0;
5213
5357
  for (const file of files) {
5214
- fs13.unlinkSync(path14.join(baselineDir, file));
5358
+ fs13.unlinkSync(path15.join(baselineDir, file));
5215
5359
  count++;
5216
5360
  }
5217
5361
  return count;
@@ -5223,7 +5367,7 @@ function cleanDiffs(diffDir) {
5223
5367
  const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5224
5368
  let count = 0;
5225
5369
  for (const file of files) {
5226
- fs13.unlinkSync(path14.join(diffDir, file));
5370
+ fs13.unlinkSync(path15.join(diffDir, file));
5227
5371
  count++;
5228
5372
  }
5229
5373
  return count;
@@ -5235,9 +5379,9 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5235
5379
  }
5236
5380
  const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5237
5381
  for (const file of currentFiles) {
5238
- const currentPath = path14.join(currentDir, file);
5239
- const baselinePath = path14.join(baselineDir, file);
5240
- const diffPath = path14.join(diffDir, file);
5382
+ const currentPath = path15.join(currentDir, file);
5383
+ const baselinePath = path15.join(baselineDir, file);
5384
+ const diffPath = path15.join(diffDir, file);
5241
5385
  if (!fs13.existsSync(baselinePath)) {
5242
5386
  createBaseline(currentPath, baselinePath);
5243
5387
  results.push({
@@ -5271,7 +5415,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5271
5415
 
5272
5416
  // src/commands/visual.ts
5273
5417
  import fs14 from "fs";
5274
- import path15 from "path";
5418
+ import path16 from "path";
5275
5419
  function registerVisualCommand(program2) {
5276
5420
  program2.command("visual").description("\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5 - \u622A\u56FE\u6BD4\u5BF9\u4E0E\u57FA\u7EBF\u7BA1\u7406").argument("<action>", "\u64CD\u4F5C\u7C7B\u578B (test|approve|clean)").option("--threshold <number>", "\u50CF\u7D20\u5DEE\u5F02\u9608\u503C (0-1)", "0.1").action(async (action, options) => {
5277
5421
  try {
@@ -5346,9 +5490,9 @@ async function runVisualTest(threshold, baselineDir, diffDir, config) {
5346
5490
  }
5347
5491
  function findCurrentScreenshotsDir(baselineDir) {
5348
5492
  const possibleDirs = [
5349
- path15.join(process.cwd(), "test-results"),
5350
- path15.join(process.cwd(), "tests", "visual", "current"),
5351
- path15.join(process.cwd(), baselineDir, "..", "current")
5493
+ path16.join(process.cwd(), "test-results"),
5494
+ path16.join(process.cwd(), "tests", "visual", "current"),
5495
+ path16.join(process.cwd(), baselineDir, "..", "current")
5352
5496
  ];
5353
5497
  for (const dir of possibleDirs) {
5354
5498
  if (fs14.existsSync(dir)) {
@@ -5364,7 +5508,7 @@ function findPngFiles(dir) {
5364
5508
  if (!fs14.existsSync(d)) return;
5365
5509
  const entries = fs14.readdirSync(d, { withFileTypes: true });
5366
5510
  for (const entry of entries) {
5367
- const fullPath = path15.join(d, entry.name);
5511
+ const fullPath = path16.join(d, entry.name);
5368
5512
  if (entry.isDirectory() && entry.name !== "node_modules") {
5369
5513
  walk(fullPath);
5370
5514
  } else if (entry.name.endsWith(".png")) {
@@ -5393,7 +5537,7 @@ function displayVisualResults(results, threshold) {
5393
5537
  console.log();
5394
5538
  console.log(chalk9.green(" \u901A\u8FC7\u7684\u622A\u56FE:"));
5395
5539
  for (const result of passed) {
5396
- const name = path15.basename(result.baselinePath);
5540
+ const name = path16.basename(result.baselinePath);
5397
5541
  if (result.totalPixels > 0) {
5398
5542
  const diffPct = (result.diffRatio * 100).toFixed(2);
5399
5543
  console.log(chalk9.green(` \u2713 ${name} (\u5DEE\u5F02: ${diffPct}%)`));
@@ -5406,7 +5550,7 @@ function displayVisualResults(results, threshold) {
5406
5550
  console.log();
5407
5551
  console.log(chalk9.red(" \u5931\u8D25\u7684\u622A\u56FE:"));
5408
5552
  for (const result of failed) {
5409
- const name = path15.basename(result.baselinePath);
5553
+ const name = path16.basename(result.baselinePath);
5410
5554
  if (result.diffPixels === -1) {
5411
5555
  console.log(chalk9.red(` \u2717 ${name} (\u5C3A\u5BF8\u4E0D\u5339\u914D)`));
5412
5556
  } else {
@@ -5414,7 +5558,7 @@ function displayVisualResults(results, threshold) {
5414
5558
  console.log(chalk9.red(` \u2717 ${name} (\u5DEE\u5F02: ${diffPct}%)`));
5415
5559
  }
5416
5560
  if (result.diffPath) {
5417
- console.log(chalk9.gray(` \u5DEE\u5F02\u56FE: ${path15.relative(process.cwd(), result.diffPath)}`));
5561
+ console.log(chalk9.gray(` \u5DEE\u5F02\u56FE: ${path16.relative(process.cwd(), result.diffPath)}`));
5418
5562
  }
5419
5563
  }
5420
5564
  console.log();
@@ -5461,7 +5605,7 @@ import inquirer4 from "inquirer";
5461
5605
  import ora8 from "ora";
5462
5606
  import { execFile as execFile4 } from "child_process";
5463
5607
  import fs15 from "fs";
5464
- import path16 from "path";
5608
+ import path17 from "path";
5465
5609
  var DEPENDENCY_GROUPS = [
5466
5610
  {
5467
5611
  name: "Vitest (\u5355\u5143/\u7EC4\u4EF6/API\u6D4B\u8BD5)",
@@ -5495,7 +5639,7 @@ function registerSetupCommand(program2) {
5495
5639
  async function executeSetup(options) {
5496
5640
  console.log(chalk10.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
5497
5641
  const projectInfo = detectProject();
5498
- if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
5642
+ if (!fs15.existsSync(path17.join(process.cwd(), "package.json"))) {
5499
5643
  throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
5500
5644
  }
5501
5645
  if (projectInfo.frameworkConfidence > 0) {
@@ -5529,8 +5673,8 @@ async function executeSetup(options) {
5529
5673
  }
5530
5674
  ]);
5531
5675
  if (chooseDir !== "root") {
5532
- installDir = path16.join(process.cwd(), chooseDir);
5533
- if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
5676
+ installDir = path17.join(process.cwd(), chooseDir);
5677
+ if (!fs15.existsSync(path17.join(installDir, "package.json"))) {
5534
5678
  throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
5535
5679
  }
5536
5680
  }
@@ -5569,7 +5713,7 @@ ${allPackages.map((p) => ` - ${p}`).join("\n")}`,
5569
5713
  const pm = getPackageManager(projectInfo.packageManager);
5570
5714
  const installCmd = pm === "npm" ? "npm install -D" : pm === "yarn" ? "yarn add -D" : pm === "pnpm" ? "pnpm add -D" : "bun add -D";
5571
5715
  for (const group of selectedGroups) {
5572
- const dirHint = installDir !== process.cwd() ? ` (\u5728 ${path16.relative(process.cwd(), installDir) || installDir})` : "";
5716
+ const dirHint = installDir !== process.cwd() ? ` (\u5728 ${path17.relative(process.cwd(), installDir) || installDir})` : "";
5573
5717
  console.log(chalk10.white(` ${installCmd} ${group.packages.join(" ")}${dirHint}`));
5574
5718
  if (group.postInstall) {
5575
5719
  for (const cmd of group.postInstall) {
@@ -5850,7 +5994,7 @@ async function executeChange(_options) {
5850
5994
  }
5851
5995
 
5852
5996
  // src/cli.ts
5853
- var VERSION = "0.3.04";
5997
+ var VERSION = "0.3.06";
5854
5998
  function printLogo() {
5855
5999
  const logo = `
5856
6000
  ${chalk13.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}