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/cli.js CHANGED
@@ -11,11 +11,11 @@ import { Command } from "commander";
11
11
  import chalk12 from "chalk";
12
12
 
13
13
  // src/commands/init.ts
14
- import chalk3 from "chalk";
14
+ import chalk2 from "chalk";
15
15
  import inquirer from "inquirer";
16
- import ora2 from "ora";
17
- import fs7 from "fs";
18
- import path7 from "path";
16
+ import ora from "ora";
17
+ import fs6 from "fs";
18
+ import path6 from "path";
19
19
 
20
20
  // src/services/detector.ts
21
21
  import fs from "fs";
@@ -1017,8 +1017,13 @@ ${error.actual ? `\u5B9E\u9645\u503C: ${error.actual}` : ""}`;
1017
1017
  6. \u5982\u679C\u6709 props/emits \u4FE1\u606F\uFF0C\u52A1\u5FC5\u9488\u5BF9\u6BCF\u4E2A prop \u548C emit \u751F\u6210\u6D4B\u8BD5`;
1018
1018
  }
1019
1019
  buildGenerateTestUserPrompt(req) {
1020
+ const importPath = this.computeTestImportPath(req.type, req.target);
1020
1021
  let prompt = `\u8BF7\u4E3A\u4EE5\u4E0B\u6587\u4EF6\u751F\u6210${req.type}\u6D4B\u8BD5\u4EE3\u7801:
1021
1022
  \u76EE\u6807\u6587\u4EF6: ${req.target}
1023
+ \u6D4B\u8BD5\u6587\u4EF6\u5C06\u653E\u5728: ${this.getTestOutputDir(req.type)}/
1024
+ \u6B63\u786E\u7684 import \u8DEF\u5F84: ${importPath}
1025
+
1026
+ \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
1022
1027
  `;
1023
1028
  if (req.analysis) {
1024
1029
  prompt += "\n\u6E90\u7801\u5206\u6790\u7ED3\u679C:\n";
@@ -1068,6 +1073,46 @@ ${req.context}
1068
1073
  }
1069
1074
  return prompt;
1070
1075
  }
1076
+ /**
1077
+ * 根据测试类型和源文件路径,计算从测试文件到源文件的正确相对导入路径
1078
+ */
1079
+ computeTestImportPath(testType, targetPath) {
1080
+ if (!targetPath) return targetPath;
1081
+ const testDirMap = {
1082
+ unit: "tests/unit",
1083
+ component: "tests/component",
1084
+ e2e: "tests/e2e",
1085
+ api: "tests/api",
1086
+ visual: "tests/visual",
1087
+ performance: "tests/e2e"
1088
+ };
1089
+ const testDir = testDirMap[testType];
1090
+ if (!testDir) return targetPath;
1091
+ if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
1092
+ return targetPath;
1093
+ }
1094
+ const depth = testDir.split("/").length;
1095
+ const prefix = "../".repeat(depth);
1096
+ let cleanPath = targetPath.replace(/^\.\//, "");
1097
+ if (!cleanPath.endsWith(".vue")) {
1098
+ cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
1099
+ }
1100
+ return `${prefix}${cleanPath}`;
1101
+ }
1102
+ /**
1103
+ * 获取测试输出目录
1104
+ */
1105
+ getTestOutputDir(testType) {
1106
+ const dirMap = {
1107
+ unit: "tests/unit",
1108
+ component: "tests/component",
1109
+ e2e: "tests/e2e",
1110
+ api: "tests/api",
1111
+ visual: "tests/visual",
1112
+ performance: "tests/e2e"
1113
+ };
1114
+ return dirMap[testType] || "tests/unit";
1115
+ }
1071
1116
  parseGenerateTestResponse(content) {
1072
1117
  const codeBlockMatch = content.match(/```(?:typescript|ts|javascript|js)?\s*\n([\s\S]*?)```/);
1073
1118
  const code = codeBlockMatch ? codeBlockMatch[1].trim() : content.replace(/^(?:```[\s\S]*?\n)?/, "").replace(/\n?```$/, "").trim();
@@ -1096,10 +1141,12 @@ ISSUES: \u95EE\u9898\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F
1096
1141
  SUGGESTIONS: \u6539\u8FDB\u5EFA\u8BAE\u5217\u8868\uFF08\u6BCF\u884C\u4E00\u4E2A\uFF0C\u683C\u5F0F "- \u5EFA\u8BAE\u63CF\u8FF0"\uFF09`;
1097
1142
  }
1098
1143
  buildReviewTestUserPrompt(req) {
1144
+ const importPath = this.computeTestImportPath(req.testType, req.target);
1099
1145
  let prompt = `\u8BF7\u5BA1\u67E5\u4EE5\u4E0B\u6D4B\u8BD5\u7528\u4F8B\u662F\u5426\u4E0E\u6E90\u7801\u8D34\u5207\u4E14\u51C6\u786E\u3002
1100
1146
 
1101
1147
  \u88AB\u6D4B\u6587\u4EF6: ${req.target}
1102
1148
  \u6D4B\u8BD5\u7C7B\u578B: ${req.testType}
1149
+ \u6B63\u786E\u7684 import \u8DEF\u5F84\u5E94\u4E3A: ${importPath}\uFF08\u6D4B\u8BD5\u6587\u4EF6\u4F4D\u4E8E ${this.getTestOutputDir(req.testType)}/\uFF09
1103
1150
 
1104
1151
  --- \u6E90\u7801\u5185\u5BB9 ---
1105
1152
  \`\`\`typescript
@@ -1262,99 +1309,6 @@ async function testAIConnection(config) {
1262
1309
  return provider.testConnection();
1263
1310
  }
1264
1311
 
1265
- // src/services/test-reviewer.ts
1266
- import chalk2 from "chalk";
1267
- import ora from "ora";
1268
- var MAX_RETRIES = 3;
1269
- var REVIEW_THRESHOLD = 0.6;
1270
- async function generateWithReview(params) {
1271
- const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
1272
- const generatorProvider = createAIProvider(aiConfig);
1273
- const reviewerProvider = createAIProvider(aiConfig);
1274
- if (!generatorProvider.capabilities.generateTest) {
1275
- throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
1276
- }
1277
- let currentCode = "";
1278
- let currentDescription = "";
1279
- let currentConfidence = 0;
1280
- let lastReview = null;
1281
- let approved = false;
1282
- let attempts = 0;
1283
- for (let i = 0; i < MAX_RETRIES; i++) {
1284
- attempts = i + 1;
1285
- onAttempt?.(attempts, MAX_RETRIES);
1286
- const generationPrompt = i === 0 ? void 0 : `\u4E0A\u6B21\u751F\u6210\u7684\u6D4B\u8BD5\u672A\u901A\u8FC7\u5BA1\u8BA1\uFF0C\u8BF7\u6839\u636E\u4EE5\u4E0B\u53CD\u9988\u91CD\u65B0\u751F\u6210\uFF1A
1287
- \u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
1288
- \u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
1289
- \u5177\u4F53\u95EE\u9898:
1290
- ${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
1291
- \u6539\u8FDB\u5EFA\u8BAE:
1292
- ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
1293
-
1294
- \u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
1295
- const generateResponse = await generatorProvider.generateTest({
1296
- type: testType,
1297
- target: targetPath,
1298
- context: i === 0 ? sourceCode : `${sourceCode}
1299
-
1300
- --- \u5BA1\u8BA1\u53CD\u9988 ---
1301
- ${generationPrompt}`,
1302
- framework: framework || void 0,
1303
- analysis
1304
- });
1305
- currentCode = generateResponse.code;
1306
- currentDescription = generateResponse.description;
1307
- currentConfidence = generateResponse.confidence;
1308
- const reviewRequest = {
1309
- target: targetPath,
1310
- sourceCode,
1311
- analysis,
1312
- testCode: currentCode,
1313
- testType,
1314
- generationDescription: currentDescription
1315
- };
1316
- lastReview = await reviewerProvider.reviewTest(reviewRequest);
1317
- approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
1318
- if (approved) {
1319
- break;
1320
- }
1321
- }
1322
- return {
1323
- code: currentCode,
1324
- description: currentDescription,
1325
- confidence: currentConfidence,
1326
- approved,
1327
- attempts,
1328
- reviewScore: lastReview?.score ?? 0,
1329
- reviewFeedback: lastReview?.feedback ?? "",
1330
- reviewIssues: lastReview?.issues ?? [],
1331
- reviewSuggestions: lastReview?.suggestions ?? []
1332
- };
1333
- }
1334
- function printReviewReport(report) {
1335
- if (report.length === 0) return;
1336
- const approved = report.filter((r) => r.approved);
1337
- const failed = report.filter((r) => !r.approved);
1338
- console.log();
1339
- console.log(chalk2.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
1340
- console.log(chalk2.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1341
- for (const entry of approved) {
1342
- console.log(` ${chalk2.green("\u2713")} ${chalk2.gray(entry.target)} ${chalk2.green(`(${(entry.score * 100).toFixed(0)}%`)}${entry.attempts > 1 ? chalk2.yellow(` ${entry.attempts}\u6B21\u5C1D\u8BD5`) : ""})`);
1343
- }
1344
- for (const entry of failed) {
1345
- console.log(` ${chalk2.red("\u2717")} ${chalk2.gray(entry.target)} ${chalk2.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
1346
- if (entry.issues.length > 0) {
1347
- for (const issue of entry.issues.slice(0, 3)) {
1348
- console.log(chalk2.gray(` - ${issue}`));
1349
- }
1350
- }
1351
- if (entry.feedback) {
1352
- console.log(chalk2.gray(` \u53CD\u9988: ${entry.feedback}`));
1353
- }
1354
- }
1355
- console.log();
1356
- }
1357
-
1358
1312
  // src/services/source-analyzer.ts
1359
1313
  import fs4 from "fs";
1360
1314
  import path4 from "path";
@@ -1790,132 +1744,496 @@ function generateMockRoutesFromAPICalls(apiCalls) {
1790
1744
  return routes;
1791
1745
  }
1792
1746
 
1793
- // src/services/template.ts
1747
+ // src/services/global-config.ts
1794
1748
  import fs5 from "fs";
1795
1749
  import path5 from "path";
1796
- import { fileURLToPath } from "url";
1797
- import Handlebars from "handlebars";
1798
- var __filename = fileURLToPath(import.meta.url);
1799
- var __dirname = path5.dirname(__filename);
1800
- var TEMPLATE_MAP = {
1801
- unit: "unit-test.hbs",
1802
- component: "component-test.hbs",
1803
- e2e: "e2e-test.hbs",
1804
- api: "api-test.hbs",
1805
- visual: "visual-test.hbs",
1806
- performance: "performance-test.hbs"
1807
- };
1808
- var customTemplates = /* @__PURE__ */ new Map();
1809
- Handlebars.registerHelper("eq", (a, b) => a === b);
1810
- Handlebars.registerHelper("neq", (a, b) => a !== b);
1811
- Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
1812
- Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
1813
- Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
1814
- Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
1815
- Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
1816
- Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
1817
- Handlebars.registerHelper("and", (...args) => {
1818
- args.pop();
1819
- return args.every(Boolean);
1820
- });
1821
- Handlebars.registerHelper("or", (...args) => {
1822
- args.pop();
1823
- return args.some(Boolean);
1824
- });
1825
- Handlebars.registerHelper("propDefaultValue", (prop) => {
1826
- if (!prop.defaultValue) return "undefined";
1827
- const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
1828
- return map[prop.type] || prop.defaultValue;
1829
- });
1830
- Handlebars.registerHelper("propTestValue", (prop) => {
1831
- const map = {
1832
- String: "'test-value'",
1833
- Number: "42",
1834
- Boolean: "true",
1835
- Array: "[1, 2, 3]",
1836
- Object: '{ key: "value" }'
1837
- };
1838
- return map[prop.type] || "'test-value'";
1839
- });
1840
- function renderTemplate(type, context) {
1841
- const templateContent = loadTemplate(type);
1842
- const template = Handlebars.compile(templateContent);
1843
- const fullContext = {
1844
- vueVersion: 3,
1845
- typescript: true,
1846
- imports: [],
1847
- extraImports: [],
1848
- globalPlugins: [],
1849
- globalStubs: [],
1850
- mountOptions: "",
1851
- hasAnalysis: false,
1852
- exports: [],
1853
- functionExports: [],
1854
- valueExports: [],
1855
- props: [],
1856
- emits: [],
1857
- methods: [],
1858
- computed: [],
1859
- isVueComponent: false,
1860
- requiredProps: [],
1861
- optionalProps: [],
1862
- ...context,
1863
- framework: context.framework || "vue",
1864
- camelName: context.camelName || toCamelCase(context.name),
1865
- pascalName: context.pascalName || toPascalCase(context.name)
1866
- };
1867
- if (fullContext.exports && fullContext.exports.length > 0) {
1868
- fullContext.hasAnalysis = true;
1869
- fullContext.functionExports = fullContext.exports.filter(
1870
- (e) => e.kind === "function" || e.kind === "default"
1871
- );
1872
- fullContext.valueExports = fullContext.exports.filter(
1873
- (e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
1874
- );
1875
- }
1876
- if (fullContext.props && fullContext.props.length > 0) {
1877
- fullContext.isVueComponent = true;
1878
- fullContext.requiredProps = fullContext.props.filter((p) => p.required);
1879
- fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
1750
+ import os from "os";
1751
+ var QAT_DIR = path5.join(os.homedir(), ".qat");
1752
+ var AI_CONFIG_PATH = path5.join(QAT_DIR, "ai.json");
1753
+ function loadGlobalAIConfig() {
1754
+ if (!fs5.existsSync(AI_CONFIG_PATH)) {
1755
+ return null;
1880
1756
  }
1881
- if (fullContext.emits && fullContext.emits.length > 0) {
1882
- fullContext.isVueComponent = true;
1757
+ try {
1758
+ const content = fs5.readFileSync(AI_CONFIG_PATH, "utf-8");
1759
+ const config = JSON.parse(content);
1760
+ if (!config.baseUrl || !config.model) {
1761
+ return null;
1762
+ }
1763
+ return config;
1764
+ } catch {
1765
+ return null;
1883
1766
  }
1884
- return template(fullContext);
1885
1767
  }
1886
- function loadTemplate(type) {
1887
- const custom = customTemplates.get(type);
1888
- if (custom) return custom;
1889
- const templateDir = getTemplateDir();
1890
- const templateFile = TEMPLATE_MAP[type];
1891
- const templatePath = path5.join(templateDir, templateFile);
1892
- if (fs5.existsSync(templatePath)) {
1893
- return fs5.readFileSync(templatePath, "utf-8");
1768
+ function saveGlobalAIConfig(config) {
1769
+ if (!fs5.existsSync(QAT_DIR)) {
1770
+ fs5.mkdirSync(QAT_DIR, { recursive: true });
1894
1771
  }
1895
- return getBuiltinTemplate(type);
1772
+ fs5.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1896
1773
  }
1897
- function getTemplateDir() {
1898
- const projectTemplates = path5.join(process.cwd(), "templates");
1899
- if (fs5.existsSync(projectTemplates)) {
1900
- return projectTemplates;
1901
- }
1902
- return path5.join(__dirname, "..", "templates");
1774
+ function toAIConfig(globalConfig) {
1775
+ return {
1776
+ provider: globalConfig.provider || "openai",
1777
+ apiKey: globalConfig.apiKey || void 0,
1778
+ baseUrl: globalConfig.baseUrl,
1779
+ model: globalConfig.model
1780
+ };
1781
+ }
1782
+ function maskApiKey(apiKey) {
1783
+ if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
1784
+ if (apiKey.length <= 8) return "****";
1785
+ return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
1786
+ }
1787
+ function getAIConfigPath() {
1788
+ return AI_CONFIG_PATH;
1903
1789
  }
1904
- function getBuiltinTemplate(type) {
1905
- const templates = {
1906
- unit: `import { describe, it, expect } from 'vitest';
1907
- {{#if hasAnalysis}}
1908
- {{#each valueExports}}
1909
- import { {{name}} } from '{{../target}}';
1910
- {{/each}}
1911
- {{#each functionExports}}
1912
- import { {{name}} } from '{{../target}}';
1913
- {{/each}}
1914
- {{else}}
1915
- import { {{camelName}} } from '{{target}}';
1916
- {{/if}}
1917
1790
 
1918
- describe('{{name}}', () => {
1791
+ // src/commands/init.ts
1792
+ function registerInitCommand(program2) {
1793
+ program2.command("init").description("\u521D\u59CB\u5316\u6D4B\u8BD5\u9879\u76EE - \u68C0\u6D4B\u9879\u76EE\u3001\u751F\u6210\u914D\u7F6E\u3001\u81EA\u52A8\u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B").option("-f, --force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\u6587\u4EF6").action(async (options) => {
1794
+ try {
1795
+ await executeInit(options);
1796
+ } catch (error) {
1797
+ console.error(chalk2.red(`
1798
+ \u2717 ${error instanceof Error ? error.message : String(error)}
1799
+ `));
1800
+ process.exit(1);
1801
+ }
1802
+ });
1803
+ }
1804
+ async function executeInit(options) {
1805
+ const spinner = ora("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
1806
+ const projectInfo = detectProject();
1807
+ spinner.stop();
1808
+ displayProjectInfo(projectInfo);
1809
+ if (!projectInfo.isVue) {
1810
+ const { proceed } = await inquirer.prompt([
1811
+ {
1812
+ type: "confirm",
1813
+ name: "proceed",
1814
+ message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
1815
+ default: false
1816
+ }
1817
+ ]);
1818
+ if (!proceed) {
1819
+ console.log(chalk2.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
1820
+ return;
1821
+ }
1822
+ }
1823
+ let globalAI = loadGlobalAIConfig();
1824
+ if (!globalAI) {
1825
+ console.log(chalk2.cyan(" AI \u6A21\u578B\u914D\u7F6E (\u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\uFF0C\u4E4B\u540E\u53EF\u901A\u8FC7 qat change \u4FEE\u6539)\n"));
1826
+ globalAI = await promptAIConfig();
1827
+ saveGlobalAIConfig(globalAI);
1828
+ console.log(chalk2.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
1829
+ `));
1830
+ } else {
1831
+ console.log(chalk2.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk2.white(globalAI.model)} @ ${chalk2.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
1832
+ `));
1833
+ }
1834
+ const aiConfig = toAIConfig(globalAI);
1835
+ if (aiConfig.apiKey || aiConfig.baseUrl) {
1836
+ const testSpinner = ora(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
1837
+ try {
1838
+ const result = await testAIConnection(aiConfig);
1839
+ if (result.ok) {
1840
+ testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk2.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
1841
+ } else {
1842
+ testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
1843
+ console.log(chalk2.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
1844
+ }
1845
+ } catch (error) {
1846
+ testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
1847
+ }
1848
+ }
1849
+ const config = buildProjectConfig(projectInfo);
1850
+ let configPath;
1851
+ const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
1852
+ const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
1853
+ const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
1854
+ if (configExists && !options.force) {
1855
+ const { overwrite } = await inquirer.prompt([
1856
+ {
1857
+ type: "confirm",
1858
+ name: "overwrite",
1859
+ message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
1860
+ default: true
1861
+ }
1862
+ ]);
1863
+ if (!overwrite) {
1864
+ console.log(chalk2.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
1865
+ configPath = existingConfigPath;
1866
+ } else {
1867
+ const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
1868
+ try {
1869
+ configPath = await writeConfigFile(process.cwd(), config, true);
1870
+ fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
1871
+ } catch (error) {
1872
+ fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
1873
+ throw error;
1874
+ }
1875
+ }
1876
+ } else {
1877
+ const fileSpinner = ora("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
1878
+ try {
1879
+ configPath = await writeConfigFile(process.cwd(), config, options.force);
1880
+ fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
1881
+ } catch (error) {
1882
+ fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
1883
+ throw error;
1884
+ }
1885
+ }
1886
+ const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
1887
+ const createdDirs = createTestDirectories(config);
1888
+ dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
1889
+ if (config.mock?.enabled !== false) {
1890
+ const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
1891
+ initMockRoutesDir(mockDir);
1892
+ const srcDir2 = config.project?.srcDir || "src";
1893
+ const apiCalls = scanAPICalls(srcDir2);
1894
+ if (apiCalls.length > 0) {
1895
+ const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
1896
+ const mockFilePath = path6.join(process.cwd(), mockDir, "auto-generated.json");
1897
+ if (!fs6.existsSync(mockFilePath)) {
1898
+ fs6.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
1899
+ console.log(chalk2.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
1900
+ }
1901
+ } else {
1902
+ console.log(chalk2.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
1903
+ }
1904
+ }
1905
+ const srcDir = config.project?.srcDir || "src";
1906
+ const components = discoverVueComponents(process.cwd(), srcDir);
1907
+ const utilities = discoverUtilityFiles(process.cwd(), srcDir);
1908
+ const totalFiles = components.length + utilities.length;
1909
+ if (totalFiles > 0) {
1910
+ console.log(chalk2.cyan(`
1911
+ \u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
1912
+ }
1913
+ displayResult(configPath, createdDirs, totalFiles, projectInfo);
1914
+ }
1915
+ async function promptAIConfig() {
1916
+ const answers = await inquirer.prompt([
1917
+ {
1918
+ type: "input",
1919
+ name: "apiKey",
1920
+ message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
1921
+ default: ""
1922
+ },
1923
+ {
1924
+ type: "input",
1925
+ name: "baseUrl",
1926
+ message: "API Base URL:",
1927
+ default: "https://api.deepseek.com/v1",
1928
+ validate: (input) => {
1929
+ if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
1930
+ if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
1931
+ return true;
1932
+ }
1933
+ },
1934
+ {
1935
+ type: "input",
1936
+ name: "model",
1937
+ message: "\u6A21\u578B\u540D\u79F0:",
1938
+ default: "deepseek-chat",
1939
+ validate: (input) => {
1940
+ if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
1941
+ return true;
1942
+ }
1943
+ }
1944
+ ]);
1945
+ return {
1946
+ provider: "openai",
1947
+ apiKey: answers.apiKey?.trim() || "",
1948
+ baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
1949
+ model: answers.model?.trim() || "deepseek-chat"
1950
+ };
1951
+ }
1952
+ function buildProjectConfig(projectInfo) {
1953
+ return {
1954
+ project: {
1955
+ framework: projectInfo.framework,
1956
+ uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
1957
+ monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
1958
+ vite: projectInfo.isVite,
1959
+ srcDir: projectInfo.srcDir,
1960
+ appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
1961
+ },
1962
+ vitest: {
1963
+ enabled: true,
1964
+ coverage: true,
1965
+ globals: true,
1966
+ environment: "happy-dom"
1967
+ },
1968
+ playwright: {
1969
+ enabled: true,
1970
+ browsers: ["chromium"],
1971
+ baseURL: "http://localhost:5173",
1972
+ screenshot: "only-on-failure"
1973
+ },
1974
+ visual: {
1975
+ enabled: true,
1976
+ threshold: 0.1,
1977
+ baselineDir: "tests/visual/baseline",
1978
+ diffDir: "tests/visual/diff"
1979
+ },
1980
+ lighthouse: {
1981
+ enabled: true,
1982
+ urls: ["http://localhost:5173"],
1983
+ runs: 3,
1984
+ thresholds: {
1985
+ performance: 80,
1986
+ accessibility: 90
1987
+ }
1988
+ },
1989
+ mock: {
1990
+ enabled: true,
1991
+ port: 3456,
1992
+ routesDir: "tests/mock/routes"
1993
+ }
1994
+ };
1995
+ }
1996
+ function displayProjectInfo(info) {
1997
+ console.log(chalk2.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
1998
+ console.log(chalk2.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1999
+ const items = [
2000
+ ["\u9879\u76EE\u540D\u79F0", info.name],
2001
+ ["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
2002
+ ["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
2003
+ ["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
2004
+ ["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
2005
+ ["TypeScript", info.typescript ? "\u662F" : "\u5426"],
2006
+ ["\u5305\u7BA1\u7406\u5668", info.packageManager],
2007
+ ["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
2008
+ ["\u6E90\u7801\u76EE\u5F55", info.srcDir]
2009
+ ];
2010
+ if (info.appDirs.length > 0) {
2011
+ items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
2012
+ }
2013
+ if (info.testFrameworks.length > 0) {
2014
+ items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
2015
+ }
2016
+ if (info.componentDirs.length > 0) {
2017
+ items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
2018
+ }
2019
+ for (const [label, value] of items) {
2020
+ const displayValue = value === true ? chalk2.green("\u2713") : value === false ? chalk2.red("\u2717") : String(value);
2021
+ console.log(` ${chalk2.white(label.padEnd(12))} ${displayValue}`);
2022
+ }
2023
+ console.log();
2024
+ }
2025
+ function createTestDirectories(config) {
2026
+ const dirs = [];
2027
+ const dirMap = {
2028
+ "tests": true,
2029
+ "tests/unit": config.vitest?.enabled !== false,
2030
+ "tests/component": config.vitest?.enabled !== false,
2031
+ "tests/e2e": config.playwright?.enabled !== false,
2032
+ "tests/api": config.mock?.enabled !== false,
2033
+ "tests/visual": config.visual?.enabled !== false,
2034
+ "tests/visual/baseline": config.visual?.enabled !== false,
2035
+ "tests/visual/diff": config.visual?.enabled !== false,
2036
+ "tests/mock": config.mock?.enabled !== false,
2037
+ "tests/mock/routes": config.mock?.enabled !== false
2038
+ };
2039
+ for (const [dir, shouldCreate] of Object.entries(dirMap)) {
2040
+ if (shouldCreate) {
2041
+ const fullPath = path6.join(process.cwd(), dir);
2042
+ if (!fs6.existsSync(fullPath)) {
2043
+ fs6.mkdirSync(fullPath, { recursive: true });
2044
+ dirs.push(dir);
2045
+ }
2046
+ }
2047
+ }
2048
+ return dirs;
2049
+ }
2050
+ function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo) {
2051
+ const relativeConfig = path6.relative(process.cwd(), configPath);
2052
+ console.log(chalk2.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
2053
+ console.log(chalk2.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
2054
+ console.log(chalk2.gray(` ${relativeConfig}`));
2055
+ console.log();
2056
+ if (createdDirs.length > 0) {
2057
+ console.log(chalk2.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
2058
+ for (const dir of createdDirs) {
2059
+ console.log(chalk2.gray(` ${dir}/`));
2060
+ }
2061
+ console.log();
2062
+ }
2063
+ console.log(chalk2.cyan(" \u4E0B\u4E00\u6B65:"));
2064
+ if (totalTestableFiles > 0) {
2065
+ console.log(chalk2.gray(` 1. qat create \u9009\u62E9\u6587\u4EF6\u5E76\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (\u53D1\u73B0 ${totalTestableFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6)`));
2066
+ } else {
2067
+ console.log(chalk2.gray(" 1. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
2068
+ }
2069
+ console.log(chalk2.gray(" 2. qat run \u6267\u884C\u6D4B\u8BD5"));
2070
+ console.log(chalk2.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
2071
+ if (projectInfo.testFrameworks.length === 0) {
2072
+ console.log();
2073
+ console.log(chalk2.yellow(" \u26A0 \u672A\u68C0\u6D4B\u5230\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\uFF0C\u8FD0\u884C qat run \u65F6\u4F1A\u63D0\u793A\u5B89\u88C5"));
2074
+ }
2075
+ console.log();
2076
+ }
2077
+
2078
+ // src/commands/create.ts
2079
+ import chalk4 from "chalk";
2080
+ import inquirer2 from "inquirer";
2081
+ import ora3 from "ora";
2082
+ import fs8 from "fs";
2083
+ import path8 from "path";
2084
+
2085
+ // src/services/template.ts
2086
+ import fs7 from "fs";
2087
+ import path7 from "path";
2088
+ import { fileURLToPath } from "url";
2089
+ import Handlebars from "handlebars";
2090
+ var __filename = fileURLToPath(import.meta.url);
2091
+ var __dirname = path7.dirname(__filename);
2092
+ var TEMPLATE_MAP = {
2093
+ unit: "unit-test.hbs",
2094
+ component: "component-test.hbs",
2095
+ e2e: "e2e-test.hbs",
2096
+ api: "api-test.hbs",
2097
+ visual: "visual-test.hbs",
2098
+ performance: "performance-test.hbs"
2099
+ };
2100
+ var customTemplates = /* @__PURE__ */ new Map();
2101
+ Handlebars.registerHelper("eq", (a, b) => a === b);
2102
+ Handlebars.registerHelper("neq", (a, b) => a !== b);
2103
+ Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
2104
+ Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
2105
+ Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
2106
+ Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
2107
+ Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
2108
+ Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
2109
+ Handlebars.registerHelper("and", (...args) => {
2110
+ args.pop();
2111
+ return args.every(Boolean);
2112
+ });
2113
+ Handlebars.registerHelper("or", (...args) => {
2114
+ args.pop();
2115
+ return args.some(Boolean);
2116
+ });
2117
+ Handlebars.registerHelper("propDefaultValue", (prop) => {
2118
+ if (!prop.defaultValue) return "undefined";
2119
+ const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
2120
+ return map[prop.type] || prop.defaultValue;
2121
+ });
2122
+ Handlebars.registerHelper("propTestValue", (prop) => {
2123
+ const map = {
2124
+ String: "'test-value'",
2125
+ Number: "42",
2126
+ Boolean: "true",
2127
+ Array: "[1, 2, 3]",
2128
+ Object: '{ key: "value" }'
2129
+ };
2130
+ return map[prop.type] || "'test-value'";
2131
+ });
2132
+ function resolveImportPath(testType, targetPath) {
2133
+ if (!targetPath) return targetPath;
2134
+ const testDirMap = {
2135
+ unit: "tests/unit",
2136
+ component: "tests/component",
2137
+ e2e: "tests/e2e",
2138
+ api: "tests/api",
2139
+ visual: "tests/visual",
2140
+ performance: "tests/e2e"
2141
+ };
2142
+ const testDir = testDirMap[testType];
2143
+ if (!testDir) return targetPath;
2144
+ if (targetPath.startsWith("../") || targetPath.startsWith("./../")) {
2145
+ return targetPath;
2146
+ }
2147
+ const depth = testDir.split("/").length;
2148
+ const prefix = "../".repeat(depth);
2149
+ let cleanPath = targetPath.replace(/^\.\//, "");
2150
+ if (!cleanPath.endsWith(".vue")) {
2151
+ cleanPath = cleanPath.replace(/\.(ts|js|tsx|jsx)$/, "");
2152
+ }
2153
+ return `${prefix}${cleanPath}`;
2154
+ }
2155
+ function renderTemplate(type, context) {
2156
+ const templateContent = loadTemplate(type);
2157
+ const template = Handlebars.compile(templateContent);
2158
+ const resolvedTarget = resolveImportPath(type, context.target);
2159
+ const fullContext = {
2160
+ vueVersion: 3,
2161
+ typescript: true,
2162
+ imports: [],
2163
+ extraImports: [],
2164
+ globalPlugins: [],
2165
+ globalStubs: [],
2166
+ mountOptions: "",
2167
+ hasAnalysis: false,
2168
+ exports: [],
2169
+ functionExports: [],
2170
+ valueExports: [],
2171
+ props: [],
2172
+ emits: [],
2173
+ methods: [],
2174
+ computed: [],
2175
+ isVueComponent: false,
2176
+ requiredProps: [],
2177
+ optionalProps: [],
2178
+ ...context,
2179
+ // 使用计算后的正确路径
2180
+ target: resolvedTarget,
2181
+ framework: context.framework || "vue",
2182
+ camelName: context.camelName || toCamelCase(context.name),
2183
+ pascalName: context.pascalName || toPascalCase(context.name)
2184
+ };
2185
+ if (fullContext.exports && fullContext.exports.length > 0) {
2186
+ fullContext.hasAnalysis = true;
2187
+ fullContext.functionExports = fullContext.exports.filter(
2188
+ (e) => e.kind === "function" || e.kind === "default"
2189
+ );
2190
+ fullContext.valueExports = fullContext.exports.filter(
2191
+ (e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
2192
+ );
2193
+ }
2194
+ if (fullContext.props && fullContext.props.length > 0) {
2195
+ fullContext.isVueComponent = true;
2196
+ fullContext.requiredProps = fullContext.props.filter((p) => p.required);
2197
+ fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
2198
+ }
2199
+ if (fullContext.emits && fullContext.emits.length > 0) {
2200
+ fullContext.isVueComponent = true;
2201
+ }
2202
+ return template(fullContext);
2203
+ }
2204
+ function loadTemplate(type) {
2205
+ const custom = customTemplates.get(type);
2206
+ if (custom) return custom;
2207
+ const templateDir = getTemplateDir();
2208
+ const templateFile = TEMPLATE_MAP[type];
2209
+ const templatePath = path7.join(templateDir, templateFile);
2210
+ if (fs7.existsSync(templatePath)) {
2211
+ return fs7.readFileSync(templatePath, "utf-8");
2212
+ }
2213
+ return getBuiltinTemplate(type);
2214
+ }
2215
+ function getTemplateDir() {
2216
+ const projectTemplates = path7.join(process.cwd(), "templates");
2217
+ if (fs7.existsSync(projectTemplates)) {
2218
+ return projectTemplates;
2219
+ }
2220
+ return path7.join(__dirname, "..", "templates");
2221
+ }
2222
+ function getBuiltinTemplate(type) {
2223
+ const templates = {
2224
+ unit: `import { describe, it, expect } from 'vitest';
2225
+ {{#if hasAnalysis}}
2226
+ {{#each valueExports}}
2227
+ import { {{name}} } from '{{../target}}';
2228
+ {{/each}}
2229
+ {{#each functionExports}}
2230
+ import { {{name}} } from '{{../target}}';
2231
+ {{/each}}
2232
+ {{else}}
2233
+ import { {{camelName}} } from '{{target}}';
2234
+ {{/if}}
2235
+
2236
+ describe('{{name}}', () => {
1919
2237
  {{#if hasAnalysis}}
1920
2238
  {{#each functionExports}}
1921
2239
  describe('{{name}}()', () => {
@@ -2191,647 +2509,127 @@ test.describe('{{name}} performance', () => {
2191
2509
  return {
2192
2510
  domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
2193
2511
  loadComplete: entry?.loadEventEnd - entry?.loadEventStart ?? 0,
2194
- domInteractive: entry?.domInteractive ?? 0,
2195
- };
2196
- });
2197
-
2198
- // DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
2199
- expect(performanceMetrics.domInteractive).toBeLessThan(2000);
2200
- });
2201
-
2202
- test('should not have memory leaks', async ({ page }) => {
2203
- await page.goto('/');
2204
-
2205
- const metrics = await page.metrics();
2206
- expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
2207
- });
2208
- });
2209
- `
2210
- };
2211
- return templates[type];
2212
- }
2213
- function toCamelCase(str) {
2214
- return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
2215
- }
2216
- function toPascalCase(str) {
2217
- const camel = toCamelCase(str);
2218
- return camel.charAt(0).toUpperCase() + camel.slice(1);
2219
- }
2220
-
2221
- // src/services/global-config.ts
2222
- import fs6 from "fs";
2223
- import path6 from "path";
2224
- import os from "os";
2225
- var QAT_DIR = path6.join(os.homedir(), ".qat");
2226
- var AI_CONFIG_PATH = path6.join(QAT_DIR, "ai.json");
2227
- function loadGlobalAIConfig() {
2228
- if (!fs6.existsSync(AI_CONFIG_PATH)) {
2229
- return null;
2230
- }
2231
- try {
2232
- const content = fs6.readFileSync(AI_CONFIG_PATH, "utf-8");
2233
- const config = JSON.parse(content);
2234
- if (!config.baseUrl || !config.model) {
2235
- return null;
2236
- }
2237
- return config;
2238
- } catch {
2239
- return null;
2240
- }
2241
- }
2242
- function saveGlobalAIConfig(config) {
2243
- if (!fs6.existsSync(QAT_DIR)) {
2244
- fs6.mkdirSync(QAT_DIR, { recursive: true });
2245
- }
2246
- fs6.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
2247
- }
2248
- function toAIConfig(globalConfig) {
2249
- return {
2250
- provider: globalConfig.provider || "openai",
2251
- apiKey: globalConfig.apiKey || void 0,
2252
- baseUrl: globalConfig.baseUrl,
2253
- model: globalConfig.model
2254
- };
2255
- }
2256
- function maskApiKey(apiKey) {
2257
- if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
2258
- if (apiKey.length <= 8) return "****";
2259
- return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
2260
- }
2261
- function getAIConfigPath() {
2262
- return AI_CONFIG_PATH;
2263
- }
2512
+ domInteractive: entry?.domInteractive ?? 0,
2513
+ };
2514
+ });
2264
2515
 
2265
- // src/commands/init.ts
2266
- var TEST_TYPE_DIR = {
2267
- unit: "tests/unit",
2268
- component: "tests/component",
2269
- e2e: "tests/e2e",
2270
- api: "tests/api",
2271
- visual: "tests/visual",
2272
- performance: "tests/e2e"
2273
- };
2274
- function inferTestType(filePath) {
2275
- const normalized = filePath.replace(/\\/g, "/").toLowerCase();
2276
- if (/\/api\//.test(normalized) || /api\.(ts|js)$/.test(normalized)) {
2277
- return "api";
2278
- }
2279
- if (normalized.endsWith(".vue")) {
2280
- return "component";
2281
- }
2282
- if (/\/composables?\//.test(normalized) || /\/hooks?\//.test(normalized) || /^use[A-Z]/.test(path7.basename(filePath))) {
2283
- return "unit";
2284
- }
2285
- if (/\/(utils|helpers|services|lib)\//.test(normalized)) {
2286
- return "unit";
2287
- }
2288
- return "unit";
2289
- }
2290
- function registerInitCommand(program2) {
2291
- program2.command("init").description("\u521D\u59CB\u5316\u6D4B\u8BD5\u9879\u76EE - \u68C0\u6D4B\u9879\u76EE\u3001\u751F\u6210\u914D\u7F6E\u3001\u81EA\u52A8\u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B").option("-f, --force", "\u5F3A\u5236\u8986\u76D6\u5DF2\u6709\u914D\u7F6E\u6587\u4EF6").action(async (options) => {
2292
- try {
2293
- await executeInit(options);
2294
- } catch (error) {
2295
- console.error(chalk3.red(`
2296
- \u2717 ${error instanceof Error ? error.message : String(error)}
2297
- `));
2298
- process.exit(1);
2299
- }
2516
+ // DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
2517
+ expect(performanceMetrics.domInteractive).toBeLessThan(2000);
2300
2518
  });
2301
- }
2302
- async function executeInit(options) {
2303
- const spinner = ora2("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
2304
- const projectInfo = detectProject();
2305
- spinner.stop();
2306
- displayProjectInfo(projectInfo);
2307
- if (!projectInfo.isVue) {
2308
- const { proceed } = await inquirer.prompt([
2309
- {
2310
- type: "confirm",
2311
- name: "proceed",
2312
- message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
2313
- default: false
2314
- }
2315
- ]);
2316
- if (!proceed) {
2317
- console.log(chalk3.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
2318
- return;
2319
- }
2320
- }
2321
- let globalAI = loadGlobalAIConfig();
2322
- if (!globalAI) {
2323
- console.log(chalk3.cyan(" AI \u6A21\u578B\u914D\u7F6E (\u9996\u6B21\u4F7F\u7528\u9700\u914D\u7F6E\uFF0C\u4E4B\u540E\u53EF\u901A\u8FC7 qat change \u4FEE\u6539)\n"));
2324
- globalAI = await promptAIConfig();
2325
- saveGlobalAIConfig(globalAI);
2326
- console.log(chalk3.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
2327
- `));
2328
- } else {
2329
- console.log(chalk3.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk3.white(globalAI.model)} @ ${chalk3.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
2330
- `));
2331
- }
2332
- const aiConfig = toAIConfig(globalAI);
2333
- if (aiConfig.apiKey || aiConfig.baseUrl) {
2334
- const testSpinner = ora2(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
2335
- try {
2336
- const result = await testAIConnection(aiConfig);
2337
- if (result.ok) {
2338
- testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk3.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
2339
- } else {
2340
- testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
2341
- console.log(chalk3.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
2342
- }
2343
- } catch (error) {
2344
- testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
2345
- }
2346
- }
2347
- const config = buildProjectConfig(projectInfo);
2348
- let configPath;
2349
- const existingConfigPath = path7.join(process.cwd(), "qat.config.js");
2350
- const existingTsPath = path7.join(process.cwd(), "qat.config.ts");
2351
- const configExists = fs7.existsSync(existingConfigPath) || fs7.existsSync(existingTsPath);
2352
- if (configExists && !options.force) {
2353
- const { overwrite } = await inquirer.prompt([
2354
- {
2355
- type: "confirm",
2356
- name: "overwrite",
2357
- message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
2358
- default: true
2359
- }
2360
- ]);
2361
- if (!overwrite) {
2362
- console.log(chalk3.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
2363
- configPath = existingConfigPath;
2364
- } else {
2365
- const fileSpinner = ora2("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
2366
- try {
2367
- configPath = await writeConfigFile(process.cwd(), config, true);
2368
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
2369
- } catch (error) {
2370
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
2371
- throw error;
2372
- }
2373
- }
2374
- } else {
2375
- const fileSpinner = ora2("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
2376
- try {
2377
- configPath = await writeConfigFile(process.cwd(), config, options.force);
2378
- fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
2379
- } catch (error) {
2380
- fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
2381
- throw error;
2382
- }
2383
- }
2384
- const dirSpinner = ora2("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
2385
- const createdDirs = createTestDirectories(config);
2386
- dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
2387
- if (config.mock?.enabled !== false) {
2388
- const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
2389
- initMockRoutesDir(mockDir);
2390
- const srcDir = config.project?.srcDir || "src";
2391
- const apiCalls = scanAPICalls(srcDir);
2392
- if (apiCalls.length > 0) {
2393
- const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
2394
- const mockFilePath = path7.join(process.cwd(), mockDir, "auto-generated.json");
2395
- if (!fs7.existsSync(mockFilePath)) {
2396
- fs7.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
2397
- console.log(chalk3.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
2398
- }
2399
- } else {
2400
- console.log(chalk3.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
2401
- }
2402
- }
2403
- const useAI = isAIAvailable(aiConfig);
2404
- const generatedFiles = await autoGenerateTests(config, projectInfo, aiConfig, useAI);
2405
- displayResult(configPath, createdDirs, generatedFiles, projectInfo);
2406
- }
2407
- async function promptAIConfig() {
2408
- const answers = await inquirer.prompt([
2409
- {
2410
- type: "input",
2411
- name: "apiKey",
2412
- message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
2413
- default: ""
2414
- },
2415
- {
2416
- type: "input",
2417
- name: "baseUrl",
2418
- message: "API Base URL:",
2419
- default: "https://api.deepseek.com/v1",
2420
- validate: (input) => {
2421
- if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
2422
- if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
2423
- return true;
2424
- }
2425
- },
2426
- {
2427
- type: "input",
2428
- name: "model",
2429
- message: "\u6A21\u578B\u540D\u79F0:",
2430
- default: "deepseek-chat",
2431
- validate: (input) => {
2432
- if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
2433
- return true;
2434
- }
2435
- }
2436
- ]);
2437
- return {
2438
- provider: "openai",
2439
- apiKey: answers.apiKey?.trim() || "",
2440
- baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
2441
- model: answers.model?.trim() || "deepseek-chat"
2442
- };
2443
- }
2444
- function buildProjectConfig(projectInfo) {
2445
- return {
2446
- project: {
2447
- framework: projectInfo.framework,
2448
- uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
2449
- monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
2450
- vite: projectInfo.isVite,
2451
- srcDir: projectInfo.srcDir,
2452
- appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
2453
- },
2454
- vitest: {
2455
- enabled: true,
2456
- coverage: true,
2457
- globals: true,
2458
- environment: "happy-dom"
2459
- },
2460
- playwright: {
2461
- enabled: true,
2462
- browsers: ["chromium"],
2463
- baseURL: "http://localhost:5173",
2464
- screenshot: "only-on-failure"
2465
- },
2466
- visual: {
2467
- enabled: true,
2468
- threshold: 0.1,
2469
- baselineDir: "tests/visual/baseline",
2470
- diffDir: "tests/visual/diff"
2471
- },
2472
- lighthouse: {
2473
- enabled: true,
2474
- urls: ["http://localhost:5173"],
2475
- runs: 3,
2476
- thresholds: {
2477
- performance: 80,
2478
- accessibility: 90
2479
- }
2480
- },
2481
- mock: {
2482
- enabled: true,
2483
- port: 3456,
2484
- routesDir: "tests/mock/routes"
2485
- }
2519
+
2520
+ test('should not have memory leaks', async ({ page }) => {
2521
+ await page.goto('/');
2522
+
2523
+ const metrics = await page.metrics();
2524
+ expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
2525
+ });
2526
+ });
2527
+ `
2486
2528
  };
2529
+ return templates[type];
2487
2530
  }
2488
- async function autoGenerateTests(config, projectInfo, aiConfig, useAI) {
2489
- const srcDir = config.project?.srcDir || "src";
2490
- const components = discoverVueComponents(process.cwd(), srcDir);
2491
- const utilities = discoverUtilityFiles(process.cwd(), srcDir);
2492
- if (components.length === 0 && utilities.length === 0) {
2493
- console.log(chalk3.yellow("\n \u672A\u53D1\u73B0\u53EF\u6D4B\u8BD5\u7684\u6E90\u7801\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u6D4B\u8BD5\u7528\u4F8B\u751F\u6210\u3002"));
2494
- return [];
2495
- }
2496
- const allTargets = [];
2497
- for (const compPath of components.slice(0, 30)) {
2498
- allTargets.push({ filePath: compPath, testType: inferTestType(compPath) });
2499
- }
2500
- for (const utilPath of utilities.slice(0, 30)) {
2501
- allTargets.push({ filePath: utilPath, testType: inferTestType(utilPath) });
2502
- }
2503
- const selectedTargets = await selectTargetFiles(allTargets);
2504
- if (selectedTargets.length === 0) {
2505
- console.log(chalk3.gray("\n \u672A\u9009\u62E9\u4EFB\u4F55\u6587\u4EF6\uFF0C\u8DF3\u8FC7\u6D4B\u8BD5\u7528\u4F8B\u751F\u6210\u3002"));
2506
- return [];
2507
- }
2508
- const total = selectedTargets.length;
2509
- const genSpinner = ora2(`\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [0/${total}] ...`).start();
2510
- const generatedFiles = [];
2511
- const typeCount = {};
2512
- const reviewReport = [];
2513
- let current = 0;
2514
- let failed = 0;
2515
- for (const { filePath, testType } of selectedTargets) {
2516
- current++;
2517
- const fileLabel = path7.basename(filePath);
2518
- genSpinner.text = `\u6B63\u5728\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B [${current}/${total}] ${chalk3.cyan(fileLabel)} ...`;
2519
- try {
2520
- const result = await generateTestForTarget(
2521
- testType,
2522
- filePath,
2523
- config,
2524
- projectInfo,
2525
- aiConfig,
2526
- useAI
2527
- );
2528
- if (result) {
2529
- generatedFiles.push(result.filePath);
2530
- typeCount[testType] = (typeCount[testType] || 0) + 1;
2531
- if (result.reviewEntry) {
2532
- reviewReport.push(result.reviewEntry);
2533
- }
2534
- }
2535
- } catch {
2536
- failed++;
2537
- }
2538
- }
2539
- if (generatedFiles.length > 0) {
2540
- const summary = Object.entries(typeCount).map(([type, count]) => `${count} ${type}`).join(", ");
2541
- const approvedCount = reviewReport.filter((r) => r.approved).length;
2542
- let msg = `\u5DF2\u751F\u6210 ${generatedFiles.length}/${total} \u4E2A\u6D4B\u8BD5\u7528\u4F8B (${summary}) \u2014 AI \u5BA1\u8BA1 ${approvedCount}/${reviewReport.length} \u901A\u8FC7`;
2543
- if (failed > 0) msg += chalk3.yellow(` ${failed} \u4E2A\u5931\u8D25`);
2544
- genSpinner.succeed(msg);
2545
- } else {
2546
- genSpinner.warn(`\u672A\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B (${failed} \u4E2A\u5931\u8D25)`);
2547
- }
2548
- if (reviewReport.length > 0) {
2549
- printReviewReport(reviewReport);
2550
- }
2551
- return generatedFiles;
2552
- }
2553
- async function selectTargetFiles(allTargets) {
2554
- const testTypeLabels = {
2555
- unit: chalk3.blue("[unit]"),
2556
- component: chalk3.magenta("[comp]"),
2557
- e2e: chalk3.green("[e2e]"),
2558
- api: chalk3.yellow("[api]"),
2559
- visual: chalk3.cyan("[visual]"),
2560
- performance: chalk3.gray("[perf]")
2561
- };
2562
- const choices = allTargets.map(({ filePath, testType }) => ({
2563
- name: `${testTypeLabels[testType]} ${filePath}`,
2564
- value: filePath,
2565
- short: filePath
2566
- }));
2567
- choices.push({
2568
- name: chalk3.gray("\u270E \u624B\u52A8\u8F93\u5165\u6587\u4EF6/\u76EE\u5F55\u8DEF\u5F84"),
2569
- value: "__manual__",
2570
- short: "\u624B\u52A8\u8F93\u5165"
2571
- });
2572
- const { selected } = await inquirer.prompt([
2573
- {
2574
- type: "checkbox",
2575
- name: "selected",
2576
- message: "\u9009\u62E9\u8981\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B\u7684\u6587\u4EF6 (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
2577
- choices,
2578
- pageSize: 15
2579
- }
2580
- ]);
2581
- const manualPaths = [];
2582
- if (selected.includes("__manual__")) {
2583
- const { manualInput } = await inquirer.prompt([
2584
- {
2585
- type: "input",
2586
- name: "manualInput",
2587
- message: "\u8F93\u5165\u6587\u4EF6\u6216\u76EE\u5F55\u8DEF\u5F84\uFF08\u591A\u4E2A\u7528\u9017\u53F7\u5206\u9694\uFF09:",
2588
- default: "",
2589
- filter: (input) => input.split(",").map((s) => s.trim()).filter(Boolean)
2590
- }
2591
- ]);
2592
- for (const p of manualInput) {
2593
- const resolved = path7.resolve(process.cwd(), p);
2594
- if (fs7.existsSync(resolved)) {
2595
- const stat = fs7.statSync(resolved);
2596
- if (stat.isDirectory()) {
2597
- const dirFiles = walkDirForTestableFiles(resolved);
2598
- manualPaths.push(...dirFiles);
2599
- } else if (stat.isFile()) {
2600
- manualPaths.push(p.replace(/\\/g, "/"));
2601
- }
2602
- } else {
2603
- console.log(chalk3.yellow(` \u8DEF\u5F84\u4E0D\u5B58\u5728\uFF0C\u5DF2\u8DF3\u8FC7: ${p}`));
2604
- }
2605
- }
2606
- }
2607
- const selectedPaths = selected.filter((s) => s !== "__manual__");
2608
- const selectedSet = new Set(selectedPaths);
2609
- const result = allTargets.filter((t) => selectedSet.has(t.filePath));
2610
- const existingPaths = new Set(result.map((t) => t.filePath));
2611
- for (const mp of manualPaths) {
2612
- if (!existingPaths.has(mp)) {
2613
- result.push({ filePath: mp, testType: inferTestType(mp) });
2614
- }
2615
- }
2616
- return result;
2531
+ function toCamelCase(str) {
2532
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
2617
2533
  }
2618
- function walkDirForTestableFiles(dir) {
2619
- const files = [];
2620
- try {
2621
- const entries = fs7.readdirSync(dir, { withFileTypes: true });
2622
- for (const entry of entries) {
2623
- if (entry.name === "node_modules" || entry.name === "dist" || entry.name.startsWith(".")) continue;
2624
- const fullPath = path7.join(dir, entry.name);
2625
- if (entry.isDirectory()) {
2626
- files.push(...walkDirForTestableFiles(fullPath));
2627
- } else if (entry.isFile() && /\.(vue|ts|js)$/.test(entry.name)) {
2628
- files.push(path7.relative(process.cwd(), fullPath).replace(/\\/g, "/"));
2629
- }
2630
- }
2631
- } catch {
2632
- }
2633
- return files;
2534
+ function toPascalCase(str) {
2535
+ const camel = toCamelCase(str);
2536
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2634
2537
  }
2635
- async function generateTestForTarget(testType, targetPath, config, projectInfo, aiConfig, useAI) {
2636
- const basename = path7.basename(targetPath, path7.extname(targetPath));
2637
- const name = basename.replace(/[^a-zA-Z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "") || `${testType}-test`;
2638
- const outputDir = TEST_TYPE_DIR[testType];
2639
- const fileName = `${name}.${testType === "e2e" ? "spec" : "test"}.ts`;
2640
- const filePath = path7.join(process.cwd(), outputDir, fileName);
2641
- if (fs7.existsSync(filePath)) return null;
2642
- let content;
2643
- let reviewEntry;
2644
- if (useAI) {
2645
- const result = await generateWithAIAndReview(testType, name, targetPath, aiConfig, projectInfo);
2646
- content = result.code;
2647
- reviewEntry = result.reviewEntry;
2648
- } else {
2649
- const analysis = analyzeFile(targetPath);
2650
- content = renderTemplate(testType, {
2651
- name,
2538
+
2539
+ // src/services/test-reviewer.ts
2540
+ import chalk3 from "chalk";
2541
+ import ora2 from "ora";
2542
+ var MAX_RETRIES = 3;
2543
+ var REVIEW_THRESHOLD = 0.6;
2544
+ async function generateWithReview(params) {
2545
+ const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
2546
+ const generatorProvider = createAIProvider(aiConfig);
2547
+ const reviewerProvider = createAIProvider(aiConfig);
2548
+ if (!generatorProvider.capabilities.generateTest) {
2549
+ throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
2550
+ }
2551
+ let currentCode = "";
2552
+ let currentDescription = "";
2553
+ let currentConfidence = 0;
2554
+ let lastReview = null;
2555
+ let approved = false;
2556
+ let attempts = 0;
2557
+ for (let i = 0; i < MAX_RETRIES; i++) {
2558
+ attempts = i + 1;
2559
+ onAttempt?.(attempts, MAX_RETRIES);
2560
+ const generationPrompt = i === 0 ? void 0 : `\u4E0A\u6B21\u751F\u6210\u7684\u6D4B\u8BD5\u672A\u901A\u8FC7\u5BA1\u8BA1\uFF0C\u8BF7\u6839\u636E\u4EE5\u4E0B\u53CD\u9988\u91CD\u65B0\u751F\u6210\uFF1A
2561
+ \u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
2562
+ \u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
2563
+ \u5177\u4F53\u95EE\u9898:
2564
+ ${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
2565
+ \u6539\u8FDB\u5EFA\u8BAE:
2566
+ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
2567
+
2568
+ \u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
2569
+ const generateResponse = await generatorProvider.generateTest({
2570
+ type: testType,
2652
2571
  target: targetPath,
2653
- framework: projectInfo.framework,
2654
- vueVersion: projectInfo.vueVersion,
2655
- typescript: projectInfo.typescript,
2656
- uiLibrary: projectInfo.uiLibrary,
2657
- extraImports: projectInfo.componentTestSetup?.extraImports,
2658
- globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
2659
- globalStubs: projectInfo.componentTestSetup?.globalStubs,
2660
- mountOptions: projectInfo.componentTestSetup?.mountOptions,
2661
- exports: analysis.exports.map((e) => ({
2662
- name: e.name,
2663
- kind: e.kind,
2664
- params: e.params,
2665
- isAsync: e.isAsync,
2666
- returnType: e.returnType
2667
- })),
2668
- props: analysis.vueAnalysis?.props.map((p) => ({
2669
- name: p.name,
2670
- type: p.type,
2671
- required: p.required,
2672
- defaultValue: p.defaultValue
2673
- })),
2674
- emits: analysis.vueAnalysis?.emits.map((e) => ({
2675
- name: e.name,
2676
- params: e.params
2677
- })),
2678
- methods: analysis.vueAnalysis?.methods,
2679
- computed: analysis.vueAnalysis?.computed
2572
+ context: i === 0 ? sourceCode : `${sourceCode}
2573
+
2574
+ --- \u5BA1\u8BA1\u53CD\u9988 ---
2575
+ ${generationPrompt}`,
2576
+ framework: framework || void 0,
2577
+ analysis
2680
2578
  });
2579
+ currentCode = generateResponse.code;
2580
+ currentDescription = generateResponse.description;
2581
+ currentConfidence = generateResponse.confidence;
2582
+ const reviewRequest = {
2583
+ target: targetPath,
2584
+ sourceCode,
2585
+ analysis,
2586
+ testCode: currentCode,
2587
+ testType,
2588
+ generationDescription: currentDescription
2589
+ };
2590
+ lastReview = await reviewerProvider.reviewTest(reviewRequest);
2591
+ approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
2592
+ if (approved) {
2593
+ break;
2594
+ }
2681
2595
  }
2682
- if (!fs7.existsSync(path7.join(process.cwd(), outputDir))) {
2683
- fs7.mkdirSync(path7.join(process.cwd(), outputDir), { recursive: true });
2684
- }
2685
- fs7.writeFileSync(filePath, content, "utf-8");
2686
2596
  return {
2687
- filePath: path7.relative(process.cwd(), filePath).replace(/\\/g, "/"),
2688
- reviewEntry
2689
- };
2690
- }
2691
- async function generateWithAIAndReview(type, name, target, aiConfig, projectInfo) {
2692
- const fullPath = path7.resolve(process.cwd(), target);
2693
- let sourceCode = "";
2694
- if (fs7.existsSync(fullPath)) {
2695
- sourceCode = fs7.readFileSync(fullPath, "utf-8");
2696
- }
2697
- const analysis = analyzeFile(target);
2698
- const analysisSummary = analysis.exports.length > 0 || analysis.vueAnalysis ? {
2699
- exports: analysis.exports.map((e) => ({
2700
- name: e.name,
2701
- kind: e.kind,
2702
- params: e.params,
2703
- isAsync: e.isAsync
2704
- })),
2705
- props: analysis.vueAnalysis?.props.map((p) => ({
2706
- name: p.name,
2707
- type: p.type,
2708
- required: p.required
2709
- })),
2710
- emits: analysis.vueAnalysis?.emits.map((e) => ({
2711
- name: e.name,
2712
- params: e.params
2713
- })),
2714
- methods: analysis.vueAnalysis?.methods,
2715
- computed: analysis.vueAnalysis?.computed
2716
- } : void 0;
2717
- const reviewResult = await generateWithReview({
2718
- testType: type,
2719
- targetPath: target,
2720
- sourceCode,
2721
- analysis: analysisSummary,
2722
- aiConfig,
2723
- framework: "vue"
2724
- });
2725
- const headerComment = [
2726
- `// AI Generated Test - ${name}`,
2727
- `// \u5BA1\u8BA1: ${reviewResult.approved ? "\u901A\u8FC7" : "\u672A\u901A\u8FC7"} (${(reviewResult.reviewScore * 100).toFixed(0)}%) \u2014 ${reviewResult.attempts}\u6B21\u5C1D\u8BD5`,
2728
- reviewResult.reviewFeedback ? `// \u5BA1\u8BA1\u610F\u89C1: ${reviewResult.reviewFeedback}` : "",
2729
- ""
2730
- ].filter(Boolean).join("\n");
2731
- const code = `${headerComment}
2732
- ${reviewResult.code}`;
2733
- const reviewEntry = {
2734
- target,
2735
- testType: type,
2736
- approved: reviewResult.approved,
2737
- attempts: reviewResult.attempts,
2738
- score: reviewResult.reviewScore,
2739
- feedback: reviewResult.reviewFeedback,
2740
- issues: reviewResult.approved ? [] : reviewResult.reviewIssues
2597
+ code: currentCode,
2598
+ description: currentDescription,
2599
+ confidence: currentConfidence,
2600
+ approved,
2601
+ attempts,
2602
+ reviewScore: lastReview?.score ?? 0,
2603
+ reviewFeedback: lastReview?.feedback ?? "",
2604
+ reviewIssues: lastReview?.issues ?? [],
2605
+ reviewSuggestions: lastReview?.suggestions ?? []
2741
2606
  };
2742
- return { code, reviewEntry };
2743
2607
  }
2744
- function displayProjectInfo(info) {
2745
- console.log(chalk3.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
2608
+ function printReviewReport(report) {
2609
+ if (report.length === 0) return;
2610
+ const approved = report.filter((r) => r.approved);
2611
+ const failed = report.filter((r) => !r.approved);
2612
+ console.log();
2613
+ console.log(chalk3.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
2746
2614
  console.log(chalk3.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
2747
- const items = [
2748
- ["\u9879\u76EE\u540D\u79F0", info.name],
2749
- ["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
2750
- ["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
2751
- ["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
2752
- ["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
2753
- ["TypeScript", info.typescript ? "\u662F" : "\u5426"],
2754
- ["\u5305\u7BA1\u7406\u5668", info.packageManager],
2755
- ["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
2756
- ["\u6E90\u7801\u76EE\u5F55", info.srcDir]
2757
- ];
2758
- if (info.appDirs.length > 0) {
2759
- items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
2760
- }
2761
- if (info.testFrameworks.length > 0) {
2762
- items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
2763
- }
2764
- if (info.componentDirs.length > 0) {
2765
- items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
2766
- }
2767
- for (const [label, value] of items) {
2768
- const displayValue = value === true ? chalk3.green("\u2713") : value === false ? chalk3.red("\u2717") : String(value);
2769
- console.log(` ${chalk3.white(label.padEnd(12))} ${displayValue}`);
2615
+ for (const entry of approved) {
2616
+ console.log(` ${chalk3.green("\u2713")} ${chalk3.gray(entry.target)} ${chalk3.green(`(${(entry.score * 100).toFixed(0)}%`)}${entry.attempts > 1 ? chalk3.yellow(` ${entry.attempts}\u6B21\u5C1D\u8BD5`) : ""})`);
2770
2617
  }
2771
- console.log();
2772
- }
2773
- function createTestDirectories(config) {
2774
- const dirs = [];
2775
- const dirMap = {
2776
- "tests": true,
2777
- "tests/unit": config.vitest?.enabled !== false,
2778
- "tests/component": config.vitest?.enabled !== false,
2779
- "tests/e2e": config.playwright?.enabled !== false,
2780
- "tests/api": config.mock?.enabled !== false,
2781
- "tests/visual": config.visual?.enabled !== false,
2782
- "tests/visual/baseline": config.visual?.enabled !== false,
2783
- "tests/visual/diff": config.visual?.enabled !== false,
2784
- "tests/mock": config.mock?.enabled !== false,
2785
- "tests/mock/routes": config.mock?.enabled !== false
2786
- };
2787
- for (const [dir, shouldCreate] of Object.entries(dirMap)) {
2788
- if (shouldCreate) {
2789
- const fullPath = path7.join(process.cwd(), dir);
2790
- if (!fs7.existsSync(fullPath)) {
2791
- fs7.mkdirSync(fullPath, { recursive: true });
2792
- dirs.push(dir);
2618
+ for (const entry of failed) {
2619
+ console.log(` ${chalk3.red("\u2717")} ${chalk3.gray(entry.target)} ${chalk3.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
2620
+ if (entry.issues.length > 0) {
2621
+ for (const issue of entry.issues.slice(0, 3)) {
2622
+ console.log(chalk3.gray(` - ${issue}`));
2793
2623
  }
2794
2624
  }
2795
- }
2796
- return dirs;
2797
- }
2798
- function displayResult(configPath, createdDirs, generatedFiles, projectInfo) {
2799
- const relativeConfig = path7.relative(process.cwd(), configPath);
2800
- console.log(chalk3.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
2801
- console.log(chalk3.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
2802
- console.log(chalk3.gray(` ${relativeConfig}`));
2803
- console.log();
2804
- if (generatedFiles.length > 0) {
2805
- console.log(chalk3.white(" \u5DF2\u751F\u6210\u6D4B\u8BD5\u7528\u4F8B:"));
2806
- for (const file of generatedFiles) {
2807
- const typeLabel = file.includes("/unit/") ? chalk3.blue("[unit]") : file.includes("/component/") ? chalk3.magenta("[comp]") : file.includes("/api/") ? chalk3.yellow("[api]") : chalk3.gray("[other]");
2808
- console.log(` ${typeLabel} ${chalk3.gray(file)}`);
2625
+ if (entry.feedback) {
2626
+ console.log(chalk3.gray(` \u53CD\u9988: ${entry.feedback}`));
2809
2627
  }
2810
- console.log();
2811
- }
2812
- console.log(chalk3.cyan(" \u4E0B\u4E00\u6B65:"));
2813
- if (generatedFiles.length > 0) {
2814
- console.log(chalk3.gray(" 1. qat run \u6267\u884C\u6D4B\u8BD5"));
2815
- console.log(chalk3.gray(" 2. qat create \u6DFB\u52A0\u66F4\u591A\u6D4B\u8BD5\u7528\u4F8B"));
2816
- console.log(chalk3.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
2817
- } else {
2818
- console.log(chalk3.gray(" 1. qat change \u914D\u7F6E AI \u6A21\u578B"));
2819
- console.log(chalk3.gray(" 2. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
2820
- }
2821
- if (projectInfo.testFrameworks.length === 0) {
2822
- console.log();
2823
- console.log(chalk3.yellow(" \u26A0 \u672A\u68C0\u6D4B\u5230\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\uFF0C\u5EFA\u8BAE\u8FD0\u884C:"));
2824
- console.log(chalk3.gray(` qat setup`));
2825
2628
  }
2826
2629
  console.log();
2827
2630
  }
2828
2631
 
2829
2632
  // src/commands/create.ts
2830
- import chalk4 from "chalk";
2831
- import inquirer2 from "inquirer";
2832
- import ora3 from "ora";
2833
- import fs8 from "fs";
2834
- import path8 from "path";
2835
2633
  var TEST_TYPE_LABELS = {
2836
2634
  unit: "\u5355\u5143\u6D4B\u8BD5",
2837
2635
  component: "\u7EC4\u4EF6\u6D4B\u8BD5",
@@ -3169,18 +2967,29 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
3169
2967
  import chalk5 from "chalk";
3170
2968
  import inquirer3 from "inquirer";
3171
2969
  import ora4 from "ora";
3172
- import fs9 from "fs";
3173
- import path11 from "path";
2970
+ import fs11 from "fs";
2971
+ import path12 from "path";
3174
2972
 
3175
2973
  // src/runners/vitest-runner.ts
3176
2974
  import { execFile } from "child_process";
3177
2975
  import path9 from "path";
2976
+ import fs9 from "fs";
2977
+ import os2 from "os";
2978
+ var isVerbose = () => process.env.QAT_VERBOSE === "true";
2979
+ function debug(label, ...args) {
2980
+ if (isVerbose()) {
2981
+ console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
2982
+ }
2983
+ }
3178
2984
  async function runVitest(options) {
3179
2985
  const startTime = Date.now();
3180
2986
  const args = buildVitestArgs(options);
2987
+ debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
3181
2988
  try {
3182
2989
  const result = await execVitest(args);
3183
2990
  const endTime = Date.now();
2991
+ 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`);
2992
+ debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
3184
2993
  return {
3185
2994
  type: options.type,
3186
2995
  status: result.success ? "passed" : "failed",
@@ -3223,14 +3032,14 @@ function buildVitestArgs(options) {
3223
3032
  if (options.files && options.files.length > 0) {
3224
3033
  args.push(...options.files);
3225
3034
  } else {
3226
- const includeMap = {
3227
- unit: ["tests/unit"],
3228
- component: ["tests/component"],
3229
- api: ["tests/api"]
3035
+ const pathMap = {
3036
+ unit: "tests/unit/**/*.test.ts",
3037
+ component: "tests/component/**/*.test.ts",
3038
+ api: "tests/api/**/*.test.ts"
3230
3039
  };
3231
- const includes = includeMap[options.type];
3232
- if (includes) {
3233
- args.push("--include", includes.map((d) => `${d}/**/*.test.ts`).join(","));
3040
+ const testPattern = pathMap[options.type];
3041
+ if (testPattern) {
3042
+ args.push(testPattern);
3234
3043
  }
3235
3044
  }
3236
3045
  if (options.coverage) {
@@ -3245,12 +3054,11 @@ function buildVitestArgs(options) {
3245
3054
  return args;
3246
3055
  }
3247
3056
  async function execVitest(args) {
3248
- const os2 = await import("os");
3249
- const fs15 = await import("fs");
3250
3057
  const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
3251
3058
  const argsWithOutput = [...args, "--outputFile", tmpFile];
3252
3059
  return new Promise((resolve, reject) => {
3253
3060
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
3061
+ debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
3254
3062
  const child = execFile(npx, argsWithOutput, {
3255
3063
  cwd: process.cwd(),
3256
3064
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
@@ -3258,85 +3066,113 @@ async function execVitest(args) {
3258
3066
  shell: true
3259
3067
  }, (error, stdout, stderr) => {
3260
3068
  const rawOutput = stdout || stderr || "";
3069
+ const exitCode = error && "code" in error ? error.code : 0;
3070
+ debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
3071
+ debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
3261
3072
  let jsonResult = null;
3262
3073
  try {
3263
- if (fs15.existsSync(tmpFile)) {
3264
- jsonResult = fs15.readFileSync(tmpFile, "utf-8");
3265
- fs15.unlinkSync(tmpFile);
3074
+ if (fs9.existsSync(tmpFile)) {
3075
+ jsonResult = fs9.readFileSync(tmpFile, "utf-8");
3076
+ debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
3077
+ debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
3078
+ fs9.unlinkSync(tmpFile);
3079
+ } else {
3080
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
3266
3081
  }
3267
- } catch {
3082
+ } catch (e) {
3083
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3268
3084
  }
3269
3085
  if (jsonResult) {
3270
3086
  try {
3271
- const parsed = parseVitestJSONResult(jsonResult);
3272
- resolve({ ...parsed, rawOutput });
3087
+ const parsed = parseVitestJSON(jsonResult);
3088
+ debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3089
+ resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
3273
3090
  return;
3274
- } catch {
3091
+ } catch (e) {
3092
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3275
3093
  }
3276
3094
  }
3095
+ debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
3277
3096
  try {
3278
- const parsed = parseVitestJSONOutput(rawOutput);
3279
- resolve({ ...parsed, rawOutput });
3280
- } catch {
3281
- if (rawOutput) {
3282
- resolve({ ...parseVitestTextOutput(rawOutput, !!error), rawOutput });
3283
- } else if (error && error.message.includes("ENOENT")) {
3284
- reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
3285
- } else {
3286
- resolve({ success: !error, suites: [], rawOutput });
3287
- }
3097
+ const parsed = parseFromStdout(rawOutput);
3098
+ debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3099
+ resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
3100
+ return;
3101
+ } catch (e) {
3102
+ debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3103
+ }
3104
+ debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
3105
+ if (rawOutput) {
3106
+ const parsed = parseVitestTextOutput(rawOutput, !!error);
3107
+ debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3108
+ resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
3109
+ return;
3110
+ }
3111
+ if (error && error.message.includes("ENOENT")) {
3112
+ reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
3113
+ return;
3288
3114
  }
3115
+ debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
3116
+ resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
3289
3117
  });
3290
3118
  child.on("error", (err) => {
3291
3119
  try {
3292
- fs15.unlinkSync(tmpFile);
3120
+ fs9.unlinkSync(tmpFile);
3293
3121
  } catch {
3294
3122
  }
3295
3123
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
3296
3124
  });
3297
3125
  });
3298
3126
  }
3299
- function parseVitestJSONResult(jsonStr) {
3127
+ function parseVitestJSON(jsonStr) {
3300
3128
  const data = JSON.parse(jsonStr);
3301
3129
  const suites = [];
3130
+ debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
3302
3131
  if (data.testResults && Array.isArray(data.testResults)) {
3132
+ debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
3303
3133
  for (const fileResult of data.testResults) {
3134
+ const suiteTests = parseTestResults(fileResult);
3135
+ suites.push({
3136
+ name: path9.basename(fileResult.name || "unknown"),
3137
+ file: fileResult.name || "unknown",
3138
+ type: "unit",
3139
+ status: mapVitestStatus(fileResult.status),
3140
+ duration: fileResult.duration || 0,
3141
+ tests: suiteTests
3142
+ });
3143
+ }
3144
+ }
3145
+ if (suites.length === 0 && data.numTotalTests !== void 0) {
3146
+ debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
3147
+ suites.push({
3148
+ name: "Vitest Results",
3149
+ file: "unknown",
3150
+ type: "unit",
3151
+ status: data.numFailedTests > 0 ? "failed" : "passed",
3152
+ duration: 0,
3153
+ tests: buildTestsFromSummary(data)
3154
+ });
3155
+ }
3156
+ if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
3157
+ debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
3158
+ for (const suiteData of data.suites) {
3304
3159
  const suiteTests = [];
3305
- const assertions = fileResult.assertionResults || fileResult.tests || [];
3306
- for (const assertion of assertions) {
3160
+ for (const test of suiteData.tests || []) {
3307
3161
  suiteTests.push({
3308
- name: assertion.title || assertion.fullName || assertion.name || "unknown",
3309
- file: fileResult.name || "unknown",
3310
- status: mapVitestStatus(assertion.status),
3311
- duration: assertion.duration || 0,
3312
- error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
3162
+ name: test.name || test.title || "unknown",
3163
+ file: test.file || suiteData.file || "unknown",
3164
+ status: mapVitestStatus(test.status || test.result?.status),
3165
+ duration: test.duration || test.result?.duration || 0,
3166
+ error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
3313
3167
  retries: 0
3314
3168
  });
3315
3169
  }
3316
- if (suiteTests.length === 0 && fileResult.numPassingTests !== void 0) {
3317
- const counts = [
3318
- { n: fileResult.numPassingTests || 0, s: "passed" },
3319
- { n: fileResult.numFailingTests || 0, s: "failed" },
3320
- { n: fileResult.numPendingTests || 0, s: "skipped" }
3321
- ];
3322
- for (const { n, s } of counts) {
3323
- for (let i = 0; i < n; i++) {
3324
- suiteTests.push({
3325
- name: `${s} test ${i + 1}`,
3326
- file: fileResult.name || "unknown",
3327
- status: s,
3328
- duration: 0,
3329
- retries: 0
3330
- });
3331
- }
3332
- }
3333
- }
3334
3170
  suites.push({
3335
- name: path9.basename(fileResult.name || "unknown"),
3336
- file: fileResult.name || "unknown",
3171
+ name: suiteData.name || "unknown",
3172
+ file: suiteData.file || "unknown",
3337
3173
  type: "unit",
3338
- status: mapVitestStatus(fileResult.status),
3339
- duration: fileResult.duration || 0,
3174
+ status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
3175
+ duration: 0,
3340
3176
  tests: suiteTests
3341
3177
  });
3342
3178
  }
@@ -3345,58 +3181,101 @@ function parseVitestJSONResult(jsonStr) {
3345
3181
  if (data.coverageMap) {
3346
3182
  coverage = extractCoverage(data.coverageMap);
3347
3183
  }
3348
- const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
3184
+ if (!coverage && data.coverage && typeof data.coverage === "object") {
3185
+ const cov = data.coverage;
3186
+ const totals = cov.totals || cov;
3187
+ const getVal = (key) => {
3188
+ const v = totals[key];
3189
+ return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
3190
+ };
3191
+ coverage = {
3192
+ lines: getVal("lines"),
3193
+ statements: getVal("statements"),
3194
+ functions: getVal("functions"),
3195
+ branches: getVal("branches")
3196
+ };
3197
+ }
3198
+ const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
3349
3199
  return { success, suites, coverage };
3350
3200
  }
3351
- function parseVitestJSONOutput(output) {
3352
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3353
- if (!jsonMatch) {
3354
- return parseVitestTextOutput(output, false);
3201
+ function parseTestResults(fileResult) {
3202
+ const tests = [];
3203
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
3204
+ for (const assertion of assertions) {
3205
+ tests.push({
3206
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
3207
+ file: fileResult.name || "unknown",
3208
+ status: mapVitestStatus(assertion.status),
3209
+ duration: assertion.duration || 0,
3210
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
3211
+ retries: 0
3212
+ });
3355
3213
  }
3356
- try {
3214
+ if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
3215
+ tests.push(...buildTestsFromSummary(fileResult));
3216
+ }
3217
+ return tests;
3218
+ }
3219
+ function buildTestsFromSummary(data) {
3220
+ const tests = [];
3221
+ const counts = [
3222
+ [data.numPassedTests || 0, "passed"],
3223
+ [data.numFailedTests || 0, "failed"],
3224
+ [data.numPendingTests || 0, "skipped"]
3225
+ ];
3226
+ for (const [n, s] of counts) {
3227
+ for (let i = 0; i < n; i++) {
3228
+ tests.push({
3229
+ name: `${s} test ${i + 1}`,
3230
+ file: data.name || "unknown",
3231
+ status: s,
3232
+ duration: 0,
3233
+ retries: 0
3234
+ });
3235
+ }
3236
+ }
3237
+ return tests;
3238
+ }
3239
+ function parseFromStdout(output) {
3240
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3241
+ if (jsonMatch) {
3357
3242
  const data = JSON.parse(jsonMatch[0]);
3358
3243
  const suites = [];
3359
3244
  if (data.testResults && Array.isArray(data.testResults)) {
3360
3245
  for (const fileResult of data.testResults) {
3361
- const suite = {
3362
- name: path9.basename(fileResult.name || fileResult.assertionResults?.[0]?.ancestorTitles?.[0] || "unknown"),
3246
+ suites.push({
3247
+ name: path9.basename(fileResult.name || "unknown"),
3363
3248
  file: fileResult.name || "unknown",
3364
3249
  type: "unit",
3365
3250
  status: mapVitestStatus(fileResult.status),
3366
3251
  duration: fileResult.duration || 0,
3367
- tests: (fileResult.assertionResults || []).map((assertion) => ({
3368
- name: assertion.title || assertion.fullName || "unknown",
3369
- file: fileResult.name || "unknown",
3370
- status: mapVitestStatus(assertion.status),
3371
- duration: assertion.duration || 0,
3372
- error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : void 0,
3373
- retries: 0
3374
- }))
3375
- };
3376
- suites.push(suite);
3252
+ tests: parseTestResults(fileResult)
3253
+ });
3377
3254
  }
3378
3255
  }
3379
- let coverage;
3380
- if (data.coverageMap) {
3381
- coverage = extractCoverage(data.coverageMap);
3382
- }
3383
3256
  const success = data.success !== false && suites.every((s) => s.status !== "failed");
3257
+ let coverage;
3258
+ if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
3384
3259
  return { success, suites, coverage };
3385
- } catch {
3386
- return parseVitestTextOutput(output, false);
3387
3260
  }
3261
+ const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
3262
+ if (anyJsonMatch) {
3263
+ return parseVitestJSON(anyJsonMatch[0]);
3264
+ }
3265
+ throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
3388
3266
  }
3389
3267
  function parseVitestTextOutput(output, hasError) {
3390
3268
  const suites = [];
3269
+ debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
3270
+ const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
3271
+ const lines = output.split("\n");
3391
3272
  let totalPassed = 0;
3392
3273
  let totalFailed = 0;
3393
- const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
3394
- const lines = output.split("\n");
3395
3274
  for (const line of lines) {
3396
3275
  const match = line.match(suiteRegex);
3397
3276
  if (match) {
3398
3277
  const file = match[1];
3399
- const testCount = parseInt(match[2], 10);
3278
+ const testCount = parseInt(match[4], 10);
3400
3279
  const isPassed = line.includes("\u2713");
3401
3280
  if (isPassed) totalPassed += testCount;
3402
3281
  else totalFailed += testCount;
@@ -3408,23 +3287,47 @@ function parseVitestTextOutput(output, hasError) {
3408
3287
  duration: 0,
3409
3288
  tests: Array.from({ length: testCount }, (_, i) => ({
3410
3289
  name: `test ${i + 1}`,
3411
- file,
3412
- status: isPassed ? "passed" : "failed",
3290
+ file,
3291
+ status: isPassed ? "passed" : "failed",
3292
+ duration: 0,
3293
+ retries: 0
3294
+ }))
3295
+ });
3296
+ }
3297
+ }
3298
+ if (suites.length === 0) {
3299
+ const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
3300
+ if (summaryMatch) {
3301
+ const count = parseInt(summaryMatch[1], 10);
3302
+ const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
3303
+ suites.push({
3304
+ name: "Vitest Summary",
3305
+ file: "unknown",
3306
+ type: "unit",
3307
+ status,
3308
+ duration: 0,
3309
+ tests: Array.from({ length: count }, (_, i) => ({
3310
+ name: `test ${i + 1}`,
3311
+ file: "unknown",
3312
+ status,
3413
3313
  duration: 0,
3414
3314
  retries: 0
3415
3315
  }))
3416
3316
  });
3417
3317
  }
3418
3318
  }
3319
+ debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
3419
3320
  return {
3420
3321
  success: !hasError || totalFailed === 0,
3421
3322
  suites
3422
3323
  };
3423
3324
  }
3424
3325
  function mapVitestStatus(status) {
3326
+ if (!status) return "pending";
3425
3327
  switch (status) {
3426
3328
  case "passed":
3427
3329
  case "pass":
3330
+ case "done":
3428
3331
  return "passed";
3429
3332
  case "failed":
3430
3333
  case "fail":
@@ -3432,6 +3335,7 @@ function mapVitestStatus(status) {
3432
3335
  case "skipped":
3433
3336
  case "skip":
3434
3337
  case "pending":
3338
+ case "todo":
3435
3339
  return "skipped";
3436
3340
  default:
3437
3341
  return "pending";
@@ -3910,9 +3814,226 @@ function calculateAverageMetrics(results) {
3910
3814
  };
3911
3815
  }
3912
3816
 
3817
+ // src/services/reporter.ts
3818
+ import fs10 from "fs";
3819
+ import path11 from "path";
3820
+ function aggregateResults(results) {
3821
+ const summary = {
3822
+ total: 0,
3823
+ passed: 0,
3824
+ failed: 0,
3825
+ skipped: 0,
3826
+ pending: 0
3827
+ };
3828
+ const byType = {};
3829
+ let coverage;
3830
+ for (const result of results) {
3831
+ const typeKey = result.type;
3832
+ if (!byType[typeKey]) {
3833
+ byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
3834
+ }
3835
+ for (const suite of result.suites) {
3836
+ for (const test of suite.tests) {
3837
+ summary.total++;
3838
+ byType[typeKey].total++;
3839
+ if (test.status === "passed") {
3840
+ summary.passed++;
3841
+ byType[typeKey].passed++;
3842
+ } else if (test.status === "failed") {
3843
+ summary.failed++;
3844
+ byType[typeKey].failed++;
3845
+ } else if (test.status === "skipped") {
3846
+ summary.skipped++;
3847
+ byType[typeKey].skipped++;
3848
+ } else {
3849
+ summary.pending++;
3850
+ }
3851
+ }
3852
+ }
3853
+ if (result.coverage) {
3854
+ if (!coverage) {
3855
+ coverage = { ...result.coverage };
3856
+ } else {
3857
+ coverage.lines = Math.max(coverage.lines, result.coverage.lines);
3858
+ coverage.statements = Math.max(coverage.statements, result.coverage.statements);
3859
+ coverage.functions = Math.max(coverage.functions, result.coverage.functions);
3860
+ coverage.branches = Math.max(coverage.branches, result.coverage.branches);
3861
+ }
3862
+ }
3863
+ }
3864
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
3865
+ return {
3866
+ timestamp: Date.now(),
3867
+ duration: totalDuration,
3868
+ results,
3869
+ summary,
3870
+ byType,
3871
+ coverage: coverage?.lines ? coverage : void 0
3872
+ };
3873
+ }
3874
+ function formatDuration(ms) {
3875
+ if (ms < 1e3) return `${ms}ms`;
3876
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
3877
+ const minutes = Math.floor(ms / 6e4);
3878
+ const seconds = (ms % 6e4 / 1e3).toFixed(0);
3879
+ return `${minutes}m ${seconds}s`;
3880
+ }
3881
+ function formatTimestamp(ts) {
3882
+ return new Date(ts).toLocaleString("zh-CN", {
3883
+ year: "numeric",
3884
+ month: "2-digit",
3885
+ day: "2-digit",
3886
+ hour: "2-digit",
3887
+ minute: "2-digit",
3888
+ second: "2-digit"
3889
+ });
3890
+ }
3891
+ function pct(value) {
3892
+ return `${(value * 100).toFixed(1)}%`;
3893
+ }
3894
+ function renderCoverageMD(coverage) {
3895
+ return `
3896
+ ### \u8986\u76D6\u7387
3897
+
3898
+ | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
3899
+ |------|--------|------|
3900
+ | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
3901
+ | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
3902
+ | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
3903
+ | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
3904
+ `;
3905
+ }
3906
+ function renderProgressBar(value) {
3907
+ const filled = Math.round(value * 10);
3908
+ const empty = 10 - filled;
3909
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
3910
+ }
3911
+ var TYPE_LABELS = {
3912
+ unit: "\u5355\u5143\u6D4B\u8BD5",
3913
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5",
3914
+ e2e: "E2E \u6D4B\u8BD5",
3915
+ api: "API \u6D4B\u8BD5",
3916
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
3917
+ performance: "\u6027\u80FD\u6D4B\u8BD5"
3918
+ };
3919
+ function generateMDReport(data) {
3920
+ const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
3921
+ const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
3922
+ const lines = [];
3923
+ lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
3924
+ lines.push("");
3925
+ lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
3926
+ lines.push("");
3927
+ lines.push(`## \u603B\u89C8`);
3928
+ lines.push("");
3929
+ lines.push(`| \u6307\u6807 | \u6570\u503C |`);
3930
+ lines.push(`|------|------|`);
3931
+ lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
3932
+ lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
3933
+ lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
3934
+ if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
3935
+ if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
3936
+ if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
3937
+ lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
3938
+ lines.push("");
3939
+ if (Object.keys(data.byType).length > 0) {
3940
+ lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
3941
+ lines.push("");
3942
+ lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
3943
+ lines.push(`|------|------|------|------|------|--------|`);
3944
+ for (const [type, stats] of Object.entries(data.byType)) {
3945
+ const label = TYPE_LABELS[type] || type;
3946
+ const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
3947
+ lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
3948
+ }
3949
+ lines.push("");
3950
+ }
3951
+ if (data.coverage) {
3952
+ lines.push(renderCoverageMD(data.coverage));
3953
+ lines.push("");
3954
+ }
3955
+ lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
3956
+ lines.push("");
3957
+ for (const result of data.results) {
3958
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
3959
+ const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
3960
+ lines.push(`### ${statusIcon} ${typeLabel}`);
3961
+ lines.push("");
3962
+ if (result.suites.length === 0) {
3963
+ lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
3964
+ lines.push("");
3965
+ continue;
3966
+ }
3967
+ for (const suite of result.suites) {
3968
+ const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
3969
+ lines.push(`#### ${suiteIcon} ${suite.name}`);
3970
+ lines.push("");
3971
+ lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
3972
+ lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
3973
+ lines.push("");
3974
+ if (suite.tests.length > 0) {
3975
+ lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
3976
+ lines.push(`|------|----------|------|`);
3977
+ for (const test of suite.tests) {
3978
+ const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
3979
+ const name = test.error ? `**${test.name}**` : test.name;
3980
+ lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
3981
+ }
3982
+ lines.push("");
3983
+ }
3984
+ const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
3985
+ if (failedTests.length > 0) {
3986
+ lines.push(`<details>`);
3987
+ lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
3988
+ lines.push("");
3989
+ for (const test of failedTests) {
3990
+ lines.push(`**${test.name}**`);
3991
+ lines.push("```");
3992
+ lines.push(test.error.message);
3993
+ if (test.error.stack) {
3994
+ lines.push(test.error.stack);
3995
+ }
3996
+ if (test.error.expected && test.error.actual) {
3997
+ lines.push(`Expected: ${test.error.expected}`);
3998
+ lines.push(`Actual: ${test.error.actual}`);
3999
+ }
4000
+ lines.push("```");
4001
+ lines.push("");
4002
+ }
4003
+ lines.push(`</details>`);
4004
+ lines.push("");
4005
+ }
4006
+ }
4007
+ }
4008
+ lines.push("---");
4009
+ lines.push("");
4010
+ lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
4011
+ return lines.join("\n");
4012
+ }
4013
+ function writeReportToDisk(data, outputDir) {
4014
+ const md = generateMDReport(data);
4015
+ const dir = path11.resolve(outputDir);
4016
+ if (!fs10.existsSync(dir)) {
4017
+ fs10.mkdirSync(dir, { recursive: true });
4018
+ }
4019
+ const mdPath = path11.join(dir, "report.md");
4020
+ fs10.writeFileSync(mdPath, md, "utf-8");
4021
+ const jsonPath = path11.join(dir, "report.json");
4022
+ fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
4023
+ return mdPath;
4024
+ }
4025
+
3913
4026
  // src/commands/run.ts
3914
4027
  var RESULTS_DIR = ".qat-results";
3915
4028
  var SERVER_REQUIRED_TYPES = ["e2e", "visual", "performance"];
4029
+ var TYPE_DEPENDENCIES = {
4030
+ unit: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
4031
+ component: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest @vue/test-utils happy-dom" },
4032
+ api: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
4033
+ e2e: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
4034
+ visual: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
4035
+ performance: { pkg: "lighthouse", runner: "Lighthouse", installCmd: "npm install -D lighthouse" }
4036
+ };
3916
4037
  var TYPE_RUNNERS = {
3917
4038
  unit: "Vitest",
3918
4039
  component: "Vitest",
@@ -3921,7 +4042,7 @@ var TYPE_RUNNERS = {
3921
4042
  visual: "Playwright",
3922
4043
  performance: "Lighthouse"
3923
4044
  };
3924
- var TYPE_LABELS = {
4045
+ var TYPE_LABELS2 = {
3925
4046
  unit: "\u5355\u5143\u6D4B\u8BD5",
3926
4047
  component: "\u7EC4\u4EF6\u6D4B\u8BD5",
3927
4048
  e2e: "E2E\u6D4B\u8BD5",
@@ -3942,7 +4063,7 @@ function registerRunCommand(program2) {
3942
4063
  });
3943
4064
  }
3944
4065
  async function executeRun(options) {
3945
- const { type, file, tag, watch, parallel, browser } = options;
4066
+ const { type, file, watch, parallel } = options;
3946
4067
  const config = await loadConfig(options.config);
3947
4068
  const globalAI = loadGlobalAIConfig();
3948
4069
  if (globalAI) {
@@ -3957,6 +4078,61 @@ async function executeRun(options) {
3957
4078
  console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u8BF7\u5728 qat.config.ts \u4E2D\u542F\u7528\uFF09\n"));
3958
4079
  return;
3959
4080
  }
4081
+ const missingDeps = checkTestDependencies(typesToRun);
4082
+ if (missingDeps.length > 0) {
4083
+ console.log(chalk5.yellow("\n \u26A0 \u4EE5\u4E0B\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\u672A\u5B89\u88C5:\n"));
4084
+ for (const dep of missingDeps) {
4085
+ console.log(chalk5.white(` ${dep.runner} (${dep.pkg})`));
4086
+ console.log(chalk5.gray(` \u5B89\u88C5\u547D\u4EE4: ${chalk5.cyan(dep.installCmd)}`));
4087
+ }
4088
+ console.log();
4089
+ const { action } = await inquirer3.prompt([
4090
+ {
4091
+ type: "list",
4092
+ name: "action",
4093
+ message: "\u5982\u4F55\u5904\u7406\uFF1F",
4094
+ choices: [
4095
+ { name: "\u81EA\u52A8\u5B89\u88C5\u7F3A\u5931\u4F9D\u8D56", value: "install" },
4096
+ { name: "\u8DF3\u8FC7\u672A\u5B89\u88C5\u7684\u6D4B\u8BD5\u7C7B\u578B", value: "skip" },
4097
+ { name: "\u53D6\u6D88\u8FD0\u884C", value: "cancel" }
4098
+ ],
4099
+ default: "install"
4100
+ }
4101
+ ]);
4102
+ if (action === "cancel") {
4103
+ console.log(chalk5.gray("\n \u5DF2\u53D6\u6D88\u8FD0\u884C\n"));
4104
+ return;
4105
+ }
4106
+ if (action === "install") {
4107
+ const installed = await installTestDependencies(missingDeps);
4108
+ if (!installed) {
4109
+ console.log(chalk5.yellow(" \u90E8\u5206\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF0C\u5C06\u8DF3\u8FC7\u5BF9\u5E94\u7684\u6D4B\u8BD5\u7C7B\u578B"));
4110
+ }
4111
+ const stillMissing = checkTestDependencies(typesToRun);
4112
+ if (stillMissing.length > 0) {
4113
+ const missingRunners = new Set(stillMissing.map((d) => d.pkg));
4114
+ typesToRun = typesToRun.filter((t) => {
4115
+ const dep = TYPE_DEPENDENCIES[t];
4116
+ return !dep || !missingRunners.has(dep.pkg);
4117
+ });
4118
+ if (typesToRun.length === 0) {
4119
+ console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\uFF09\n"));
4120
+ return;
4121
+ }
4122
+ }
4123
+ }
4124
+ if (action === "skip") {
4125
+ const missingRunners = new Set(missingDeps.map((d) => d.pkg));
4126
+ typesToRun = typesToRun.filter((t) => {
4127
+ const dep = TYPE_DEPENDENCIES[t];
4128
+ return !dep || !missingRunners.has(dep.pkg);
4129
+ });
4130
+ if (typesToRun.length === 0) {
4131
+ console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\n"));
4132
+ return;
4133
+ }
4134
+ }
4135
+ }
3960
4136
  const serverNeededTypes = typesToRun.filter((t) => SERVER_REQUIRED_TYPES.includes(t));
3961
4137
  if (serverNeededTypes.length > 0) {
3962
4138
  const serverOk = await checkDevServer(config);
@@ -3965,7 +4141,7 @@ async function executeRun(options) {
3965
4141
  {
3966
4142
  type: "list",
3967
4143
  name: "action",
3968
- message: `\u8FD0\u884C ${serverNeededTypes.map((t) => TYPE_LABELS[t]).join("\u3001")} \u9700\u8981\u9879\u76EE\u670D\u52A1\u8FD0\u884C\uFF0C\u5982\u4F55\u5904\u7406\uFF1F`,
4144
+ message: `\u8FD0\u884C ${serverNeededTypes.map((t) => TYPE_LABELS2[t]).join("\u3001")} \u9700\u8981\u9879\u76EE\u670D\u52A1\u8FD0\u884C\uFF0C\u5982\u4F55\u5904\u7406\uFF1F`,
3969
4145
  choices: [
3970
4146
  { name: "\u81EA\u52A8\u542F\u52A8 dev server \u5E76\u8FD0\u884C", value: "start" },
3971
4147
  { name: "\u8DF3\u8FC7\u8FD9\u4E9B\u6D4B\u8BD5\u7C7B\u578B\uFF0C\u4EC5\u8FD0\u884C\u5176\u4ED6\u6D4B\u8BD5", value: "skip" },
@@ -4046,7 +4222,7 @@ async function executeRun(options) {
4046
4222
  spinner.stop();
4047
4223
  } else {
4048
4224
  for (const testType of typesToRun) {
4049
- const label = TYPE_LABELS[testType] || testType;
4225
+ const label = TYPE_LABELS2[testType] || testType;
4050
4226
  const spinner = ora4(`\u6B63\u5728\u8FD0\u884C ${label}...`).start();
4051
4227
  try {
4052
4228
  const result = await runTestType(testType, config, options);
@@ -4058,14 +4234,25 @@ async function executeRun(options) {
4058
4234
  }
4059
4235
  }
4060
4236
  }
4061
- displaySummary(results, config);
4237
+ displayJestStyleResults(results);
4062
4238
  saveRunResults(results);
4239
+ const reportData = aggregateResults(results);
4240
+ const outputDir = config.report.outputDir || "qat-report";
4241
+ const reportPath = writeReportToDisk(reportData, outputDir);
4242
+ const relativePath = path12.relative(process.cwd(), reportPath);
4243
+ console.log(chalk5.gray(`
4244
+ \u62A5\u544A\u5DF2\u751F\u6210: ${chalk5.cyan(relativePath)}`));
4245
+ console.log();
4246
+ const hasFailures = results.some((r) => r.status === "failed");
4247
+ if (hasFailures && isAIAvailable(config.ai)) {
4248
+ await aiAnalyzeFailures(results, config.ai);
4249
+ }
4063
4250
  }
4064
4251
  function printDryRunCommands(types, options, config) {
4065
4252
  console.log();
4066
4253
  console.log(chalk5.cyan(" \u53EF\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4:\n"));
4067
4254
  for (const testType of types) {
4068
- const label = TYPE_LABELS[testType];
4255
+ const label = TYPE_LABELS2[testType];
4069
4256
  const runner = TYPE_RUNNERS[testType];
4070
4257
  let cmd;
4071
4258
  switch (runner) {
@@ -4078,7 +4265,7 @@ function printDryRunCommands(types, options, config) {
4078
4265
  api: "tests/api"
4079
4266
  };
4080
4267
  const dir = dirMap[testType];
4081
- if (dir) cmd += ` --include '${dir}/**/*.test.ts'`;
4268
+ if (dir) cmd += ` '${dir}/**/*.test.ts'`;
4082
4269
  if (config.vitest.coverage) cmd += " --coverage";
4083
4270
  }
4084
4271
  break;
@@ -4133,7 +4320,7 @@ async function determineTypesToRun(type, file, config) {
4133
4320
  name: "selectedTypes",
4134
4321
  message: "\u9009\u62E9\u8981\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
4135
4322
  choices: enabledTypes.map((t) => ({
4136
- name: `${TYPE_LABELS[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
4323
+ name: `${TYPE_LABELS2[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
4137
4324
  value: t,
4138
4325
  checked: true
4139
4326
  })),
@@ -4192,7 +4379,7 @@ async function runTestType(testType, config, options) {
4192
4379
  throw new Error(`\u672A\u77E5\u7684\u8FD0\u884C\u5668: ${runner}`);
4193
4380
  }
4194
4381
  }
4195
- async function runWatchMode(config, options) {
4382
+ async function runWatchMode(_config, options) {
4196
4383
  console.log(chalk5.cyan(" \u76D1\u542C\u6A21\u5F0F\u5DF2\u542F\u52A8 (Ctrl+C \u9000\u51FA)\n"));
4197
4384
  const { spawn } = await import("child_process");
4198
4385
  const args = ["vitest", "--watch"];
@@ -4208,7 +4395,7 @@ async function runWatchMode(config, options) {
4208
4395
  };
4209
4396
  const dirs = types.map((t) => dirMap[t]).filter(Boolean);
4210
4397
  if (dirs.length > 0) {
4211
- args.push("--include", dirs.map((d) => `${d}/**/*.test.ts`).join(","));
4398
+ args.push(...dirs.map((d) => `${d}/**/*.test.ts`));
4212
4399
  }
4213
4400
  }
4214
4401
  const child = spawn("npx", args, {
@@ -4267,53 +4454,82 @@ function suiteStatusToSpinner(status) {
4267
4454
  return "warn";
4268
4455
  }
4269
4456
  }
4270
- async function displaySummary(results, config) {
4271
- let totalSuites = 0;
4457
+ function displayJestStyleResults(results) {
4458
+ console.log();
4459
+ for (const result of results) {
4460
+ const typeLabel = TYPE_LABELS2[result.type] || result.type;
4461
+ if (result.suites.length === 0) {
4462
+ console.log(chalk5.gray(` ${typeLabel}: \u65E0\u6D4B\u8BD5\u7ED3\u679C`));
4463
+ continue;
4464
+ }
4465
+ for (const suite of result.suites) {
4466
+ if (suite.tests.length === 0) continue;
4467
+ const suiteIcon = suite.status === "passed" ? chalk5.green("PASS") : chalk5.red("FAIL");
4468
+ console.log(` ${suiteIcon} ${chalk5.white(suite.name)} ${chalk5.gray(`(${formatDuration2(suite.duration)})`)}`);
4469
+ for (const test of suite.tests) {
4470
+ const icon = test.status === "passed" ? chalk5.green(" \u2713") : test.status === "failed" ? chalk5.red(" \u2715") : chalk5.yellow(" \u25CB");
4471
+ const name = test.status === "failed" ? chalk5.red(test.name) : test.name;
4472
+ console.log(` ${icon} ${name} ${chalk5.gray(formatDuration2(test.duration))}`);
4473
+ }
4474
+ }
4475
+ console.log();
4476
+ }
4272
4477
  let totalPassed = 0;
4273
4478
  let totalFailed = 0;
4274
4479
  let totalSkipped = 0;
4275
4480
  let totalDuration = 0;
4481
+ const typeStats = {};
4276
4482
  for (const result of results) {
4277
4483
  totalDuration += result.duration;
4484
+ if (!typeStats[result.type]) {
4485
+ typeStats[result.type] = { passed: 0, failed: 0, skipped: 0, duration: 0 };
4486
+ }
4487
+ typeStats[result.type].duration += result.duration;
4278
4488
  for (const suite of result.suites) {
4279
- totalSuites++;
4280
4489
  for (const test of suite.tests) {
4281
- if (test.status === "passed") totalPassed++;
4282
- else if (test.status === "failed") totalFailed++;
4283
- else if (test.status === "skipped") totalSkipped++;
4490
+ if (test.status === "passed") {
4491
+ totalPassed++;
4492
+ typeStats[result.type].passed++;
4493
+ } else if (test.status === "failed") {
4494
+ totalFailed++;
4495
+ typeStats[result.type].failed++;
4496
+ } else if (test.status === "skipped") {
4497
+ totalSkipped++;
4498
+ typeStats[result.type].skipped++;
4499
+ }
4284
4500
  }
4285
- }
4286
- }
4287
- const total = totalPassed + totalFailed + totalSkipped;
4288
- console.log();
4289
- console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4290
- if (totalFailed > 0) {
4291
- console.log(chalk5.red(" \u2717 \u6D4B\u8BD5\u5931\u8D25"));
4292
- } else if (total === 0) {
4293
- console.log(chalk5.yellow(" \u26A0 \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
4294
- } else {
4295
- console.log(chalk5.green(" \u2713 \u5168\u90E8\u901A\u8FC7"));
4501
+ }
4296
4502
  }
4503
+ const total = totalPassed + totalFailed + totalSkipped;
4504
+ console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4505
+ console.log(chalk5.white(" \u6D4B\u8BD5\u7ED3\u679C\u6C47\u603B"));
4506
+ console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4297
4507
  console.log();
4298
- console.log(` ${chalk5.white("\u6D4B\u8BD5\u7528\u4F8B:")} ${total} total`);
4299
- if (totalPassed > 0) console.log(` ${chalk5.green(" \u901A\u8FC7:")} ${totalPassed}`);
4300
- if (totalFailed > 0) console.log(` ${chalk5.red(" \u5931\u8D25:")} ${totalFailed}`);
4301
- if (totalSkipped > 0) console.log(` ${chalk5.yellow(" \u8DF3\u8FC7:")} ${totalSkipped}`);
4302
- console.log(` ${chalk5.gray(" \u5957\u4EF6:")} ${totalSuites}`);
4303
- console.log(` ${chalk5.gray(" \u8017\u65F6:")} ${formatDuration(totalDuration)}`);
4304
- if (results.length > 1) {
4305
- console.log();
4306
- for (const result of results) {
4307
- const label = TYPE_LABELS[result.type] || result.type;
4308
- const icon = result.status === "passed" ? chalk5.green("\u2713") : result.status === "failed" ? chalk5.red("\u2717") : chalk5.yellow("\u26A0");
4309
- const testCount = result.suites.reduce((sum, s) => sum + s.tests.length, 0);
4310
- console.log(` ${icon} ${label} (${testCount} tests, ${formatDuration(result.duration)})`);
4508
+ console.log(` ${chalk5.white("\u7C7B\u578B".padEnd(14))} ${chalk5.white("\u901A\u8FC7".padStart(6))} ${chalk5.white("\u5931\u8D25".padStart(6))} ${chalk5.white("\u8DF3\u8FC7".padStart(6))} ${chalk5.white("\u603B\u8BA1".padStart(6))} ${chalk5.white("\u901A\u8FC7\u7387".padStart(8))} ${chalk5.white("\u8017\u65F6".padStart(8))}`);
4509
+ console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
4510
+ for (const [type, stats] of Object.entries(typeStats)) {
4511
+ const label = (TYPE_LABELS2[type] || type).padEnd(14);
4512
+ const typeTotal = stats.passed + stats.failed + stats.skipped;
4513
+ const rate = typeTotal > 0 ? (stats.passed / typeTotal * 100).toFixed(0) + "%" : "-";
4514
+ const rateColored = typeTotal > 0 && stats.passed === typeTotal ? chalk5.green(rate.padStart(8)) : stats.failed > 0 ? chalk5.red(rate.padStart(8)) : rate.padStart(8);
4515
+ console.log(` ${label} ${String(stats.passed).padStart(6)} ${String(stats.failed).padStart(6)} ${String(stats.skipped).padStart(6)} ${String(typeTotal).padStart(6)} ${rateColored} ${formatDuration2(stats.duration).padStart(8)}`);
4516
+ }
4517
+ console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
4518
+ const totalRate = total > 0 ? (totalPassed / total * 100).toFixed(0) + "%" : "-";
4519
+ const totalRateColored = total > 0 && totalFailed === 0 ? chalk5.green(totalRate.padStart(8)) : totalFailed > 0 ? chalk5.red(totalRate.padStart(8)) : totalRate.padStart(8);
4520
+ console.log(` ${"\u603B\u8BA1".padEnd(14)} ${String(totalPassed).padStart(6)} ${String(totalFailed).padStart(6)} ${String(totalSkipped).padStart(6)} ${String(total).padStart(6)} ${totalRateColored} ${formatDuration2(totalDuration).padStart(8)}`);
4521
+ console.log();
4522
+ for (const result of results) {
4523
+ if (result.coverage) {
4524
+ const c = result.coverage;
4525
+ console.log(chalk5.cyan(" \u8986\u76D6\u7387:"));
4526
+ console.log(` \u8BED\u53E5: ${coverageColor(c.statements)} \u5206\u652F: ${coverageColor(c.branches)} \u51FD\u6570: ${coverageColor(c.functions)} \u884C: ${coverageColor(c.lines)}`);
4527
+ console.log();
4311
4528
  }
4312
4529
  }
4313
4530
  for (const result of results) {
4314
4531
  if (result.type === "performance" && result.performance) {
4315
4532
  const p = result.performance;
4316
- console.log();
4317
4533
  console.log(chalk5.cyan(" \u6027\u80FD\u6307\u6807:"));
4318
4534
  console.log(` Performance: ${scoreColor(p.performance)} ${p.performance}/100`);
4319
4535
  console.log(` Accessibility: ${scoreColor(p.accessibility)} ${p.accessibility}/100`);
@@ -4322,6 +4538,7 @@ async function displaySummary(results, config) {
4322
4538
  if (p.lcp !== void 0) console.log(` LCP: ${p.lcp.toFixed(0)}ms`);
4323
4539
  if (p.fid !== void 0) console.log(` FID: ${p.fid.toFixed(0)}ms`);
4324
4540
  if (p.cls !== void 0) console.log(` CLS: ${p.cls.toFixed(3)}`);
4541
+ console.log();
4325
4542
  }
4326
4543
  }
4327
4544
  const failedTests = results.flatMap(
@@ -4330,23 +4547,31 @@ async function displaySummary(results, config) {
4330
4547
  )
4331
4548
  );
4332
4549
  if (failedTests.length > 0) {
4333
- console.log();
4334
4550
  console.log(chalk5.red(" \u5931\u8D25\u8BE6\u60C5:"));
4335
4551
  for (const { suite, test } of failedTests) {
4336
- console.log(chalk5.red(` \u2717 ${suite.name} > ${test.name}`));
4552
+ console.log(chalk5.red(` \u2715 ${suite.name} > ${test.name}`));
4337
4553
  if (test.error?.message) {
4338
4554
  console.log(chalk5.gray(` ${test.error.message.split("\n")[0]}`));
4339
4555
  }
4340
4556
  }
4557
+ console.log();
4341
4558
  }
4342
- console.log(chalk5.cyan(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
4343
- console.log();
4344
4559
  if (totalFailed > 0) {
4560
+ console.log(chalk5.red(` Tests: ${totalFailed} failed, ${totalPassed} passed, ${total} total`));
4345
4561
  process.exitCode = 1;
4562
+ } else if (total === 0) {
4563
+ console.log(chalk5.yellow(" \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
4564
+ } else {
4565
+ console.log(chalk5.green(` Tests: ${totalPassed} passed, ${total} total`));
4346
4566
  }
4347
- if (totalFailed > 0 && isAIAvailable(config.ai)) {
4348
- await aiAnalyzeFailures(results, config.ai);
4349
- }
4567
+ console.log(chalk5.gray(` Time: ${formatDuration2(totalDuration)}`));
4568
+ console.log();
4569
+ }
4570
+ function coverageColor(value) {
4571
+ const pct3 = `${(value * 100).toFixed(1)}%`;
4572
+ if (value >= 0.8) return chalk5.green(pct3);
4573
+ if (value >= 0.5) return chalk5.yellow(pct3);
4574
+ return chalk5.red(pct3);
4350
4575
  }
4351
4576
  async function aiAnalyzeFailures(results, aiConfig) {
4352
4577
  const failedTests = results.flatMap(
@@ -4375,7 +4600,7 @@ async function aiAnalyzeFailures(results, aiConfig) {
4375
4600
  }
4376
4601
  }
4377
4602
  }
4378
- function formatDuration(ms) {
4603
+ function formatDuration2(ms) {
4379
4604
  if (ms < 1e3) return `${ms}ms`;
4380
4605
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
4381
4606
  const min = Math.floor(ms / 6e4);
@@ -4401,8 +4626,8 @@ async function checkDevServer(config) {
4401
4626
  }
4402
4627
  async function startDevServer(config) {
4403
4628
  const { spawn } = await import("child_process");
4404
- const hasPnpm = fs9.existsSync(path11.join(process.cwd(), "pnpm-lock.yaml"));
4405
- const hasYarn = fs9.existsSync(path11.join(process.cwd(), "yarn.lock"));
4629
+ const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4630
+ const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4406
4631
  const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4407
4632
  console.log(chalk5.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
4408
4633
  const child = spawn(pkgCmd, ["run", "dev"], {
@@ -4431,27 +4656,99 @@ async function startDevServer(config) {
4431
4656
  }
4432
4657
  function saveRunResults(results) {
4433
4658
  if (results.length === 0) return;
4434
- const resultsPath = path11.join(process.cwd(), RESULTS_DIR);
4435
- if (!fs9.existsSync(resultsPath)) {
4436
- fs9.mkdirSync(resultsPath, { recursive: true });
4659
+ const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
4660
+ if (!fs11.existsSync(resultsPath)) {
4661
+ fs11.mkdirSync(resultsPath, { recursive: true });
4437
4662
  }
4438
4663
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4439
4664
  const fileName = `result-${timestamp}.json`;
4440
- const filePath = path11.join(resultsPath, fileName);
4665
+ const filePath = path12.join(resultsPath, fileName);
4441
4666
  const data = {
4442
4667
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4443
4668
  results
4444
4669
  };
4445
- fs9.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4670
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4446
4671
  try {
4447
- const files = fs9.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4672
+ const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4448
4673
  while (files.length > 20) {
4449
4674
  const oldest = files.shift();
4450
- fs9.unlinkSync(path11.join(resultsPath, oldest));
4675
+ fs11.unlinkSync(path12.join(resultsPath, oldest));
4451
4676
  }
4452
4677
  } catch {
4453
4678
  }
4454
4679
  }
4680
+ function checkTestDependencies(types) {
4681
+ const pkgPath = path12.join(process.cwd(), "package.json");
4682
+ let allDeps = {};
4683
+ if (fs11.existsSync(pkgPath)) {
4684
+ try {
4685
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
4686
+ allDeps = {
4687
+ ...pkg.dependencies,
4688
+ ...pkg.devDependencies
4689
+ };
4690
+ } catch {
4691
+ }
4692
+ }
4693
+ const missingDeps = [];
4694
+ const checkedPkgs = /* @__PURE__ */ new Set();
4695
+ for (const testType of types) {
4696
+ const dep = TYPE_DEPENDENCIES[testType];
4697
+ if (dep && !checkedPkgs.has(dep.pkg)) {
4698
+ checkedPkgs.add(dep.pkg);
4699
+ if (!allDeps[dep.pkg]) {
4700
+ missingDeps.push(dep);
4701
+ }
4702
+ }
4703
+ }
4704
+ return missingDeps;
4705
+ }
4706
+ async function installTestDependencies(missingDeps) {
4707
+ const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4708
+ const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4709
+ const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4710
+ const { execFile: execFile5 } = await import("child_process");
4711
+ let allSuccess = true;
4712
+ for (const dep of missingDeps) {
4713
+ const spinner = ora4(`\u6B63\u5728\u5B89\u88C5 ${dep.pkg}...`).start();
4714
+ const isPlaywright = dep.pkg === "@playwright/test";
4715
+ const installArgs = hasPnpm ? ["add", "-D", dep.pkg] : hasYarn ? ["add", "-D", dep.pkg] : ["install", "-D", dep.pkg];
4716
+ try {
4717
+ await new Promise((resolve, reject) => {
4718
+ const cmd = pkgManager === "npm" ? "npm" : pkgManager;
4719
+ const child = execFile5(cmd, installArgs, {
4720
+ cwd: process.cwd(),
4721
+ shell: true,
4722
+ stdio: "pipe"
4723
+ }, (error) => {
4724
+ if (error) reject(error);
4725
+ else resolve();
4726
+ });
4727
+ child.on("error", reject);
4728
+ });
4729
+ if (isPlaywright) {
4730
+ spinner.text = `\u6B63\u5728\u5B89\u88C5 Playwright \u6D4F\u89C8\u5668...`;
4731
+ await new Promise((resolve, reject) => {
4732
+ const child = execFile5("npx", ["playwright", "install"], {
4733
+ cwd: process.cwd(),
4734
+ shell: true,
4735
+ stdio: "pipe"
4736
+ }, (error) => {
4737
+ if (error) reject(error);
4738
+ else resolve();
4739
+ });
4740
+ child.on("error", reject);
4741
+ });
4742
+ }
4743
+ spinner.succeed(`${dep.pkg} \u5B89\u88C5\u6210\u529F`);
4744
+ } catch (error) {
4745
+ spinner.fail(`${dep.pkg} \u5B89\u88C5\u5931\u8D25`);
4746
+ console.log(chalk5.gray(` \u53EF\u624B\u52A8\u5B89\u88C5: ${chalk5.cyan(dep.installCmd)}`));
4747
+ allSuccess = false;
4748
+ }
4749
+ }
4750
+ return allSuccess;
4751
+ }
4455
4752
 
4456
4753
  // src/commands/mock.ts
4457
4754
  import chalk6 from "chalk";
@@ -4575,219 +4872,8 @@ function showStatus() {
4575
4872
  // src/commands/report.ts
4576
4873
  import chalk7 from "chalk";
4577
4874
  import ora6 from "ora";
4578
- import fs11 from "fs";
4875
+ import fs12 from "fs";
4579
4876
  import path13 from "path";
4580
-
4581
- // src/services/reporter.ts
4582
- import fs10 from "fs";
4583
- import path12 from "path";
4584
- function aggregateResults(results) {
4585
- const summary = {
4586
- total: 0,
4587
- passed: 0,
4588
- failed: 0,
4589
- skipped: 0,
4590
- pending: 0
4591
- };
4592
- const byType = {};
4593
- let coverage;
4594
- for (const result of results) {
4595
- const typeKey = result.type;
4596
- if (!byType[typeKey]) {
4597
- byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
4598
- }
4599
- for (const suite of result.suites) {
4600
- for (const test of suite.tests) {
4601
- summary.total++;
4602
- byType[typeKey].total++;
4603
- if (test.status === "passed") {
4604
- summary.passed++;
4605
- byType[typeKey].passed++;
4606
- } else if (test.status === "failed") {
4607
- summary.failed++;
4608
- byType[typeKey].failed++;
4609
- } else if (test.status === "skipped") {
4610
- summary.skipped++;
4611
- byType[typeKey].skipped++;
4612
- } else {
4613
- summary.pending++;
4614
- }
4615
- }
4616
- }
4617
- if (result.coverage) {
4618
- if (!coverage) {
4619
- coverage = { ...result.coverage };
4620
- } else {
4621
- coverage.lines = Math.max(coverage.lines, result.coverage.lines);
4622
- coverage.statements = Math.max(coverage.statements, result.coverage.statements);
4623
- coverage.functions = Math.max(coverage.functions, result.coverage.functions);
4624
- coverage.branches = Math.max(coverage.branches, result.coverage.branches);
4625
- }
4626
- }
4627
- }
4628
- const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
4629
- return {
4630
- timestamp: Date.now(),
4631
- duration: totalDuration,
4632
- results,
4633
- summary,
4634
- byType,
4635
- coverage: coverage?.lines ? coverage : void 0
4636
- };
4637
- }
4638
- function formatDuration2(ms) {
4639
- if (ms < 1e3) return `${ms}ms`;
4640
- if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
4641
- const minutes = Math.floor(ms / 6e4);
4642
- const seconds = (ms % 6e4 / 1e3).toFixed(0);
4643
- return `${minutes}m ${seconds}s`;
4644
- }
4645
- function formatTimestamp(ts) {
4646
- return new Date(ts).toLocaleString("zh-CN", {
4647
- year: "numeric",
4648
- month: "2-digit",
4649
- day: "2-digit",
4650
- hour: "2-digit",
4651
- minute: "2-digit",
4652
- second: "2-digit"
4653
- });
4654
- }
4655
- function pct(value) {
4656
- return `${(value * 100).toFixed(1)}%`;
4657
- }
4658
- function renderCoverageMD(coverage) {
4659
- return `
4660
- ### \u8986\u76D6\u7387
4661
-
4662
- | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
4663
- |------|--------|------|
4664
- | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
4665
- | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
4666
- | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
4667
- | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
4668
- `;
4669
- }
4670
- function renderProgressBar(value) {
4671
- const filled = Math.round(value * 10);
4672
- const empty = 10 - filled;
4673
- return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
4674
- }
4675
- var TYPE_LABELS2 = {
4676
- unit: "\u5355\u5143\u6D4B\u8BD5",
4677
- component: "\u7EC4\u4EF6\u6D4B\u8BD5",
4678
- e2e: "E2E \u6D4B\u8BD5",
4679
- api: "API \u6D4B\u8BD5",
4680
- visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
4681
- performance: "\u6027\u80FD\u6D4B\u8BD5"
4682
- };
4683
- function generateMDReport(data) {
4684
- const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
4685
- const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
4686
- const lines = [];
4687
- lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
4688
- lines.push("");
4689
- lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration2(data.duration)}`);
4690
- lines.push("");
4691
- lines.push(`## \u603B\u89C8`);
4692
- lines.push("");
4693
- lines.push(`| \u6307\u6807 | \u6570\u503C |`);
4694
- lines.push(`|------|------|`);
4695
- lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
4696
- lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
4697
- lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
4698
- if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
4699
- if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
4700
- if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
4701
- lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration2(data.duration)} |`);
4702
- lines.push("");
4703
- if (Object.keys(data.byType).length > 0) {
4704
- lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
4705
- lines.push("");
4706
- lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
4707
- lines.push(`|------|------|------|------|------|--------|`);
4708
- for (const [type, stats] of Object.entries(data.byType)) {
4709
- const label = TYPE_LABELS2[type] || type;
4710
- const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
4711
- lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
4712
- }
4713
- lines.push("");
4714
- }
4715
- if (data.coverage) {
4716
- lines.push(renderCoverageMD(data.coverage));
4717
- lines.push("");
4718
- }
4719
- lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
4720
- lines.push("");
4721
- for (const result of data.results) {
4722
- const typeLabel = TYPE_LABELS2[result.type] || result.type;
4723
- const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
4724
- lines.push(`### ${statusIcon} ${typeLabel}`);
4725
- lines.push("");
4726
- if (result.suites.length === 0) {
4727
- lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
4728
- lines.push("");
4729
- continue;
4730
- }
4731
- for (const suite of result.suites) {
4732
- const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
4733
- lines.push(`#### ${suiteIcon} ${suite.name}`);
4734
- lines.push("");
4735
- lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
4736
- lines.push(`- \u8017\u65F6: ${formatDuration2(suite.duration)}`);
4737
- lines.push("");
4738
- if (suite.tests.length > 0) {
4739
- lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
4740
- lines.push(`|------|----------|------|`);
4741
- for (const test of suite.tests) {
4742
- const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
4743
- const name = test.error ? `**${test.name}**` : test.name;
4744
- lines.push(`| ${testIcon} | ${name} | ${formatDuration2(test.duration)} |`);
4745
- }
4746
- lines.push("");
4747
- }
4748
- const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
4749
- if (failedTests.length > 0) {
4750
- lines.push(`<details>`);
4751
- lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
4752
- lines.push("");
4753
- for (const test of failedTests) {
4754
- lines.push(`**${test.name}**`);
4755
- lines.push("```");
4756
- lines.push(test.error.message);
4757
- if (test.error.stack) {
4758
- lines.push(test.error.stack);
4759
- }
4760
- if (test.error.expected && test.error.actual) {
4761
- lines.push(`Expected: ${test.error.expected}`);
4762
- lines.push(`Actual: ${test.error.actual}`);
4763
- }
4764
- lines.push("```");
4765
- lines.push("");
4766
- }
4767
- lines.push(`</details>`);
4768
- lines.push("");
4769
- }
4770
- }
4771
- }
4772
- lines.push("---");
4773
- lines.push("");
4774
- lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
4775
- return lines.join("\n");
4776
- }
4777
- function writeReportToDisk(data, outputDir) {
4778
- const md = generateMDReport(data);
4779
- const dir = path12.resolve(outputDir);
4780
- if (!fs10.existsSync(dir)) {
4781
- fs10.mkdirSync(dir, { recursive: true });
4782
- }
4783
- const mdPath = path12.join(dir, "report.md");
4784
- fs10.writeFileSync(mdPath, md, "utf-8");
4785
- const jsonPath = path12.join(dir, "report.json");
4786
- fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
4787
- return mdPath;
4788
- }
4789
-
4790
- // src/commands/report.ts
4791
4877
  var RESULTS_DIR2 = ".qat-results";
4792
4878
  function registerReportCommand(program2) {
4793
4879
  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) => {
@@ -4824,14 +4910,14 @@ async function executeReport(options) {
4824
4910
  function collectResults() {
4825
4911
  const results = [];
4826
4912
  const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
4827
- if (!fs11.existsSync(resultsPath)) {
4913
+ if (!fs12.existsSync(resultsPath)) {
4828
4914
  return results;
4829
4915
  }
4830
- const files = fs11.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
4916
+ const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
4831
4917
  if (files.length > 0) {
4832
4918
  const latestFile = path13.join(resultsPath, files[0]);
4833
4919
  try {
4834
- const data = JSON.parse(fs11.readFileSync(latestFile, "utf-8"));
4920
+ const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
4835
4921
  if (Array.isArray(data)) {
4836
4922
  results.push(...data);
4837
4923
  } else if (data.results) {
@@ -4844,17 +4930,17 @@ function collectResults() {
4844
4930
  }
4845
4931
  function saveResultToHistory(reportData) {
4846
4932
  const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
4847
- if (!fs11.existsSync(resultsPath)) {
4848
- fs11.mkdirSync(resultsPath, { recursive: true });
4933
+ if (!fs12.existsSync(resultsPath)) {
4934
+ fs12.mkdirSync(resultsPath, { recursive: true });
4849
4935
  }
4850
4936
  const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
4851
4937
  const fileName = `result-${timestamp}.json`;
4852
4938
  const filePath = path13.join(resultsPath, fileName);
4853
- fs11.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
4854
- const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4939
+ fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
4940
+ const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4855
4941
  while (files.length > 20) {
4856
4942
  const oldest = files.shift();
4857
- fs11.unlinkSync(path13.join(resultsPath, oldest));
4943
+ fs12.unlinkSync(path13.join(resultsPath, oldest));
4858
4944
  }
4859
4945
  }
4860
4946
  function displayReportResult(reportPath, data) {
@@ -4915,19 +5001,19 @@ import chalk8 from "chalk";
4915
5001
  import ora7 from "ora";
4916
5002
 
4917
5003
  // src/services/visual.ts
4918
- import fs12 from "fs";
5004
+ import fs13 from "fs";
4919
5005
  import path14 from "path";
4920
5006
  import pixelmatch from "pixelmatch";
4921
5007
  import { PNG } from "pngjs";
4922
5008
  function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
4923
- if (!fs12.existsSync(baselinePath)) {
5009
+ if (!fs13.existsSync(baselinePath)) {
4924
5010
  throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
4925
5011
  }
4926
- if (!fs12.existsSync(currentPath)) {
5012
+ if (!fs13.existsSync(currentPath)) {
4927
5013
  throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
4928
5014
  }
4929
- const baseline = PNG.sync.read(fs12.readFileSync(baselinePath));
4930
- const current = PNG.sync.read(fs12.readFileSync(currentPath));
5015
+ const baseline = PNG.sync.read(fs13.readFileSync(baselinePath));
5016
+ const current = PNG.sync.read(fs13.readFileSync(currentPath));
4931
5017
  if (baseline.width !== current.width || baseline.height !== current.height) {
4932
5018
  return {
4933
5019
  passed: false,
@@ -4956,10 +5042,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
4956
5042
  let diffPath;
4957
5043
  if (diffPixels > 0) {
4958
5044
  const diffDir = path14.dirname(diffOutputPath);
4959
- if (!fs12.existsSync(diffDir)) {
4960
- fs12.mkdirSync(diffDir, { recursive: true });
5045
+ if (!fs13.existsSync(diffDir)) {
5046
+ fs13.mkdirSync(diffDir, { recursive: true });
4961
5047
  }
4962
- fs12.writeFileSync(diffOutputPath, PNG.sync.write(diff));
5048
+ fs13.writeFileSync(diffOutputPath, PNG.sync.write(diff));
4963
5049
  diffPath = diffOutputPath;
4964
5050
  }
4965
5051
  return {
@@ -4973,69 +5059,69 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
4973
5059
  };
4974
5060
  }
4975
5061
  function createBaseline(currentPath, baselinePath) {
4976
- if (!fs12.existsSync(currentPath)) {
5062
+ if (!fs13.existsSync(currentPath)) {
4977
5063
  throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
4978
5064
  }
4979
5065
  const baselineDir = path14.dirname(baselinePath);
4980
- if (!fs12.existsSync(baselineDir)) {
4981
- fs12.mkdirSync(baselineDir, { recursive: true });
5066
+ if (!fs13.existsSync(baselineDir)) {
5067
+ fs13.mkdirSync(baselineDir, { recursive: true });
4982
5068
  }
4983
- fs12.copyFileSync(currentPath, baselinePath);
5069
+ fs13.copyFileSync(currentPath, baselinePath);
4984
5070
  return baselinePath;
4985
5071
  }
4986
5072
  function updateAllBaselines(currentDir, baselineDir) {
4987
5073
  const updated = [];
4988
- if (!fs12.existsSync(currentDir)) {
5074
+ if (!fs13.existsSync(currentDir)) {
4989
5075
  return updated;
4990
5076
  }
4991
- const files = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5077
+ const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
4992
5078
  for (const file of files) {
4993
5079
  const currentPath = path14.join(currentDir, file);
4994
5080
  const baselinePath = path14.join(baselineDir, file);
4995
5081
  const baselineDirAbs = path14.dirname(baselinePath);
4996
- if (!fs12.existsSync(baselineDirAbs)) {
4997
- fs12.mkdirSync(baselineDirAbs, { recursive: true });
5082
+ if (!fs13.existsSync(baselineDirAbs)) {
5083
+ fs13.mkdirSync(baselineDirAbs, { recursive: true });
4998
5084
  }
4999
- fs12.copyFileSync(currentPath, baselinePath);
5085
+ fs13.copyFileSync(currentPath, baselinePath);
5000
5086
  updated.push(file);
5001
5087
  }
5002
5088
  return updated;
5003
5089
  }
5004
5090
  function cleanBaselines(baselineDir) {
5005
- if (!fs12.existsSync(baselineDir)) {
5091
+ if (!fs13.existsSync(baselineDir)) {
5006
5092
  return 0;
5007
5093
  }
5008
- const files = fs12.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5094
+ const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5009
5095
  let count = 0;
5010
5096
  for (const file of files) {
5011
- fs12.unlinkSync(path14.join(baselineDir, file));
5097
+ fs13.unlinkSync(path14.join(baselineDir, file));
5012
5098
  count++;
5013
5099
  }
5014
5100
  return count;
5015
5101
  }
5016
5102
  function cleanDiffs(diffDir) {
5017
- if (!fs12.existsSync(diffDir)) {
5103
+ if (!fs13.existsSync(diffDir)) {
5018
5104
  return 0;
5019
5105
  }
5020
- const files = fs12.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5106
+ const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5021
5107
  let count = 0;
5022
5108
  for (const file of files) {
5023
- fs12.unlinkSync(path14.join(diffDir, file));
5109
+ fs13.unlinkSync(path14.join(diffDir, file));
5024
5110
  count++;
5025
5111
  }
5026
5112
  return count;
5027
5113
  }
5028
5114
  function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5029
5115
  const results = [];
5030
- if (!fs12.existsSync(currentDir)) {
5116
+ if (!fs13.existsSync(currentDir)) {
5031
5117
  return results;
5032
5118
  }
5033
- const currentFiles = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5119
+ const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5034
5120
  for (const file of currentFiles) {
5035
5121
  const currentPath = path14.join(currentDir, file);
5036
5122
  const baselinePath = path14.join(baselineDir, file);
5037
5123
  const diffPath = path14.join(diffDir, file);
5038
- if (!fs12.existsSync(baselinePath)) {
5124
+ if (!fs13.existsSync(baselinePath)) {
5039
5125
  createBaseline(currentPath, baselinePath);
5040
5126
  results.push({
5041
5127
  passed: true,
@@ -5067,7 +5153,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5067
5153
  }
5068
5154
 
5069
5155
  // src/commands/visual.ts
5070
- import fs13 from "fs";
5156
+ import fs14 from "fs";
5071
5157
  import path15 from "path";
5072
5158
  function registerVisualCommand(program2) {
5073
5159
  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) => {
@@ -5148,7 +5234,7 @@ function findCurrentScreenshotsDir(baselineDir) {
5148
5234
  path15.join(process.cwd(), baselineDir, "..", "current")
5149
5235
  ];
5150
5236
  for (const dir of possibleDirs) {
5151
- if (fs13.existsSync(dir)) {
5237
+ if (fs14.existsSync(dir)) {
5152
5238
  const pngs = findPngFiles(dir);
5153
5239
  if (pngs.length > 0) return dir;
5154
5240
  }
@@ -5158,8 +5244,8 @@ function findCurrentScreenshotsDir(baselineDir) {
5158
5244
  function findPngFiles(dir) {
5159
5245
  const files = [];
5160
5246
  function walk(d) {
5161
- if (!fs13.existsSync(d)) return;
5162
- const entries = fs13.readdirSync(d, { withFileTypes: true });
5247
+ if (!fs14.existsSync(d)) return;
5248
+ const entries = fs14.readdirSync(d, { withFileTypes: true });
5163
5249
  for (const entry of entries) {
5164
5250
  const fullPath = path15.join(d, entry.name);
5165
5251
  if (entry.isDirectory() && entry.name !== "node_modules") {
@@ -5257,7 +5343,7 @@ import chalk9 from "chalk";
5257
5343
  import inquirer4 from "inquirer";
5258
5344
  import ora8 from "ora";
5259
5345
  import { execFile as execFile4 } from "child_process";
5260
- import fs14 from "fs";
5346
+ import fs15 from "fs";
5261
5347
  import path16 from "path";
5262
5348
  var DEPENDENCY_GROUPS = [
5263
5349
  {
@@ -5292,7 +5378,7 @@ function registerSetupCommand(program2) {
5292
5378
  async function executeSetup(options) {
5293
5379
  console.log(chalk9.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
5294
5380
  const projectInfo = detectProject();
5295
- if (!fs14.existsSync(path16.join(process.cwd(), "package.json"))) {
5381
+ if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
5296
5382
  throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
5297
5383
  }
5298
5384
  if (projectInfo.frameworkConfidence > 0) {
@@ -5327,7 +5413,7 @@ async function executeSetup(options) {
5327
5413
  ]);
5328
5414
  if (chooseDir !== "root") {
5329
5415
  installDir = path16.join(process.cwd(), chooseDir);
5330
- if (!fs14.existsSync(path16.join(installDir, "package.json"))) {
5416
+ if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
5331
5417
  throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
5332
5418
  }
5333
5419
  }
@@ -5647,7 +5733,7 @@ async function executeChange(_options) {
5647
5733
  }
5648
5734
 
5649
5735
  // src/cli.ts
5650
- var VERSION = "0.3.01";
5736
+ var VERSION = "0.3.03";
5651
5737
  function printLogo() {
5652
5738
  const logo = `
5653
5739
  ${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}