qat-cli 0.3.1 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/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";
@@ -1262,99 +1262,6 @@ async function testAIConnection(config) {
1262
1262
  return provider.testConnection();
1263
1263
  }
1264
1264
 
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
1265
  // src/services/source-analyzer.ts
1359
1266
  import fs4 from "fs";
1360
1267
  import path4 from "path";
@@ -1790,154 +1697,492 @@ function generateMockRoutesFromAPICalls(apiCalls) {
1790
1697
  return routes;
1791
1698
  }
1792
1699
 
1793
- // src/services/template.ts
1700
+ // src/services/global-config.ts
1794
1701
  import fs5 from "fs";
1795
1702
  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);
1703
+ import os from "os";
1704
+ var QAT_DIR = path5.join(os.homedir(), ".qat");
1705
+ var AI_CONFIG_PATH = path5.join(QAT_DIR, "ai.json");
1706
+ function loadGlobalAIConfig() {
1707
+ if (!fs5.existsSync(AI_CONFIG_PATH)) {
1708
+ return null;
1880
1709
  }
1881
- if (fullContext.emits && fullContext.emits.length > 0) {
1882
- fullContext.isVueComponent = true;
1710
+ try {
1711
+ const content = fs5.readFileSync(AI_CONFIG_PATH, "utf-8");
1712
+ const config = JSON.parse(content);
1713
+ if (!config.baseUrl || !config.model) {
1714
+ return null;
1715
+ }
1716
+ return config;
1717
+ } catch {
1718
+ return null;
1883
1719
  }
1884
- return template(fullContext);
1885
1720
  }
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");
1721
+ function saveGlobalAIConfig(config) {
1722
+ if (!fs5.existsSync(QAT_DIR)) {
1723
+ fs5.mkdirSync(QAT_DIR, { recursive: true });
1894
1724
  }
1895
- return getBuiltinTemplate(type);
1725
+ fs5.writeFileSync(AI_CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
1896
1726
  }
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");
1727
+ function toAIConfig(globalConfig) {
1728
+ return {
1729
+ provider: globalConfig.provider || "openai",
1730
+ apiKey: globalConfig.apiKey || void 0,
1731
+ baseUrl: globalConfig.baseUrl,
1732
+ model: globalConfig.model
1733
+ };
1734
+ }
1735
+ function maskApiKey(apiKey) {
1736
+ if (!apiKey) return "(\u672A\u8BBE\u7F6E)";
1737
+ if (apiKey.length <= 8) return "****";
1738
+ return apiKey.slice(0, 4) + "****" + apiKey.slice(-4);
1739
+ }
1740
+ function getAIConfigPath() {
1741
+ return AI_CONFIG_PATH;
1903
1742
  }
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
1743
 
1918
- describe('{{name}}', () => {
1919
- {{#if hasAnalysis}}
1920
- {{#each functionExports}}
1921
- describe('{{name}}()', () => {
1922
- {{#if isAsync}}
1923
- it('should resolve successfully', async () => {
1924
- const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}{{this}}: undefined{{/each}});
1925
- expect(result).toBeDefined();
1926
- });
1927
- {{else}}
1928
- it('should return a value', () => {
1929
- const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1930
- expect(result).toBeDefined();
1931
- });
1932
- {{/if}}
1933
- {{#if returnType}}
1934
- it('should return correct type', () => {
1935
- {{#if isAsync}}
1936
- const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1937
- {{else}}
1938
- const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
1939
- {{/if}}
1940
- expect(result).toBeDefined();
1744
+ // src/commands/init.ts
1745
+ function registerInitCommand(program2) {
1746
+ 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) => {
1747
+ try {
1748
+ await executeInit(options);
1749
+ } catch (error) {
1750
+ console.error(chalk2.red(`
1751
+ \u2717 ${error instanceof Error ? error.message : String(error)}
1752
+ `));
1753
+ process.exit(1);
1754
+ }
1755
+ });
1756
+ }
1757
+ async function executeInit(options) {
1758
+ const spinner = ora("\u6B63\u5728\u68C0\u6D4B\u9879\u76EE\u7ED3\u6784...").start();
1759
+ const projectInfo = detectProject();
1760
+ spinner.stop();
1761
+ displayProjectInfo(projectInfo);
1762
+ if (!projectInfo.isVue) {
1763
+ const { proceed } = await inquirer.prompt([
1764
+ {
1765
+ type: "confirm",
1766
+ name: "proceed",
1767
+ message: "\u672A\u68C0\u6D4B\u5230 Vue \u9879\u76EE\uFF0C\u662F\u5426\u7EE7\u7EED\u521D\u59CB\u5316\uFF1F",
1768
+ default: false
1769
+ }
1770
+ ]);
1771
+ if (!proceed) {
1772
+ console.log(chalk2.gray("\n \u5DF2\u53D6\u6D88\u521D\u59CB\u5316\n"));
1773
+ return;
1774
+ }
1775
+ }
1776
+ let globalAI = loadGlobalAIConfig();
1777
+ if (!globalAI) {
1778
+ 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"));
1779
+ globalAI = await promptAIConfig();
1780
+ saveGlobalAIConfig(globalAI);
1781
+ console.log(chalk2.gray(` \u914D\u7F6E\u5DF2\u4FDD\u5B58\u81F3 ${getAIConfigPath()}
1782
+ `));
1783
+ } else {
1784
+ console.log(chalk2.green(` \u2713 \u5F53\u524D AI \u6A21\u578B: ${chalk2.white(globalAI.model)} @ ${chalk2.gray(globalAI.baseUrl)} (${maskApiKey(globalAI.apiKey)})
1785
+ `));
1786
+ }
1787
+ const aiConfig = toAIConfig(globalAI);
1788
+ if (aiConfig.apiKey || aiConfig.baseUrl) {
1789
+ const testSpinner = ora(`\u6B63\u5728\u6D4B\u8BD5 AI \u8FDE\u901A\u6027 (${globalAI.model})...`).start();
1790
+ try {
1791
+ const result = await testAIConnection(aiConfig);
1792
+ if (result.ok) {
1793
+ testSpinner.succeed(`AI \u8FDE\u901A\u6B63\u5E38 ${chalk2.gray(`${globalAI.model} (${result.latencyMs}ms)`)}`);
1794
+ } else {
1795
+ testSpinner.fail(`AI \u8FDE\u901A\u5F02\u5E38: ${result.message}`);
1796
+ console.log(chalk2.yellow(" \u53EF\u8FD0\u884C qat change \u4FEE\u6539 AI \u914D\u7F6E\u3002"));
1797
+ }
1798
+ } catch (error) {
1799
+ testSpinner.fail(`AI \u8FDE\u901A\u6D4B\u8BD5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`);
1800
+ }
1801
+ }
1802
+ const config = buildProjectConfig(projectInfo);
1803
+ let configPath;
1804
+ const existingConfigPath = path6.join(process.cwd(), "qat.config.js");
1805
+ const existingTsPath = path6.join(process.cwd(), "qat.config.ts");
1806
+ const configExists = fs6.existsSync(existingConfigPath) || fs6.existsSync(existingTsPath);
1807
+ if (configExists && !options.force) {
1808
+ const { overwrite } = await inquirer.prompt([
1809
+ {
1810
+ type: "confirm",
1811
+ name: "overwrite",
1812
+ message: "\u914D\u7F6E\u6587\u4EF6 qat.config.js \u5DF2\u5B58\u5728\uFF0C\u662F\u5426\u8986\u76D6\uFF1F",
1813
+ default: true
1814
+ }
1815
+ ]);
1816
+ if (!overwrite) {
1817
+ console.log(chalk2.gray(" \u4FDD\u7559\u73B0\u6709\u914D\u7F6E\u6587\u4EF6\uFF0C\u7EE7\u7EED\u540E\u7EED\u6B65\u9AA4..."));
1818
+ configPath = existingConfigPath;
1819
+ } else {
1820
+ const fileSpinner = ora("\u6B63\u5728\u8986\u76D6\u914D\u7F6E\u6587\u4EF6...").start();
1821
+ try {
1822
+ configPath = await writeConfigFile(process.cwd(), config, true);
1823
+ fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u8986\u76D6");
1824
+ } catch (error) {
1825
+ fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u8986\u76D6\u5931\u8D25");
1826
+ throw error;
1827
+ }
1828
+ }
1829
+ } else {
1830
+ const fileSpinner = ora("\u6B63\u5728\u751F\u6210\u914D\u7F6E\u6587\u4EF6...").start();
1831
+ try {
1832
+ configPath = await writeConfigFile(process.cwd(), config, options.force);
1833
+ fileSpinner.succeed("\u914D\u7F6E\u6587\u4EF6\u5DF2\u751F\u6210");
1834
+ } catch (error) {
1835
+ fileSpinner.fail("\u914D\u7F6E\u6587\u4EF6\u751F\u6210\u5931\u8D25");
1836
+ throw error;
1837
+ }
1838
+ }
1839
+ const dirSpinner = ora("\u6B63\u5728\u521B\u5EFA\u6D4B\u8BD5\u76EE\u5F55...").start();
1840
+ const createdDirs = createTestDirectories(config);
1841
+ dirSpinner.succeed("\u6D4B\u8BD5\u76EE\u5F55\u5DF2\u521B\u5EFA");
1842
+ if (config.mock?.enabled !== false) {
1843
+ const mockDir = config.mock?.routesDir || DEFAULT_CONFIG.mock.routesDir;
1844
+ initMockRoutesDir(mockDir);
1845
+ const srcDir2 = config.project?.srcDir || "src";
1846
+ const apiCalls = scanAPICalls(srcDir2);
1847
+ if (apiCalls.length > 0) {
1848
+ const mockRoutes = generateMockRoutesFromAPICalls(apiCalls);
1849
+ const mockFilePath = path6.join(process.cwd(), mockDir, "auto-generated.json");
1850
+ if (!fs6.existsSync(mockFilePath)) {
1851
+ fs6.writeFileSync(mockFilePath, JSON.stringify(mockRoutes, null, 2), "utf-8");
1852
+ console.log(chalk2.green(` \u81EA\u52A8\u53D1\u73B0 ${apiCalls.length} \u4E2A API \u63A5\u53E3\uFF0C\u5DF2\u751F\u6210 Mock \u8DEF\u7531`));
1853
+ }
1854
+ } else {
1855
+ console.log(chalk2.gray(" \u672A\u53D1\u73B0 API \u8C03\u7528\uFF0C\u5DF2\u751F\u6210\u793A\u4F8B Mock \u8DEF\u7531"));
1856
+ }
1857
+ }
1858
+ const srcDir = config.project?.srcDir || "src";
1859
+ const components = discoverVueComponents(process.cwd(), srcDir);
1860
+ const utilities = discoverUtilityFiles(process.cwd(), srcDir);
1861
+ const totalFiles = components.length + utilities.length;
1862
+ if (totalFiles > 0) {
1863
+ console.log(chalk2.cyan(`
1864
+ \u53D1\u73B0 ${totalFiles} \u4E2A\u53EF\u6D4B\u8BD5\u6587\u4EF6 (${components.length} \u7EC4\u4EF6, ${utilities.length} \u5DE5\u5177/\u670D\u52A1)`));
1865
+ }
1866
+ displayResult(configPath, createdDirs, totalFiles, projectInfo);
1867
+ }
1868
+ async function promptAIConfig() {
1869
+ const answers = await inquirer.prompt([
1870
+ {
1871
+ type: "input",
1872
+ name: "apiKey",
1873
+ message: "API Key (Ollama \u672C\u5730\u53EF\u7559\u7A7A):",
1874
+ default: ""
1875
+ },
1876
+ {
1877
+ type: "input",
1878
+ name: "baseUrl",
1879
+ message: "API Base URL:",
1880
+ default: "https://api.deepseek.com/v1",
1881
+ validate: (input) => {
1882
+ if (!input.trim()) return "Base URL \u4E0D\u80FD\u4E3A\u7A7A";
1883
+ if (!input.trim().startsWith("http")) return "URL \u5FC5\u987B\u4EE5 http(s):// \u5F00\u5934";
1884
+ return true;
1885
+ }
1886
+ },
1887
+ {
1888
+ type: "input",
1889
+ name: "model",
1890
+ message: "\u6A21\u578B\u540D\u79F0:",
1891
+ default: "deepseek-chat",
1892
+ validate: (input) => {
1893
+ if (!input.trim()) return "\u6A21\u578B\u540D\u79F0\u4E0D\u80FD\u4E3A\u7A7A";
1894
+ return true;
1895
+ }
1896
+ }
1897
+ ]);
1898
+ return {
1899
+ provider: "openai",
1900
+ apiKey: answers.apiKey?.trim() || "",
1901
+ baseUrl: answers.baseUrl?.trim() || "https://api.deepseek.com/v1",
1902
+ model: answers.model?.trim() || "deepseek-chat"
1903
+ };
1904
+ }
1905
+ function buildProjectConfig(projectInfo) {
1906
+ return {
1907
+ project: {
1908
+ framework: projectInfo.framework,
1909
+ uiLibrary: projectInfo.uiLibrary !== "none" ? projectInfo.uiLibrary : void 0,
1910
+ monorepo: projectInfo.monorepo !== "none" ? projectInfo.monorepo : void 0,
1911
+ vite: projectInfo.isVite,
1912
+ srcDir: projectInfo.srcDir,
1913
+ appDir: projectInfo.appDirs.length > 0 ? projectInfo.appDirs[0] : void 0
1914
+ },
1915
+ vitest: {
1916
+ enabled: true,
1917
+ coverage: true,
1918
+ globals: true,
1919
+ environment: "happy-dom"
1920
+ },
1921
+ playwright: {
1922
+ enabled: true,
1923
+ browsers: ["chromium"],
1924
+ baseURL: "http://localhost:5173",
1925
+ screenshot: "only-on-failure"
1926
+ },
1927
+ visual: {
1928
+ enabled: true,
1929
+ threshold: 0.1,
1930
+ baselineDir: "tests/visual/baseline",
1931
+ diffDir: "tests/visual/diff"
1932
+ },
1933
+ lighthouse: {
1934
+ enabled: true,
1935
+ urls: ["http://localhost:5173"],
1936
+ runs: 3,
1937
+ thresholds: {
1938
+ performance: 80,
1939
+ accessibility: 90
1940
+ }
1941
+ },
1942
+ mock: {
1943
+ enabled: true,
1944
+ port: 3456,
1945
+ routesDir: "tests/mock/routes"
1946
+ }
1947
+ };
1948
+ }
1949
+ function displayProjectInfo(info) {
1950
+ console.log(chalk2.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
1951
+ 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"));
1952
+ const items = [
1953
+ ["\u9879\u76EE\u540D\u79F0", info.name],
1954
+ ["\u6846\u67B6", info.frameworkConfidence > 0.5 ? `${info.frameworkDisplayName} (\u7F6E\u4FE1\u5EA6 ${Math.round(info.frameworkConfidence * 100)}%)` : info.frameworkDisplayName],
1955
+ ["Vue \u9879\u76EE", info.isVue ? `\u662F (v${info.vueVersion})` : "\u5426"],
1956
+ ["UI \u7EC4\u4EF6\u5E93", info.uiLibrary !== "none" ? info.uiLibrary : "\u672A\u68C0\u6D4B\u5230"],
1957
+ ["Vite \u6784\u5EFA", info.isVite ? "\u662F" : "\u5426"],
1958
+ ["TypeScript", info.typescript ? "\u662F" : "\u5426"],
1959
+ ["\u5305\u7BA1\u7406\u5668", info.packageManager],
1960
+ ["Monorepo", info.monorepo !== "none" ? info.monorepo : "\u5426"],
1961
+ ["\u6E90\u7801\u76EE\u5F55", info.srcDir]
1962
+ ];
1963
+ if (info.appDirs.length > 0) {
1964
+ items.push(["\u5B50\u9879\u76EE", info.appDirs.join(", ")]);
1965
+ }
1966
+ if (info.testFrameworks.length > 0) {
1967
+ items.push(["\u5DF2\u6709\u6D4B\u8BD5\u6846\u67B6", info.testFrameworks.join(", ")]);
1968
+ }
1969
+ if (info.componentDirs.length > 0) {
1970
+ items.push(["\u7EC4\u4EF6\u76EE\u5F55", info.componentDirs.join(", ")]);
1971
+ }
1972
+ for (const [label, value] of items) {
1973
+ const displayValue = value === true ? chalk2.green("\u2713") : value === false ? chalk2.red("\u2717") : String(value);
1974
+ console.log(` ${chalk2.white(label.padEnd(12))} ${displayValue}`);
1975
+ }
1976
+ console.log();
1977
+ }
1978
+ function createTestDirectories(config) {
1979
+ const dirs = [];
1980
+ const dirMap = {
1981
+ "tests": true,
1982
+ "tests/unit": config.vitest?.enabled !== false,
1983
+ "tests/component": config.vitest?.enabled !== false,
1984
+ "tests/e2e": config.playwright?.enabled !== false,
1985
+ "tests/api": config.mock?.enabled !== false,
1986
+ "tests/visual": config.visual?.enabled !== false,
1987
+ "tests/visual/baseline": config.visual?.enabled !== false,
1988
+ "tests/visual/diff": config.visual?.enabled !== false,
1989
+ "tests/mock": config.mock?.enabled !== false,
1990
+ "tests/mock/routes": config.mock?.enabled !== false
1991
+ };
1992
+ for (const [dir, shouldCreate] of Object.entries(dirMap)) {
1993
+ if (shouldCreate) {
1994
+ const fullPath = path6.join(process.cwd(), dir);
1995
+ if (!fs6.existsSync(fullPath)) {
1996
+ fs6.mkdirSync(fullPath, { recursive: true });
1997
+ dirs.push(dir);
1998
+ }
1999
+ }
2000
+ }
2001
+ return dirs;
2002
+ }
2003
+ function displayResult(configPath, createdDirs, totalTestableFiles, projectInfo) {
2004
+ const relativeConfig = path6.relative(process.cwd(), configPath);
2005
+ console.log(chalk2.green("\n \u2713 \u9879\u76EE\u521D\u59CB\u5316\u5B8C\u6210!\n"));
2006
+ console.log(chalk2.white(" \u5DF2\u751F\u6210\u914D\u7F6E:"));
2007
+ console.log(chalk2.gray(` ${relativeConfig}`));
2008
+ console.log();
2009
+ if (createdDirs.length > 0) {
2010
+ console.log(chalk2.white(" \u5DF2\u521B\u5EFA\u76EE\u5F55:"));
2011
+ for (const dir of createdDirs) {
2012
+ console.log(chalk2.gray(` ${dir}/`));
2013
+ }
2014
+ console.log();
2015
+ }
2016
+ console.log(chalk2.cyan(" \u4E0B\u4E00\u6B65:"));
2017
+ if (totalTestableFiles > 0) {
2018
+ 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)`));
2019
+ } else {
2020
+ console.log(chalk2.gray(" 1. qat create \u521B\u5EFA\u6D4B\u8BD5\u7528\u4F8B"));
2021
+ }
2022
+ console.log(chalk2.gray(" 2. qat run \u6267\u884C\u6D4B\u8BD5"));
2023
+ console.log(chalk2.gray(" 3. qat status \u67E5\u770B AI \u6A21\u578B\u72B6\u6001"));
2024
+ if (projectInfo.testFrameworks.length === 0) {
2025
+ console.log();
2026
+ 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"));
2027
+ }
2028
+ console.log();
2029
+ }
2030
+
2031
+ // src/commands/create.ts
2032
+ import chalk4 from "chalk";
2033
+ import inquirer2 from "inquirer";
2034
+ import ora3 from "ora";
2035
+ import fs8 from "fs";
2036
+ import path8 from "path";
2037
+
2038
+ // src/services/template.ts
2039
+ import fs7 from "fs";
2040
+ import path7 from "path";
2041
+ import { fileURLToPath } from "url";
2042
+ import Handlebars from "handlebars";
2043
+ var __filename = fileURLToPath(import.meta.url);
2044
+ var __dirname = path7.dirname(__filename);
2045
+ var TEMPLATE_MAP = {
2046
+ unit: "unit-test.hbs",
2047
+ component: "component-test.hbs",
2048
+ e2e: "e2e-test.hbs",
2049
+ api: "api-test.hbs",
2050
+ visual: "visual-test.hbs",
2051
+ performance: "performance-test.hbs"
2052
+ };
2053
+ var customTemplates = /* @__PURE__ */ new Map();
2054
+ Handlebars.registerHelper("eq", (a, b) => a === b);
2055
+ Handlebars.registerHelper("neq", (a, b) => a !== b);
2056
+ Handlebars.registerHelper("includes", (arr, val) => Array.isArray(arr) && arr.includes(val));
2057
+ Handlebars.registerHelper("join", (arr, sep) => Array.isArray(arr) ? arr.join(sep) : "");
2058
+ Handlebars.registerHelper("camelCase", (str) => toCamelCase(str));
2059
+ Handlebars.registerHelper("pascalCase", (str) => toPascalCase(str));
2060
+ Handlebars.registerHelper("length", (arr) => Array.isArray(arr) ? arr.length : 0);
2061
+ Handlebars.registerHelper("gt", (a, b) => Number(a) > Number(b));
2062
+ Handlebars.registerHelper("and", (...args) => {
2063
+ args.pop();
2064
+ return args.every(Boolean);
2065
+ });
2066
+ Handlebars.registerHelper("or", (...args) => {
2067
+ args.pop();
2068
+ return args.some(Boolean);
2069
+ });
2070
+ Handlebars.registerHelper("propDefaultValue", (prop) => {
2071
+ if (!prop.defaultValue) return "undefined";
2072
+ const map = { String: "''", Number: "0", Boolean: "false", Array: "[]", Object: "{}" };
2073
+ return map[prop.type] || prop.defaultValue;
2074
+ });
2075
+ Handlebars.registerHelper("propTestValue", (prop) => {
2076
+ const map = {
2077
+ String: "'test-value'",
2078
+ Number: "42",
2079
+ Boolean: "true",
2080
+ Array: "[1, 2, 3]",
2081
+ Object: '{ key: "value" }'
2082
+ };
2083
+ return map[prop.type] || "'test-value'";
2084
+ });
2085
+ function renderTemplate(type, context) {
2086
+ const templateContent = loadTemplate(type);
2087
+ const template = Handlebars.compile(templateContent);
2088
+ const fullContext = {
2089
+ vueVersion: 3,
2090
+ typescript: true,
2091
+ imports: [],
2092
+ extraImports: [],
2093
+ globalPlugins: [],
2094
+ globalStubs: [],
2095
+ mountOptions: "",
2096
+ hasAnalysis: false,
2097
+ exports: [],
2098
+ functionExports: [],
2099
+ valueExports: [],
2100
+ props: [],
2101
+ emits: [],
2102
+ methods: [],
2103
+ computed: [],
2104
+ isVueComponent: false,
2105
+ requiredProps: [],
2106
+ optionalProps: [],
2107
+ ...context,
2108
+ framework: context.framework || "vue",
2109
+ camelName: context.camelName || toCamelCase(context.name),
2110
+ pascalName: context.pascalName || toPascalCase(context.name)
2111
+ };
2112
+ if (fullContext.exports && fullContext.exports.length > 0) {
2113
+ fullContext.hasAnalysis = true;
2114
+ fullContext.functionExports = fullContext.exports.filter(
2115
+ (e) => e.kind === "function" || e.kind === "default"
2116
+ );
2117
+ fullContext.valueExports = fullContext.exports.filter(
2118
+ (e) => e.kind !== "function" && e.kind !== "default" && e.kind !== "type"
2119
+ );
2120
+ }
2121
+ if (fullContext.props && fullContext.props.length > 0) {
2122
+ fullContext.isVueComponent = true;
2123
+ fullContext.requiredProps = fullContext.props.filter((p) => p.required);
2124
+ fullContext.optionalProps = fullContext.props.filter((p) => !p.required);
2125
+ }
2126
+ if (fullContext.emits && fullContext.emits.length > 0) {
2127
+ fullContext.isVueComponent = true;
2128
+ }
2129
+ return template(fullContext);
2130
+ }
2131
+ function loadTemplate(type) {
2132
+ const custom = customTemplates.get(type);
2133
+ if (custom) return custom;
2134
+ const templateDir = getTemplateDir();
2135
+ const templateFile = TEMPLATE_MAP[type];
2136
+ const templatePath = path7.join(templateDir, templateFile);
2137
+ if (fs7.existsSync(templatePath)) {
2138
+ return fs7.readFileSync(templatePath, "utf-8");
2139
+ }
2140
+ return getBuiltinTemplate(type);
2141
+ }
2142
+ function getTemplateDir() {
2143
+ const projectTemplates = path7.join(process.cwd(), "templates");
2144
+ if (fs7.existsSync(projectTemplates)) {
2145
+ return projectTemplates;
2146
+ }
2147
+ return path7.join(__dirname, "..", "templates");
2148
+ }
2149
+ function getBuiltinTemplate(type) {
2150
+ const templates = {
2151
+ unit: `import { describe, it, expect } from 'vitest';
2152
+ {{#if hasAnalysis}}
2153
+ {{#each valueExports}}
2154
+ import { {{name}} } from '{{../target}}';
2155
+ {{/each}}
2156
+ {{#each functionExports}}
2157
+ import { {{name}} } from '{{../target}}';
2158
+ {{/each}}
2159
+ {{else}}
2160
+ import { {{camelName}} } from '{{target}}';
2161
+ {{/if}}
2162
+
2163
+ describe('{{name}}', () => {
2164
+ {{#if hasAnalysis}}
2165
+ {{#each functionExports}}
2166
+ describe('{{name}}()', () => {
2167
+ {{#if isAsync}}
2168
+ it('should resolve successfully', async () => {
2169
+ const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}{{this}}: undefined{{/each}});
2170
+ expect(result).toBeDefined();
2171
+ });
2172
+ {{else}}
2173
+ it('should return a value', () => {
2174
+ const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
2175
+ expect(result).toBeDefined();
2176
+ });
2177
+ {{/if}}
2178
+ {{#if returnType}}
2179
+ it('should return correct type', () => {
2180
+ {{#if isAsync}}
2181
+ const result = await {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
2182
+ {{else}}
2183
+ const result = {{name}}({{#each params}}{{#unless @first}}, {{/unless}}undefined as any{{/each}});
2184
+ {{/if}}
2185
+ expect(result).toBeDefined();
1941
2186
  });
1942
2187
  {{/if}}
1943
2188
  {{#if params.length}}
@@ -2191,647 +2436,127 @@ test.describe('{{name}} performance', () => {
2191
2436
  return {
2192
2437
  domContentLoaded: entry?.domContentLoadedEventEnd - entry?.domContentLoadedEventStart ?? 0,
2193
2438
  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
- }
2439
+ domInteractive: entry?.domInteractive ?? 0,
2440
+ };
2441
+ });
2264
2442
 
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
- }
2443
+ // DOM \u5185\u5BB9\u52A0\u8F7D\u5E94\u5728 2s \u5185
2444
+ expect(performanceMetrics.domInteractive).toBeLessThan(2000);
2300
2445
  });
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
- }
2446
+
2447
+ test('should not have memory leaks', async ({ page }) => {
2448
+ await page.goto('/');
2449
+
2450
+ const metrics = await page.metrics();
2451
+ expect(metrics.JSHeapUsedSize).toBeLessThan(50 * 1024 * 1024); // 50MB
2452
+ });
2453
+ });
2454
+ `
2486
2455
  };
2456
+ return templates[type];
2487
2457
  }
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;
2458
+ function toCamelCase(str) {
2459
+ return str.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^[A-Z]/, (c) => c.toLowerCase());
2617
2460
  }
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;
2461
+ function toPascalCase(str) {
2462
+ const camel = toCamelCase(str);
2463
+ return camel.charAt(0).toUpperCase() + camel.slice(1);
2634
2464
  }
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,
2465
+
2466
+ // src/services/test-reviewer.ts
2467
+ import chalk3 from "chalk";
2468
+ import ora2 from "ora";
2469
+ var MAX_RETRIES = 3;
2470
+ var REVIEW_THRESHOLD = 0.6;
2471
+ async function generateWithReview(params) {
2472
+ const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt } = params;
2473
+ const generatorProvider = createAIProvider(aiConfig);
2474
+ const reviewerProvider = createAIProvider(aiConfig);
2475
+ if (!generatorProvider.capabilities.generateTest) {
2476
+ throw new Error("\u5F53\u524D AI Provider \u4E0D\u652F\u6301\u6D4B\u8BD5\u751F\u6210");
2477
+ }
2478
+ let currentCode = "";
2479
+ let currentDescription = "";
2480
+ let currentConfidence = 0;
2481
+ let lastReview = null;
2482
+ let approved = false;
2483
+ let attempts = 0;
2484
+ for (let i = 0; i < MAX_RETRIES; i++) {
2485
+ attempts = i + 1;
2486
+ onAttempt?.(attempts, MAX_RETRIES);
2487
+ 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
2488
+ \u5BA1\u8BA1\u8BC4\u5206: ${(lastReview.score * 100).toFixed(0)}%
2489
+ \u5BA1\u8BA1\u610F\u89C1: ${lastReview.feedback}
2490
+ \u5177\u4F53\u95EE\u9898:
2491
+ ${lastReview.issues.map((issue) => `- ${issue}`).join("\n")}
2492
+ \u6539\u8FDB\u5EFA\u8BAE:
2493
+ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
2494
+
2495
+ \u8BF7\u9488\u5BF9\u4EE5\u4E0A\u95EE\u9898\u91CD\u65B0\u751F\u6210\u66F4\u8D34\u5207\u3001\u66F4\u51C6\u786E\u7684\u6D4B\u8BD5\u7528\u4F8B\u3002`;
2496
+ const generateResponse = await generatorProvider.generateTest({
2497
+ type: testType,
2652
2498
  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
2499
+ context: i === 0 ? sourceCode : `${sourceCode}
2500
+
2501
+ --- \u5BA1\u8BA1\u53CD\u9988 ---
2502
+ ${generationPrompt}`,
2503
+ framework: framework || void 0,
2504
+ analysis
2680
2505
  });
2506
+ currentCode = generateResponse.code;
2507
+ currentDescription = generateResponse.description;
2508
+ currentConfidence = generateResponse.confidence;
2509
+ const reviewRequest = {
2510
+ target: targetPath,
2511
+ sourceCode,
2512
+ analysis,
2513
+ testCode: currentCode,
2514
+ testType,
2515
+ generationDescription: currentDescription
2516
+ };
2517
+ lastReview = await reviewerProvider.reviewTest(reviewRequest);
2518
+ approved = lastReview.approved && lastReview.score >= REVIEW_THRESHOLD;
2519
+ if (approved) {
2520
+ break;
2521
+ }
2681
2522
  }
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
2523
  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
2524
+ code: currentCode,
2525
+ description: currentDescription,
2526
+ confidence: currentConfidence,
2527
+ approved,
2528
+ attempts,
2529
+ reviewScore: lastReview?.score ?? 0,
2530
+ reviewFeedback: lastReview?.feedback ?? "",
2531
+ reviewIssues: lastReview?.issues ?? [],
2532
+ reviewSuggestions: lastReview?.suggestions ?? []
2741
2533
  };
2742
- return { code, reviewEntry };
2743
2534
  }
2744
- function displayProjectInfo(info) {
2745
- console.log(chalk3.cyan("\n \u9879\u76EE\u68C0\u6D4B\u7ED3\u679C:"));
2535
+ function printReviewReport(report) {
2536
+ if (report.length === 0) return;
2537
+ const approved = report.filter((r) => r.approved);
2538
+ const failed = report.filter((r) => !r.approved);
2539
+ console.log();
2540
+ console.log(chalk3.cyan(" \u5BA1\u8BA1\u62A5\u544A:"));
2746
2541
  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}`);
2542
+ for (const entry of approved) {
2543
+ 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
2544
  }
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);
2545
+ for (const entry of failed) {
2546
+ console.log(` ${chalk3.red("\u2717")} ${chalk3.gray(entry.target)} ${chalk3.red(`(${(entry.score * 100).toFixed(0)}%)`)}`);
2547
+ if (entry.issues.length > 0) {
2548
+ for (const issue of entry.issues.slice(0, 3)) {
2549
+ console.log(chalk3.gray(` - ${issue}`));
2793
2550
  }
2794
2551
  }
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)}`);
2552
+ if (entry.feedback) {
2553
+ console.log(chalk3.gray(` \u53CD\u9988: ${entry.feedback}`));
2809
2554
  }
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
2555
  }
2826
2556
  console.log();
2827
2557
  }
2828
2558
 
2829
2559
  // 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
2560
  var TEST_TYPE_LABELS = {
2836
2561
  unit: "\u5355\u5143\u6D4B\u8BD5",
2837
2562
  component: "\u7EC4\u4EF6\u6D4B\u8BD5",
@@ -3169,18 +2894,29 @@ function displayCreateResult(createdFiles, skippedCount, usedAI) {
3169
2894
  import chalk5 from "chalk";
3170
2895
  import inquirer3 from "inquirer";
3171
2896
  import ora4 from "ora";
3172
- import fs9 from "fs";
3173
- import path11 from "path";
2897
+ import fs11 from "fs";
2898
+ import path12 from "path";
3174
2899
 
3175
2900
  // src/runners/vitest-runner.ts
3176
2901
  import { execFile } from "child_process";
3177
2902
  import path9 from "path";
2903
+ import fs9 from "fs";
2904
+ import os2 from "os";
2905
+ var isVerbose = () => process.env.QAT_VERBOSE === "true";
2906
+ function debug(label, ...args) {
2907
+ if (isVerbose()) {
2908
+ console.log(`\x1B[90m [debug:${label}]\x1B[0m`, ...args);
2909
+ }
2910
+ }
3178
2911
  async function runVitest(options) {
3179
2912
  const startTime = Date.now();
3180
2913
  const args = buildVitestArgs(options);
2914
+ debug("vitest", "\u547D\u4EE4\u53C2\u6570:", args.join(" "));
3181
2915
  try {
3182
2916
  const result = await execVitest(args);
3183
2917
  const endTime = Date.now();
2918
+ 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`);
2919
+ debug("vitest", "\u89E3\u6790\u65B9\u5F0F:", result.parseMethod);
3184
2920
  return {
3185
2921
  type: options.type,
3186
2922
  status: result.success ? "passed" : "failed",
@@ -3245,12 +2981,11 @@ function buildVitestArgs(options) {
3245
2981
  return args;
3246
2982
  }
3247
2983
  async function execVitest(args) {
3248
- const os2 = await import("os");
3249
- const fs15 = await import("fs");
3250
2984
  const tmpFile = path9.join(os2.tmpdir(), `qat-vitest-result-${Date.now()}.json`);
3251
2985
  const argsWithOutput = [...args, "--outputFile", tmpFile];
3252
2986
  return new Promise((resolve, reject) => {
3253
2987
  const npx = process.platform === "win32" ? "npx.cmd" : "npx";
2988
+ debug("vitest", "\u6267\u884C\u547D\u4EE4:", npx, argsWithOutput.join(" "));
3254
2989
  const child = execFile(npx, argsWithOutput, {
3255
2990
  cwd: process.cwd(),
3256
2991
  env: { ...process.env, FORCE_COLOR: "0", NO_COLOR: "1" },
@@ -3258,85 +2993,113 @@ async function execVitest(args) {
3258
2993
  shell: true
3259
2994
  }, (error, stdout, stderr) => {
3260
2995
  const rawOutput = stdout || stderr || "";
2996
+ const exitCode = error && "code" in error ? error.code : 0;
2997
+ debug("vitest", `\u9000\u51FA\u7801: ${exitCode}`);
2998
+ debug("vitest", `stdout \u957F\u5EA6: ${stdout?.length || 0}, stderr \u957F\u5EA6: ${stderr?.length || 0}`);
3261
2999
  let jsonResult = null;
3262
3000
  try {
3263
- if (fs15.existsSync(tmpFile)) {
3264
- jsonResult = fs15.readFileSync(tmpFile, "utf-8");
3265
- fs15.unlinkSync(tmpFile);
3001
+ if (fs9.existsSync(tmpFile)) {
3002
+ jsonResult = fs9.readFileSync(tmpFile, "utf-8");
3003
+ debug("vitest", `\u4ECE\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5230 JSON (${jsonResult.length} \u5B57\u7B26)`);
3004
+ debug("vitest", "JSON \u524D 500 \u5B57\u7B26:", jsonResult.substring(0, 500));
3005
+ fs9.unlinkSync(tmpFile);
3006
+ } else {
3007
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u4E0D\u5B58\u5728:", tmpFile);
3266
3008
  }
3267
- } catch {
3009
+ } catch (e) {
3010
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6\u8BFB\u53D6\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3268
3011
  }
3269
3012
  if (jsonResult) {
3270
3013
  try {
3271
- const parsed = parseVitestJSONResult(jsonResult);
3272
- resolve({ ...parsed, rawOutput });
3014
+ const parsed = parseVitestJSON(jsonResult);
3015
+ debug("vitest", "\u4ECE\u4E34\u65F6\u6587\u4EF6\u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3016
+ resolve({ ...parsed, rawOutput, parseMethod: "outputFile-JSON" });
3273
3017
  return;
3274
- } catch {
3018
+ } catch (e) {
3019
+ debug("vitest", "\u4E34\u65F6\u6587\u4EF6 JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3275
3020
  }
3276
3021
  }
3022
+ debug("vitest", "\u5C1D\u8BD5\u4ECE stdout \u63D0\u53D6 JSON...");
3277
3023
  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
- }
3024
+ const parsed = parseFromStdout(rawOutput);
3025
+ debug("vitest", "\u4ECE stdout \u89E3\u6790\u6210\u529F:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3026
+ resolve({ ...parsed, rawOutput, parseMethod: "stdout-JSON" });
3027
+ return;
3028
+ } catch (e) {
3029
+ debug("vitest", "stdout JSON \u89E3\u6790\u5931\u8D25:", e instanceof Error ? e.message : String(e));
3030
+ }
3031
+ debug("vitest", "\u5C1D\u8BD5\u4ECE\u6587\u672C\u8F93\u51FA\u89E3\u6790...");
3032
+ if (rawOutput) {
3033
+ const parsed = parseVitestTextOutput(rawOutput, !!error);
3034
+ debug("vitest", "\u6587\u672C\u89E3\u6790\u7ED3\u679C:", parsed.suites.length, "\u4E2A\u5957\u4EF6");
3035
+ resolve({ ...parsed, rawOutput, parseMethod: "text-fallback" });
3036
+ return;
3037
+ }
3038
+ if (error && error.message.includes("ENOENT")) {
3039
+ reject(new Error("\u672A\u627E\u5230 vitest\uFF0C\u8BF7\u786E\u4FDD\u5DF2\u5B89\u88C5: npm install -D vitest"));
3040
+ return;
3288
3041
  }
3042
+ debug("vitest", "\u6240\u6709\u89E3\u6790\u65B9\u5F0F\u5747\u5931\u8D25\uFF0C\u8FD4\u56DE\u7A7A\u7ED3\u679C");
3043
+ resolve({ success: !error, suites: [], rawOutput, parseMethod: "none" });
3289
3044
  });
3290
3045
  child.on("error", (err) => {
3291
3046
  try {
3292
- fs15.unlinkSync(tmpFile);
3047
+ fs9.unlinkSync(tmpFile);
3293
3048
  } catch {
3294
3049
  }
3295
3050
  reject(new Error(`Vitest \u6267\u884C\u5931\u8D25: ${err.message}`));
3296
3051
  });
3297
3052
  });
3298
3053
  }
3299
- function parseVitestJSONResult(jsonStr) {
3054
+ function parseVitestJSON(jsonStr) {
3300
3055
  const data = JSON.parse(jsonStr);
3301
3056
  const suites = [];
3057
+ debug("vitest-json", "JSON \u9876\u5C42\u5B57\u6BB5:", Object.keys(data).join(", "));
3302
3058
  if (data.testResults && Array.isArray(data.testResults)) {
3059
+ debug("vitest-json", `testResults \u6570\u91CF: ${data.testResults.length}`);
3303
3060
  for (const fileResult of data.testResults) {
3061
+ const suiteTests = parseTestResults(fileResult);
3062
+ suites.push({
3063
+ name: path9.basename(fileResult.name || "unknown"),
3064
+ file: fileResult.name || "unknown",
3065
+ type: "unit",
3066
+ status: mapVitestStatus(fileResult.status),
3067
+ duration: fileResult.duration || 0,
3068
+ tests: suiteTests
3069
+ });
3070
+ }
3071
+ }
3072
+ if (suites.length === 0 && data.numTotalTests !== void 0) {
3073
+ debug("vitest-json", `\u4F7F\u7528\u6C47\u603B\u683C\u5F0F: total=${data.numTotalTests}, passed=${data.numPassedTests}`);
3074
+ suites.push({
3075
+ name: "Vitest Results",
3076
+ file: "unknown",
3077
+ type: "unit",
3078
+ status: data.numFailedTests > 0 ? "failed" : "passed",
3079
+ duration: 0,
3080
+ tests: buildTestsFromSummary(data)
3081
+ });
3082
+ }
3083
+ if (suites.length === 0 && data.suites && Array.isArray(data.suites)) {
3084
+ debug("vitest-json", `suites \u6570\u91CF: ${data.suites.length}`);
3085
+ for (const suiteData of data.suites) {
3304
3086
  const suiteTests = [];
3305
- const assertions = fileResult.assertionResults || fileResult.tests || [];
3306
- for (const assertion of assertions) {
3087
+ for (const test of suiteData.tests || []) {
3307
3088
  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,
3089
+ name: test.name || test.title || "unknown",
3090
+ file: test.file || suiteData.file || "unknown",
3091
+ status: mapVitestStatus(test.status || test.result?.status),
3092
+ duration: test.duration || test.result?.duration || 0,
3093
+ error: test.result?.errors?.[0] ? { message: test.result.errors[0].message || String(test.result.errors[0]) } : void 0,
3313
3094
  retries: 0
3314
3095
  });
3315
3096
  }
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
3097
  suites.push({
3335
- name: path9.basename(fileResult.name || "unknown"),
3336
- file: fileResult.name || "unknown",
3098
+ name: suiteData.name || "unknown",
3099
+ file: suiteData.file || "unknown",
3337
3100
  type: "unit",
3338
- status: mapVitestStatus(fileResult.status),
3339
- duration: fileResult.duration || 0,
3101
+ status: suiteTests.some((t) => t.status === "failed") ? "failed" : "passed",
3102
+ duration: 0,
3340
3103
  tests: suiteTests
3341
3104
  });
3342
3105
  }
@@ -3345,58 +3108,101 @@ function parseVitestJSONResult(jsonStr) {
3345
3108
  if (data.coverageMap) {
3346
3109
  coverage = extractCoverage(data.coverageMap);
3347
3110
  }
3348
- const success = data.success !== false && data.numFailedTests === void 0 ? suites.every((s) => s.status !== "failed") : (data.numFailedTests || 0) === 0;
3111
+ if (!coverage && data.coverage && typeof data.coverage === "object") {
3112
+ const cov = data.coverage;
3113
+ const totals = cov.totals || cov;
3114
+ const getVal = (key) => {
3115
+ const v = totals[key];
3116
+ return typeof v === "number" ? v : typeof v === "object" && v !== null && "pct" in v ? v.pct / 100 : 0;
3117
+ };
3118
+ coverage = {
3119
+ lines: getVal("lines"),
3120
+ statements: getVal("statements"),
3121
+ functions: getVal("functions"),
3122
+ branches: getVal("branches")
3123
+ };
3124
+ }
3125
+ const success = data.success !== false ? data.numFailedTests !== void 0 ? data.numFailedTests === 0 : suites.every((s) => s.status !== "failed") : false;
3349
3126
  return { success, suites, coverage };
3350
3127
  }
3351
- function parseVitestJSONOutput(output) {
3352
- const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3353
- if (!jsonMatch) {
3354
- return parseVitestTextOutput(output, false);
3128
+ function parseTestResults(fileResult) {
3129
+ const tests = [];
3130
+ const assertions = fileResult.assertionResults || fileResult.tests || [];
3131
+ for (const assertion of assertions) {
3132
+ tests.push({
3133
+ name: assertion.title || assertion.fullName || assertion.name || "unknown",
3134
+ file: fileResult.name || "unknown",
3135
+ status: mapVitestStatus(assertion.status),
3136
+ duration: assertion.duration || 0,
3137
+ error: assertion.failureMessages?.length ? { message: assertion.failureMessages[0] } : assertion.failureMessage ? { message: assertion.failureMessage } : void 0,
3138
+ retries: 0
3139
+ });
3355
3140
  }
3356
- try {
3141
+ if (tests.length === 0 && fileResult.numPassingTests !== void 0) {
3142
+ tests.push(...buildTestsFromSummary(fileResult));
3143
+ }
3144
+ return tests;
3145
+ }
3146
+ function buildTestsFromSummary(data) {
3147
+ const tests = [];
3148
+ const counts = [
3149
+ [data.numPassedTests || 0, "passed"],
3150
+ [data.numFailedTests || 0, "failed"],
3151
+ [data.numPendingTests || 0, "skipped"]
3152
+ ];
3153
+ for (const [n, s] of counts) {
3154
+ for (let i = 0; i < n; i++) {
3155
+ tests.push({
3156
+ name: `${s} test ${i + 1}`,
3157
+ file: data.name || "unknown",
3158
+ status: s,
3159
+ duration: 0,
3160
+ retries: 0
3161
+ });
3162
+ }
3163
+ }
3164
+ return tests;
3165
+ }
3166
+ function parseFromStdout(output) {
3167
+ const jsonMatch = output.match(/\{[\s\S]*"testResults"[\s\S]*\}/);
3168
+ if (jsonMatch) {
3357
3169
  const data = JSON.parse(jsonMatch[0]);
3358
3170
  const suites = [];
3359
3171
  if (data.testResults && Array.isArray(data.testResults)) {
3360
3172
  for (const fileResult of data.testResults) {
3361
- const suite = {
3362
- name: path9.basename(fileResult.name || fileResult.assertionResults?.[0]?.ancestorTitles?.[0] || "unknown"),
3173
+ suites.push({
3174
+ name: path9.basename(fileResult.name || "unknown"),
3363
3175
  file: fileResult.name || "unknown",
3364
3176
  type: "unit",
3365
3177
  status: mapVitestStatus(fileResult.status),
3366
3178
  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);
3179
+ tests: parseTestResults(fileResult)
3180
+ });
3377
3181
  }
3378
3182
  }
3379
- let coverage;
3380
- if (data.coverageMap) {
3381
- coverage = extractCoverage(data.coverageMap);
3382
- }
3383
3183
  const success = data.success !== false && suites.every((s) => s.status !== "failed");
3184
+ let coverage;
3185
+ if (data.coverageMap) coverage = extractCoverage(data.coverageMap);
3384
3186
  return { success, suites, coverage };
3385
- } catch {
3386
- return parseVitestTextOutput(output, false);
3387
3187
  }
3188
+ const anyJsonMatch = output.match(/\{[\s\S]*"numTotalTests"[\s\S]*\}/);
3189
+ if (anyJsonMatch) {
3190
+ return parseVitestJSON(anyJsonMatch[0]);
3191
+ }
3192
+ throw new Error("stdout \u4E2D\u672A\u627E\u5230\u6709\u6548 JSON");
3388
3193
  }
3389
3194
  function parseVitestTextOutput(output, hasError) {
3390
3195
  const suites = [];
3196
+ debug("vitest-text", "\u6587\u672C\u8F93\u51FA\u524D 1000 \u5B57\u7B26:", output.substring(0, 1e3));
3197
+ const suiteRegex = /[✓✗×✕]\s+(.+\.test\.(ts|js)|.+\.spec\.(ts|js))\s*\((\d+)[^)]*\)/i;
3198
+ const lines = output.split("\n");
3391
3199
  let totalPassed = 0;
3392
3200
  let totalFailed = 0;
3393
- const suiteRegex = /[✓✗×]\s+(.+\.test\.ts|.+\.spec\.ts)\s*\((\d+)\s+test/i;
3394
- const lines = output.split("\n");
3395
3201
  for (const line of lines) {
3396
3202
  const match = line.match(suiteRegex);
3397
3203
  if (match) {
3398
3204
  const file = match[1];
3399
- const testCount = parseInt(match[2], 10);
3205
+ const testCount = parseInt(match[4], 10);
3400
3206
  const isPassed = line.includes("\u2713");
3401
3207
  if (isPassed) totalPassed += testCount;
3402
3208
  else totalFailed += testCount;
@@ -3408,23 +3214,47 @@ function parseVitestTextOutput(output, hasError) {
3408
3214
  duration: 0,
3409
3215
  tests: Array.from({ length: testCount }, (_, i) => ({
3410
3216
  name: `test ${i + 1}`,
3411
- file,
3412
- status: isPassed ? "passed" : "failed",
3217
+ file,
3218
+ status: isPassed ? "passed" : "failed",
3219
+ duration: 0,
3220
+ retries: 0
3221
+ }))
3222
+ });
3223
+ }
3224
+ }
3225
+ if (suites.length === 0) {
3226
+ const summaryMatch = output.match(/Tests\s+(\d+)\s+(passed|failed)/i);
3227
+ if (summaryMatch) {
3228
+ const count = parseInt(summaryMatch[1], 10);
3229
+ const status = summaryMatch[2].toLowerCase() === "passed" ? "passed" : "failed";
3230
+ suites.push({
3231
+ name: "Vitest Summary",
3232
+ file: "unknown",
3233
+ type: "unit",
3234
+ status,
3235
+ duration: 0,
3236
+ tests: Array.from({ length: count }, (_, i) => ({
3237
+ name: `test ${i + 1}`,
3238
+ file: "unknown",
3239
+ status,
3413
3240
  duration: 0,
3414
3241
  retries: 0
3415
3242
  }))
3416
3243
  });
3417
3244
  }
3418
3245
  }
3246
+ debug("vitest-text", `\u89E3\u6790\u5230 ${suites.length} \u4E2A\u5957\u4EF6, ${totalPassed} \u901A\u8FC7, ${totalFailed} \u5931\u8D25`);
3419
3247
  return {
3420
3248
  success: !hasError || totalFailed === 0,
3421
3249
  suites
3422
3250
  };
3423
3251
  }
3424
3252
  function mapVitestStatus(status) {
3253
+ if (!status) return "pending";
3425
3254
  switch (status) {
3426
3255
  case "passed":
3427
3256
  case "pass":
3257
+ case "done":
3428
3258
  return "passed";
3429
3259
  case "failed":
3430
3260
  case "fail":
@@ -3432,6 +3262,7 @@ function mapVitestStatus(status) {
3432
3262
  case "skipped":
3433
3263
  case "skip":
3434
3264
  case "pending":
3265
+ case "todo":
3435
3266
  return "skipped";
3436
3267
  default:
3437
3268
  return "pending";
@@ -3910,9 +3741,226 @@ function calculateAverageMetrics(results) {
3910
3741
  };
3911
3742
  }
3912
3743
 
3744
+ // src/services/reporter.ts
3745
+ import fs10 from "fs";
3746
+ import path11 from "path";
3747
+ function aggregateResults(results) {
3748
+ const summary = {
3749
+ total: 0,
3750
+ passed: 0,
3751
+ failed: 0,
3752
+ skipped: 0,
3753
+ pending: 0
3754
+ };
3755
+ const byType = {};
3756
+ let coverage;
3757
+ for (const result of results) {
3758
+ const typeKey = result.type;
3759
+ if (!byType[typeKey]) {
3760
+ byType[typeKey] = { total: 0, passed: 0, failed: 0, skipped: 0 };
3761
+ }
3762
+ for (const suite of result.suites) {
3763
+ for (const test of suite.tests) {
3764
+ summary.total++;
3765
+ byType[typeKey].total++;
3766
+ if (test.status === "passed") {
3767
+ summary.passed++;
3768
+ byType[typeKey].passed++;
3769
+ } else if (test.status === "failed") {
3770
+ summary.failed++;
3771
+ byType[typeKey].failed++;
3772
+ } else if (test.status === "skipped") {
3773
+ summary.skipped++;
3774
+ byType[typeKey].skipped++;
3775
+ } else {
3776
+ summary.pending++;
3777
+ }
3778
+ }
3779
+ }
3780
+ if (result.coverage) {
3781
+ if (!coverage) {
3782
+ coverage = { ...result.coverage };
3783
+ } else {
3784
+ coverage.lines = Math.max(coverage.lines, result.coverage.lines);
3785
+ coverage.statements = Math.max(coverage.statements, result.coverage.statements);
3786
+ coverage.functions = Math.max(coverage.functions, result.coverage.functions);
3787
+ coverage.branches = Math.max(coverage.branches, result.coverage.branches);
3788
+ }
3789
+ }
3790
+ }
3791
+ const totalDuration = results.reduce((sum, r) => sum + r.duration, 0);
3792
+ return {
3793
+ timestamp: Date.now(),
3794
+ duration: totalDuration,
3795
+ results,
3796
+ summary,
3797
+ byType,
3798
+ coverage: coverage?.lines ? coverage : void 0
3799
+ };
3800
+ }
3801
+ function formatDuration(ms) {
3802
+ if (ms < 1e3) return `${ms}ms`;
3803
+ if (ms < 6e4) return `${(ms / 1e3).toFixed(2)}s`;
3804
+ const minutes = Math.floor(ms / 6e4);
3805
+ const seconds = (ms % 6e4 / 1e3).toFixed(0);
3806
+ return `${minutes}m ${seconds}s`;
3807
+ }
3808
+ function formatTimestamp(ts) {
3809
+ return new Date(ts).toLocaleString("zh-CN", {
3810
+ year: "numeric",
3811
+ month: "2-digit",
3812
+ day: "2-digit",
3813
+ hour: "2-digit",
3814
+ minute: "2-digit",
3815
+ second: "2-digit"
3816
+ });
3817
+ }
3818
+ function pct(value) {
3819
+ return `${(value * 100).toFixed(1)}%`;
3820
+ }
3821
+ function renderCoverageMD(coverage) {
3822
+ return `
3823
+ ### \u8986\u76D6\u7387
3824
+
3825
+ | \u6307\u6807 | \u8986\u76D6\u7387 | \u8FDB\u5EA6 |
3826
+ |------|--------|------|
3827
+ | \u8BED\u53E5 (Statements) | ${pct(coverage.statements)} | ${renderProgressBar(coverage.statements)} |
3828
+ | \u5206\u652F (Branches) | ${pct(coverage.branches)} | ${renderProgressBar(coverage.branches)} |
3829
+ | \u51FD\u6570 (Functions) | ${pct(coverage.functions)} | ${renderProgressBar(coverage.functions)} |
3830
+ | \u884C (Lines) | ${pct(coverage.lines)} | ${renderProgressBar(coverage.lines)} |
3831
+ `;
3832
+ }
3833
+ function renderProgressBar(value) {
3834
+ const filled = Math.round(value * 10);
3835
+ const empty = 10 - filled;
3836
+ return `${"\u2588".repeat(filled)}${"\u2591".repeat(empty)}`;
3837
+ }
3838
+ var TYPE_LABELS = {
3839
+ unit: "\u5355\u5143\u6D4B\u8BD5",
3840
+ component: "\u7EC4\u4EF6\u6D4B\u8BD5",
3841
+ e2e: "E2E \u6D4B\u8BD5",
3842
+ api: "API \u6D4B\u8BD5",
3843
+ visual: "\u89C6\u89C9\u56DE\u5F52\u6D4B\u8BD5",
3844
+ performance: "\u6027\u80FD\u6D4B\u8BD5"
3845
+ };
3846
+ function generateMDReport(data) {
3847
+ const passRate = data.summary.total > 0 ? (data.summary.passed / data.summary.total * 100).toFixed(1) : "0";
3848
+ const rateIcon = parseFloat(passRate) >= 80 ? "\u2705" : parseFloat(passRate) >= 50 ? "\u26A0\uFE0F" : "\u274C";
3849
+ const lines = [];
3850
+ lines.push(`# QAT \u6D4B\u8BD5\u62A5\u544A`);
3851
+ lines.push("");
3852
+ lines.push(`> \u751F\u6210\u65F6\u95F4: ${formatTimestamp(data.timestamp)} | \u603B\u8017\u65F6: ${formatDuration(data.duration)}`);
3853
+ lines.push("");
3854
+ lines.push(`## \u603B\u89C8`);
3855
+ lines.push("");
3856
+ lines.push(`| \u6307\u6807 | \u6570\u503C |`);
3857
+ lines.push(`|------|------|`);
3858
+ lines.push(`| \u901A\u8FC7\u7387 | ${rateIcon} **${passRate}%** |`);
3859
+ lines.push(`| \u603B\u7528\u4F8B | ${data.summary.total} |`);
3860
+ lines.push(`| \u2705 \u901A\u8FC7 | ${data.summary.passed} |`);
3861
+ if (data.summary.failed > 0) lines.push(`| \u274C \u5931\u8D25 | ${data.summary.failed} |`);
3862
+ if (data.summary.skipped > 0) lines.push(`| \u23ED\uFE0F \u8DF3\u8FC7 | ${data.summary.skipped} |`);
3863
+ if (data.summary.pending > 0) lines.push(`| \u23F3 \u5F85\u5B9A | ${data.summary.pending} |`);
3864
+ lines.push(`| \u23F1\uFE0F \u8017\u65F6 | ${formatDuration(data.duration)} |`);
3865
+ lines.push("");
3866
+ if (Object.keys(data.byType).length > 0) {
3867
+ lines.push(`## \u6309\u7C7B\u578B\u7EDF\u8BA1`);
3868
+ lines.push("");
3869
+ lines.push(`| \u7C7B\u578B | \u901A\u8FC7 | \u5931\u8D25 | \u8DF3\u8FC7 | \u603B\u8BA1 | \u901A\u8FC7\u7387 |`);
3870
+ lines.push(`|------|------|------|------|------|--------|`);
3871
+ for (const [type, stats] of Object.entries(data.byType)) {
3872
+ const label = TYPE_LABELS[type] || type;
3873
+ const rate = stats.total > 0 ? (stats.passed / stats.total * 100).toFixed(0) + "%" : "-";
3874
+ lines.push(`| ${label} | ${stats.passed} | ${stats.failed} | ${stats.skipped} | ${stats.total} | ${rate} |`);
3875
+ }
3876
+ lines.push("");
3877
+ }
3878
+ if (data.coverage) {
3879
+ lines.push(renderCoverageMD(data.coverage));
3880
+ lines.push("");
3881
+ }
3882
+ lines.push(`## \u6D4B\u8BD5\u8BE6\u60C5`);
3883
+ lines.push("");
3884
+ for (const result of data.results) {
3885
+ const typeLabel = TYPE_LABELS[result.type] || result.type;
3886
+ const statusIcon = result.status === "passed" ? "\u2705" : result.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
3887
+ lines.push(`### ${statusIcon} ${typeLabel}`);
3888
+ lines.push("");
3889
+ if (result.suites.length === 0) {
3890
+ lines.push(`*\u65E0\u6D4B\u8BD5\u7ED3\u679C*`);
3891
+ lines.push("");
3892
+ continue;
3893
+ }
3894
+ for (const suite of result.suites) {
3895
+ const suiteIcon = suite.status === "passed" ? "\u2705" : suite.status === "failed" ? "\u274C" : "\u26A0\uFE0F";
3896
+ lines.push(`#### ${suiteIcon} ${suite.name}`);
3897
+ lines.push("");
3898
+ lines.push(`- \u6587\u4EF6: \`${suite.file}\``);
3899
+ lines.push(`- \u8017\u65F6: ${formatDuration(suite.duration)}`);
3900
+ lines.push("");
3901
+ if (suite.tests.length > 0) {
3902
+ lines.push(`| \u72B6\u6001 | \u6D4B\u8BD5\u540D\u79F0 | \u8017\u65F6 |`);
3903
+ lines.push(`|------|----------|------|`);
3904
+ for (const test of suite.tests) {
3905
+ const testIcon = test.status === "passed" ? "\u2705" : test.status === "failed" ? "\u274C" : test.status === "skipped" ? "\u23ED\uFE0F" : "\u23F3";
3906
+ const name = test.error ? `**${test.name}**` : test.name;
3907
+ lines.push(`| ${testIcon} | ${name} | ${formatDuration(test.duration)} |`);
3908
+ }
3909
+ lines.push("");
3910
+ }
3911
+ const failedTests = suite.tests.filter((t) => t.status === "failed" && t.error);
3912
+ if (failedTests.length > 0) {
3913
+ lines.push(`<details>`);
3914
+ lines.push(`<summary>\u274C \u5931\u8D25\u8BE6\u60C5 (${failedTests.length})</summary>`);
3915
+ lines.push("");
3916
+ for (const test of failedTests) {
3917
+ lines.push(`**${test.name}**`);
3918
+ lines.push("```");
3919
+ lines.push(test.error.message);
3920
+ if (test.error.stack) {
3921
+ lines.push(test.error.stack);
3922
+ }
3923
+ if (test.error.expected && test.error.actual) {
3924
+ lines.push(`Expected: ${test.error.expected}`);
3925
+ lines.push(`Actual: ${test.error.actual}`);
3926
+ }
3927
+ lines.push("```");
3928
+ lines.push("");
3929
+ }
3930
+ lines.push(`</details>`);
3931
+ lines.push("");
3932
+ }
3933
+ }
3934
+ }
3935
+ lines.push("---");
3936
+ lines.push("");
3937
+ lines.push(`*\u7531 QAT \u81EA\u52A8\u5316\u6D4B\u8BD5\u5DE5\u5177\u751F\u6210 | ${formatTimestamp(data.timestamp)}*`);
3938
+ return lines.join("\n");
3939
+ }
3940
+ function writeReportToDisk(data, outputDir) {
3941
+ const md = generateMDReport(data);
3942
+ const dir = path11.resolve(outputDir);
3943
+ if (!fs10.existsSync(dir)) {
3944
+ fs10.mkdirSync(dir, { recursive: true });
3945
+ }
3946
+ const mdPath = path11.join(dir, "report.md");
3947
+ fs10.writeFileSync(mdPath, md, "utf-8");
3948
+ const jsonPath = path11.join(dir, "report.json");
3949
+ fs10.writeFileSync(jsonPath, JSON.stringify(data, null, 2), "utf-8");
3950
+ return mdPath;
3951
+ }
3952
+
3913
3953
  // src/commands/run.ts
3914
3954
  var RESULTS_DIR = ".qat-results";
3915
3955
  var SERVER_REQUIRED_TYPES = ["e2e", "visual", "performance"];
3956
+ var TYPE_DEPENDENCIES = {
3957
+ unit: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
3958
+ component: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest @vue/test-utils happy-dom" },
3959
+ api: { pkg: "vitest", runner: "Vitest", installCmd: "npm install -D vitest" },
3960
+ e2e: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
3961
+ visual: { pkg: "@playwright/test", runner: "Playwright", installCmd: "npm install -D @playwright/test && npx playwright install" },
3962
+ performance: { pkg: "lighthouse", runner: "Lighthouse", installCmd: "npm install -D lighthouse" }
3963
+ };
3916
3964
  var TYPE_RUNNERS = {
3917
3965
  unit: "Vitest",
3918
3966
  component: "Vitest",
@@ -3921,7 +3969,7 @@ var TYPE_RUNNERS = {
3921
3969
  visual: "Playwright",
3922
3970
  performance: "Lighthouse"
3923
3971
  };
3924
- var TYPE_LABELS = {
3972
+ var TYPE_LABELS2 = {
3925
3973
  unit: "\u5355\u5143\u6D4B\u8BD5",
3926
3974
  component: "\u7EC4\u4EF6\u6D4B\u8BD5",
3927
3975
  e2e: "E2E\u6D4B\u8BD5",
@@ -3957,6 +4005,61 @@ async function executeRun(options) {
3957
4005
  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
4006
  return;
3959
4007
  }
4008
+ const missingDeps = checkTestDependencies(typesToRun);
4009
+ if (missingDeps.length > 0) {
4010
+ console.log(chalk5.yellow("\n \u26A0 \u4EE5\u4E0B\u6D4B\u8BD5\u6846\u67B6\u4F9D\u8D56\u672A\u5B89\u88C5:\n"));
4011
+ for (const dep of missingDeps) {
4012
+ console.log(chalk5.white(` ${dep.runner} (${dep.pkg})`));
4013
+ console.log(chalk5.gray(` \u5B89\u88C5\u547D\u4EE4: ${chalk5.cyan(dep.installCmd)}`));
4014
+ }
4015
+ console.log();
4016
+ const { action } = await inquirer3.prompt([
4017
+ {
4018
+ type: "list",
4019
+ name: "action",
4020
+ message: "\u5982\u4F55\u5904\u7406\uFF1F",
4021
+ choices: [
4022
+ { name: "\u81EA\u52A8\u5B89\u88C5\u7F3A\u5931\u4F9D\u8D56", value: "install" },
4023
+ { name: "\u8DF3\u8FC7\u672A\u5B89\u88C5\u7684\u6D4B\u8BD5\u7C7B\u578B", value: "skip" },
4024
+ { name: "\u53D6\u6D88\u8FD0\u884C", value: "cancel" }
4025
+ ],
4026
+ default: "install"
4027
+ }
4028
+ ]);
4029
+ if (action === "cancel") {
4030
+ console.log(chalk5.gray("\n \u5DF2\u53D6\u6D88\u8FD0\u884C\n"));
4031
+ return;
4032
+ }
4033
+ if (action === "install") {
4034
+ const installed = await installTestDependencies(missingDeps);
4035
+ if (!installed) {
4036
+ console.log(chalk5.yellow(" \u90E8\u5206\u4F9D\u8D56\u5B89\u88C5\u5931\u8D25\uFF0C\u5C06\u8DF3\u8FC7\u5BF9\u5E94\u7684\u6D4B\u8BD5\u7C7B\u578B"));
4037
+ }
4038
+ const stillMissing = checkTestDependencies(typesToRun);
4039
+ if (stillMissing.length > 0) {
4040
+ const missingRunners = new Set(stillMissing.map((d) => d.pkg));
4041
+ typesToRun = typesToRun.filter((t) => {
4042
+ const dep = TYPE_DEPENDENCIES[t];
4043
+ return !dep || !missingRunners.has(dep.pkg);
4044
+ });
4045
+ if (typesToRun.length === 0) {
4046
+ console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\uFF08\u4F9D\u8D56\u672A\u5B89\u88C5\uFF09\n"));
4047
+ return;
4048
+ }
4049
+ }
4050
+ }
4051
+ if (action === "skip") {
4052
+ const missingRunners = new Set(missingDeps.map((d) => d.pkg));
4053
+ typesToRun = typesToRun.filter((t) => {
4054
+ const dep = TYPE_DEPENDENCIES[t];
4055
+ return !dep || !missingRunners.has(dep.pkg);
4056
+ });
4057
+ if (typesToRun.length === 0) {
4058
+ console.log(chalk5.yellow("\n \u6CA1\u6709\u53EF\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B\n"));
4059
+ return;
4060
+ }
4061
+ }
4062
+ }
3960
4063
  const serverNeededTypes = typesToRun.filter((t) => SERVER_REQUIRED_TYPES.includes(t));
3961
4064
  if (serverNeededTypes.length > 0) {
3962
4065
  const serverOk = await checkDevServer(config);
@@ -3965,7 +4068,7 @@ async function executeRun(options) {
3965
4068
  {
3966
4069
  type: "list",
3967
4070
  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`,
4071
+ 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
4072
  choices: [
3970
4073
  { name: "\u81EA\u52A8\u542F\u52A8 dev server \u5E76\u8FD0\u884C", value: "start" },
3971
4074
  { name: "\u8DF3\u8FC7\u8FD9\u4E9B\u6D4B\u8BD5\u7C7B\u578B\uFF0C\u4EC5\u8FD0\u884C\u5176\u4ED6\u6D4B\u8BD5", value: "skip" },
@@ -4046,7 +4149,7 @@ async function executeRun(options) {
4046
4149
  spinner.stop();
4047
4150
  } else {
4048
4151
  for (const testType of typesToRun) {
4049
- const label = TYPE_LABELS[testType] || testType;
4152
+ const label = TYPE_LABELS2[testType] || testType;
4050
4153
  const spinner = ora4(`\u6B63\u5728\u8FD0\u884C ${label}...`).start();
4051
4154
  try {
4052
4155
  const result = await runTestType(testType, config, options);
@@ -4058,14 +4161,25 @@ async function executeRun(options) {
4058
4161
  }
4059
4162
  }
4060
4163
  }
4061
- displaySummary(results, config);
4164
+ displayJestStyleResults(results);
4062
4165
  saveRunResults(results);
4166
+ const reportData = aggregateResults(results);
4167
+ const outputDir = config.report.outputDir || "qat-report";
4168
+ const reportPath = writeReportToDisk(reportData, outputDir);
4169
+ const relativePath = path12.relative(process.cwd(), reportPath);
4170
+ console.log(chalk5.gray(`
4171
+ \u62A5\u544A\u5DF2\u751F\u6210: ${chalk5.cyan(relativePath)}`));
4172
+ console.log();
4173
+ const hasFailures = results.some((r) => r.status === "failed");
4174
+ if (hasFailures && isAIAvailable(config.ai)) {
4175
+ await aiAnalyzeFailures(results, config.ai);
4176
+ }
4063
4177
  }
4064
4178
  function printDryRunCommands(types, options, config) {
4065
4179
  console.log();
4066
4180
  console.log(chalk5.cyan(" \u53EF\u6267\u884C\u7684\u6D4B\u8BD5\u547D\u4EE4:\n"));
4067
4181
  for (const testType of types) {
4068
- const label = TYPE_LABELS[testType];
4182
+ const label = TYPE_LABELS2[testType];
4069
4183
  const runner = TYPE_RUNNERS[testType];
4070
4184
  let cmd;
4071
4185
  switch (runner) {
@@ -4133,7 +4247,7 @@ async function determineTypesToRun(type, file, config) {
4133
4247
  name: "selectedTypes",
4134
4248
  message: "\u9009\u62E9\u8981\u8FD0\u884C\u7684\u6D4B\u8BD5\u7C7B\u578B (\u7A7A\u683C\u9009\u62E9/\u53D6\u6D88\uFF0C\u56DE\u8F66\u786E\u8BA4):",
4135
4249
  choices: enabledTypes.map((t) => ({
4136
- name: `${TYPE_LABELS[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
4250
+ name: `${TYPE_LABELS2[t]} (${chalk5.gray(TYPE_RUNNERS[t])})`,
4137
4251
  value: t,
4138
4252
  checked: true
4139
4253
  })),
@@ -4267,53 +4381,82 @@ function suiteStatusToSpinner(status) {
4267
4381
  return "warn";
4268
4382
  }
4269
4383
  }
4270
- async function displaySummary(results, config) {
4271
- let totalSuites = 0;
4384
+ function displayJestStyleResults(results) {
4385
+ console.log();
4386
+ for (const result of results) {
4387
+ const typeLabel = TYPE_LABELS2[result.type] || result.type;
4388
+ if (result.suites.length === 0) {
4389
+ console.log(chalk5.gray(` ${typeLabel}: \u65E0\u6D4B\u8BD5\u7ED3\u679C`));
4390
+ continue;
4391
+ }
4392
+ for (const suite of result.suites) {
4393
+ if (suite.tests.length === 0) continue;
4394
+ const suiteIcon = suite.status === "passed" ? chalk5.green("PASS") : chalk5.red("FAIL");
4395
+ console.log(` ${suiteIcon} ${chalk5.white(suite.name)} ${chalk5.gray(`(${formatDuration2(suite.duration)})`)}`);
4396
+ for (const test of suite.tests) {
4397
+ const icon = test.status === "passed" ? chalk5.green(" \u2713") : test.status === "failed" ? chalk5.red(" \u2715") : chalk5.yellow(" \u25CB");
4398
+ const name = test.status === "failed" ? chalk5.red(test.name) : test.name;
4399
+ console.log(` ${icon} ${name} ${chalk5.gray(formatDuration2(test.duration))}`);
4400
+ }
4401
+ }
4402
+ console.log();
4403
+ }
4272
4404
  let totalPassed = 0;
4273
4405
  let totalFailed = 0;
4274
4406
  let totalSkipped = 0;
4275
4407
  let totalDuration = 0;
4408
+ const typeStats = {};
4276
4409
  for (const result of results) {
4277
4410
  totalDuration += result.duration;
4411
+ if (!typeStats[result.type]) {
4412
+ typeStats[result.type] = { passed: 0, failed: 0, skipped: 0, duration: 0 };
4413
+ }
4414
+ typeStats[result.type].duration += result.duration;
4278
4415
  for (const suite of result.suites) {
4279
- totalSuites++;
4280
4416
  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++;
4417
+ if (test.status === "passed") {
4418
+ totalPassed++;
4419
+ typeStats[result.type].passed++;
4420
+ } else if (test.status === "failed") {
4421
+ totalFailed++;
4422
+ typeStats[result.type].failed++;
4423
+ } else if (test.status === "skipped") {
4424
+ totalSkipped++;
4425
+ typeStats[result.type].skipped++;
4426
+ }
4284
4427
  }
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"));
4428
+ }
4296
4429
  }
4430
+ const total = totalPassed + totalFailed + totalSkipped;
4431
+ 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"));
4432
+ console.log(chalk5.white(" \u6D4B\u8BD5\u7ED3\u679C\u6C47\u603B"));
4433
+ 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
4434
  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)})`);
4435
+ 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))}`);
4436
+ console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
4437
+ for (const [type, stats] of Object.entries(typeStats)) {
4438
+ const label = (TYPE_LABELS2[type] || type).padEnd(14);
4439
+ const typeTotal = stats.passed + stats.failed + stats.skipped;
4440
+ const rate = typeTotal > 0 ? (stats.passed / typeTotal * 100).toFixed(0) + "%" : "-";
4441
+ const rateColored = typeTotal > 0 && stats.passed === typeTotal ? chalk5.green(rate.padStart(8)) : stats.failed > 0 ? chalk5.red(rate.padStart(8)) : rate.padStart(8);
4442
+ 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)}`);
4443
+ }
4444
+ console.log(` ${"\u2500".repeat(14)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(6)} ${"\u2500".repeat(8)} ${"\u2500".repeat(8)}`);
4445
+ const totalRate = total > 0 ? (totalPassed / total * 100).toFixed(0) + "%" : "-";
4446
+ const totalRateColored = total > 0 && totalFailed === 0 ? chalk5.green(totalRate.padStart(8)) : totalFailed > 0 ? chalk5.red(totalRate.padStart(8)) : totalRate.padStart(8);
4447
+ 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)}`);
4448
+ console.log();
4449
+ for (const result of results) {
4450
+ if (result.coverage) {
4451
+ const c = result.coverage;
4452
+ console.log(chalk5.cyan(" \u8986\u76D6\u7387:"));
4453
+ console.log(` \u8BED\u53E5: ${coverageColor(c.statements)} \u5206\u652F: ${coverageColor(c.branches)} \u51FD\u6570: ${coverageColor(c.functions)} \u884C: ${coverageColor(c.lines)}`);
4454
+ console.log();
4311
4455
  }
4312
4456
  }
4313
4457
  for (const result of results) {
4314
4458
  if (result.type === "performance" && result.performance) {
4315
4459
  const p = result.performance;
4316
- console.log();
4317
4460
  console.log(chalk5.cyan(" \u6027\u80FD\u6307\u6807:"));
4318
4461
  console.log(` Performance: ${scoreColor(p.performance)} ${p.performance}/100`);
4319
4462
  console.log(` Accessibility: ${scoreColor(p.accessibility)} ${p.accessibility}/100`);
@@ -4322,6 +4465,7 @@ async function displaySummary(results, config) {
4322
4465
  if (p.lcp !== void 0) console.log(` LCP: ${p.lcp.toFixed(0)}ms`);
4323
4466
  if (p.fid !== void 0) console.log(` FID: ${p.fid.toFixed(0)}ms`);
4324
4467
  if (p.cls !== void 0) console.log(` CLS: ${p.cls.toFixed(3)}`);
4468
+ console.log();
4325
4469
  }
4326
4470
  }
4327
4471
  const failedTests = results.flatMap(
@@ -4330,23 +4474,31 @@ async function displaySummary(results, config) {
4330
4474
  )
4331
4475
  );
4332
4476
  if (failedTests.length > 0) {
4333
- console.log();
4334
4477
  console.log(chalk5.red(" \u5931\u8D25\u8BE6\u60C5:"));
4335
4478
  for (const { suite, test } of failedTests) {
4336
- console.log(chalk5.red(` \u2717 ${suite.name} > ${test.name}`));
4479
+ console.log(chalk5.red(` \u2715 ${suite.name} > ${test.name}`));
4337
4480
  if (test.error?.message) {
4338
4481
  console.log(chalk5.gray(` ${test.error.message.split("\n")[0]}`));
4339
4482
  }
4340
4483
  }
4484
+ console.log();
4341
4485
  }
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
4486
  if (totalFailed > 0) {
4487
+ console.log(chalk5.red(` Tests: ${totalFailed} failed, ${totalPassed} passed, ${total} total`));
4345
4488
  process.exitCode = 1;
4489
+ } else if (total === 0) {
4490
+ console.log(chalk5.yellow(" \u6CA1\u6709\u53D1\u73B0\u6D4B\u8BD5\u7528\u4F8B"));
4491
+ } else {
4492
+ console.log(chalk5.green(` Tests: ${totalPassed} passed, ${total} total`));
4346
4493
  }
4347
- if (totalFailed > 0 && isAIAvailable(config.ai)) {
4348
- await aiAnalyzeFailures(results, config.ai);
4349
- }
4494
+ console.log(chalk5.gray(` Time: ${formatDuration2(totalDuration)}`));
4495
+ console.log();
4496
+ }
4497
+ function coverageColor(value) {
4498
+ const pct3 = `${(value * 100).toFixed(1)}%`;
4499
+ if (value >= 0.8) return chalk5.green(pct3);
4500
+ if (value >= 0.5) return chalk5.yellow(pct3);
4501
+ return chalk5.red(pct3);
4350
4502
  }
4351
4503
  async function aiAnalyzeFailures(results, aiConfig) {
4352
4504
  const failedTests = results.flatMap(
@@ -4375,7 +4527,7 @@ async function aiAnalyzeFailures(results, aiConfig) {
4375
4527
  }
4376
4528
  }
4377
4529
  }
4378
- function formatDuration(ms) {
4530
+ function formatDuration2(ms) {
4379
4531
  if (ms < 1e3) return `${ms}ms`;
4380
4532
  if (ms < 6e4) return `${(ms / 1e3).toFixed(1)}s`;
4381
4533
  const min = Math.floor(ms / 6e4);
@@ -4401,8 +4553,8 @@ async function checkDevServer(config) {
4401
4553
  }
4402
4554
  async function startDevServer(config) {
4403
4555
  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"));
4556
+ const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4557
+ const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4406
4558
  const pkgCmd = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4407
4559
  console.log(chalk5.cyan(` \u6B63\u5728\u542F\u52A8 dev server (${pkgCmd} run dev) ...`));
4408
4560
  const child = spawn(pkgCmd, ["run", "dev"], {
@@ -4431,27 +4583,99 @@ async function startDevServer(config) {
4431
4583
  }
4432
4584
  function saveRunResults(results) {
4433
4585
  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 });
4586
+ const resultsPath = path12.join(process.cwd(), RESULTS_DIR);
4587
+ if (!fs11.existsSync(resultsPath)) {
4588
+ fs11.mkdirSync(resultsPath, { recursive: true });
4437
4589
  }
4438
4590
  const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
4439
4591
  const fileName = `result-${timestamp}.json`;
4440
- const filePath = path11.join(resultsPath, fileName);
4592
+ const filePath = path12.join(resultsPath, fileName);
4441
4593
  const data = {
4442
4594
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
4443
4595
  results
4444
4596
  };
4445
- fs9.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4597
+ fs11.writeFileSync(filePath, JSON.stringify(data, null, 2), "utf-8");
4446
4598
  try {
4447
- const files = fs9.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4599
+ const files = fs11.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4448
4600
  while (files.length > 20) {
4449
4601
  const oldest = files.shift();
4450
- fs9.unlinkSync(path11.join(resultsPath, oldest));
4602
+ fs11.unlinkSync(path12.join(resultsPath, oldest));
4451
4603
  }
4452
4604
  } catch {
4453
4605
  }
4454
4606
  }
4607
+ function checkTestDependencies(types) {
4608
+ const pkgPath = path12.join(process.cwd(), "package.json");
4609
+ let allDeps = {};
4610
+ if (fs11.existsSync(pkgPath)) {
4611
+ try {
4612
+ const pkg = JSON.parse(fs11.readFileSync(pkgPath, "utf-8"));
4613
+ allDeps = {
4614
+ ...pkg.dependencies,
4615
+ ...pkg.devDependencies
4616
+ };
4617
+ } catch {
4618
+ }
4619
+ }
4620
+ const missingDeps = [];
4621
+ const checkedPkgs = /* @__PURE__ */ new Set();
4622
+ for (const testType of types) {
4623
+ const dep = TYPE_DEPENDENCIES[testType];
4624
+ if (dep && !checkedPkgs.has(dep.pkg)) {
4625
+ checkedPkgs.add(dep.pkg);
4626
+ if (!allDeps[dep.pkg]) {
4627
+ missingDeps.push(dep);
4628
+ }
4629
+ }
4630
+ }
4631
+ return missingDeps;
4632
+ }
4633
+ async function installTestDependencies(missingDeps) {
4634
+ const hasPnpm = fs11.existsSync(path12.join(process.cwd(), "pnpm-lock.yaml"));
4635
+ const hasYarn = fs11.existsSync(path12.join(process.cwd(), "yarn.lock"));
4636
+ const pkgManager = hasPnpm ? "pnpm" : hasYarn ? "yarn" : "npm";
4637
+ const { execFile: execFile5 } = await import("child_process");
4638
+ let allSuccess = true;
4639
+ for (const dep of missingDeps) {
4640
+ const spinner = ora4(`\u6B63\u5728\u5B89\u88C5 ${dep.pkg}...`).start();
4641
+ const isPlaywright = dep.pkg === "@playwright/test";
4642
+ const installArgs = hasPnpm ? ["add", "-D", dep.pkg] : hasYarn ? ["add", "-D", dep.pkg] : ["install", "-D", dep.pkg];
4643
+ try {
4644
+ await new Promise((resolve, reject) => {
4645
+ const cmd = pkgManager === "npm" ? "npm" : pkgManager;
4646
+ const child = execFile5(cmd, installArgs, {
4647
+ cwd: process.cwd(),
4648
+ shell: true,
4649
+ stdio: "pipe"
4650
+ }, (error) => {
4651
+ if (error) reject(error);
4652
+ else resolve();
4653
+ });
4654
+ child.on("error", reject);
4655
+ });
4656
+ if (isPlaywright) {
4657
+ spinner.text = `\u6B63\u5728\u5B89\u88C5 Playwright \u6D4F\u89C8\u5668...`;
4658
+ await new Promise((resolve, reject) => {
4659
+ const child = execFile5("npx", ["playwright", "install"], {
4660
+ cwd: process.cwd(),
4661
+ shell: true,
4662
+ stdio: "pipe"
4663
+ }, (error) => {
4664
+ if (error) reject(error);
4665
+ else resolve();
4666
+ });
4667
+ child.on("error", reject);
4668
+ });
4669
+ }
4670
+ spinner.succeed(`${dep.pkg} \u5B89\u88C5\u6210\u529F`);
4671
+ } catch (error) {
4672
+ spinner.fail(`${dep.pkg} \u5B89\u88C5\u5931\u8D25`);
4673
+ console.log(chalk5.gray(` \u53EF\u624B\u52A8\u5B89\u88C5: ${chalk5.cyan(dep.installCmd)}`));
4674
+ allSuccess = false;
4675
+ }
4676
+ }
4677
+ return allSuccess;
4678
+ }
4455
4679
 
4456
4680
  // src/commands/mock.ts
4457
4681
  import chalk6 from "chalk";
@@ -4575,219 +4799,8 @@ function showStatus() {
4575
4799
  // src/commands/report.ts
4576
4800
  import chalk7 from "chalk";
4577
4801
  import ora6 from "ora";
4578
- import fs11 from "fs";
4802
+ import fs12 from "fs";
4579
4803
  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
4804
  var RESULTS_DIR2 = ".qat-results";
4792
4805
  function registerReportCommand(program2) {
4793
4806
  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 +4837,14 @@ async function executeReport(options) {
4824
4837
  function collectResults() {
4825
4838
  const results = [];
4826
4839
  const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
4827
- if (!fs11.existsSync(resultsPath)) {
4840
+ if (!fs12.existsSync(resultsPath)) {
4828
4841
  return results;
4829
4842
  }
4830
- const files = fs11.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
4843
+ const files = fs12.readdirSync(resultsPath).filter((f) => f.endsWith(".json")).sort().reverse();
4831
4844
  if (files.length > 0) {
4832
4845
  const latestFile = path13.join(resultsPath, files[0]);
4833
4846
  try {
4834
- const data = JSON.parse(fs11.readFileSync(latestFile, "utf-8"));
4847
+ const data = JSON.parse(fs12.readFileSync(latestFile, "utf-8"));
4835
4848
  if (Array.isArray(data)) {
4836
4849
  results.push(...data);
4837
4850
  } else if (data.results) {
@@ -4844,17 +4857,17 @@ function collectResults() {
4844
4857
  }
4845
4858
  function saveResultToHistory(reportData) {
4846
4859
  const resultsPath = path13.join(process.cwd(), RESULTS_DIR2);
4847
- if (!fs11.existsSync(resultsPath)) {
4848
- fs11.mkdirSync(resultsPath, { recursive: true });
4860
+ if (!fs12.existsSync(resultsPath)) {
4861
+ fs12.mkdirSync(resultsPath, { recursive: true });
4849
4862
  }
4850
4863
  const timestamp = new Date(reportData.timestamp).toISOString().replace(/[:.]/g, "-");
4851
4864
  const fileName = `result-${timestamp}.json`;
4852
4865
  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();
4866
+ fs12.writeFileSync(filePath, JSON.stringify(reportData, null, 2), "utf-8");
4867
+ const files = fs12.readdirSync(resultsPath).filter((f) => f.startsWith("result-") && f.endsWith(".json")).sort();
4855
4868
  while (files.length > 20) {
4856
4869
  const oldest = files.shift();
4857
- fs11.unlinkSync(path13.join(resultsPath, oldest));
4870
+ fs12.unlinkSync(path13.join(resultsPath, oldest));
4858
4871
  }
4859
4872
  }
4860
4873
  function displayReportResult(reportPath, data) {
@@ -4915,19 +4928,19 @@ import chalk8 from "chalk";
4915
4928
  import ora7 from "ora";
4916
4929
 
4917
4930
  // src/services/visual.ts
4918
- import fs12 from "fs";
4931
+ import fs13 from "fs";
4919
4932
  import path14 from "path";
4920
4933
  import pixelmatch from "pixelmatch";
4921
4934
  import { PNG } from "pngjs";
4922
4935
  function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.1) {
4923
- if (!fs12.existsSync(baselinePath)) {
4936
+ if (!fs13.existsSync(baselinePath)) {
4924
4937
  throw new Error(`\u57FA\u7EBF\u56FE\u7247\u4E0D\u5B58\u5728: ${baselinePath}`);
4925
4938
  }
4926
- if (!fs12.existsSync(currentPath)) {
4939
+ if (!fs13.existsSync(currentPath)) {
4927
4940
  throw new Error(`\u5F53\u524D\u56FE\u7247\u4E0D\u5B58\u5728: ${currentPath}`);
4928
4941
  }
4929
- const baseline = PNG.sync.read(fs12.readFileSync(baselinePath));
4930
- const current = PNG.sync.read(fs12.readFileSync(currentPath));
4942
+ const baseline = PNG.sync.read(fs13.readFileSync(baselinePath));
4943
+ const current = PNG.sync.read(fs13.readFileSync(currentPath));
4931
4944
  if (baseline.width !== current.width || baseline.height !== current.height) {
4932
4945
  return {
4933
4946
  passed: false,
@@ -4956,10 +4969,10 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
4956
4969
  let diffPath;
4957
4970
  if (diffPixels > 0) {
4958
4971
  const diffDir = path14.dirname(diffOutputPath);
4959
- if (!fs12.existsSync(diffDir)) {
4960
- fs12.mkdirSync(diffDir, { recursive: true });
4972
+ if (!fs13.existsSync(diffDir)) {
4973
+ fs13.mkdirSync(diffDir, { recursive: true });
4961
4974
  }
4962
- fs12.writeFileSync(diffOutputPath, PNG.sync.write(diff));
4975
+ fs13.writeFileSync(diffOutputPath, PNG.sync.write(diff));
4963
4976
  diffPath = diffOutputPath;
4964
4977
  }
4965
4978
  return {
@@ -4973,69 +4986,69 @@ function compareImages(baselinePath, currentPath, diffOutputPath, threshold = 0.
4973
4986
  };
4974
4987
  }
4975
4988
  function createBaseline(currentPath, baselinePath) {
4976
- if (!fs12.existsSync(currentPath)) {
4989
+ if (!fs13.existsSync(currentPath)) {
4977
4990
  throw new Error(`\u5F53\u524D\u622A\u56FE\u4E0D\u5B58\u5728: ${currentPath}`);
4978
4991
  }
4979
4992
  const baselineDir = path14.dirname(baselinePath);
4980
- if (!fs12.existsSync(baselineDir)) {
4981
- fs12.mkdirSync(baselineDir, { recursive: true });
4993
+ if (!fs13.existsSync(baselineDir)) {
4994
+ fs13.mkdirSync(baselineDir, { recursive: true });
4982
4995
  }
4983
- fs12.copyFileSync(currentPath, baselinePath);
4996
+ fs13.copyFileSync(currentPath, baselinePath);
4984
4997
  return baselinePath;
4985
4998
  }
4986
4999
  function updateAllBaselines(currentDir, baselineDir) {
4987
5000
  const updated = [];
4988
- if (!fs12.existsSync(currentDir)) {
5001
+ if (!fs13.existsSync(currentDir)) {
4989
5002
  return updated;
4990
5003
  }
4991
- const files = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5004
+ const files = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
4992
5005
  for (const file of files) {
4993
5006
  const currentPath = path14.join(currentDir, file);
4994
5007
  const baselinePath = path14.join(baselineDir, file);
4995
5008
  const baselineDirAbs = path14.dirname(baselinePath);
4996
- if (!fs12.existsSync(baselineDirAbs)) {
4997
- fs12.mkdirSync(baselineDirAbs, { recursive: true });
5009
+ if (!fs13.existsSync(baselineDirAbs)) {
5010
+ fs13.mkdirSync(baselineDirAbs, { recursive: true });
4998
5011
  }
4999
- fs12.copyFileSync(currentPath, baselinePath);
5012
+ fs13.copyFileSync(currentPath, baselinePath);
5000
5013
  updated.push(file);
5001
5014
  }
5002
5015
  return updated;
5003
5016
  }
5004
5017
  function cleanBaselines(baselineDir) {
5005
- if (!fs12.existsSync(baselineDir)) {
5018
+ if (!fs13.existsSync(baselineDir)) {
5006
5019
  return 0;
5007
5020
  }
5008
- const files = fs12.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5021
+ const files = fs13.readdirSync(baselineDir).filter((f) => f.endsWith(".png"));
5009
5022
  let count = 0;
5010
5023
  for (const file of files) {
5011
- fs12.unlinkSync(path14.join(baselineDir, file));
5024
+ fs13.unlinkSync(path14.join(baselineDir, file));
5012
5025
  count++;
5013
5026
  }
5014
5027
  return count;
5015
5028
  }
5016
5029
  function cleanDiffs(diffDir) {
5017
- if (!fs12.existsSync(diffDir)) {
5030
+ if (!fs13.existsSync(diffDir)) {
5018
5031
  return 0;
5019
5032
  }
5020
- const files = fs12.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5033
+ const files = fs13.readdirSync(diffDir).filter((f) => f.endsWith(".png"));
5021
5034
  let count = 0;
5022
5035
  for (const file of files) {
5023
- fs12.unlinkSync(path14.join(diffDir, file));
5036
+ fs13.unlinkSync(path14.join(diffDir, file));
5024
5037
  count++;
5025
5038
  }
5026
5039
  return count;
5027
5040
  }
5028
5041
  function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5029
5042
  const results = [];
5030
- if (!fs12.existsSync(currentDir)) {
5043
+ if (!fs13.existsSync(currentDir)) {
5031
5044
  return results;
5032
5045
  }
5033
- const currentFiles = fs12.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5046
+ const currentFiles = fs13.readdirSync(currentDir).filter((f) => f.endsWith(".png"));
5034
5047
  for (const file of currentFiles) {
5035
5048
  const currentPath = path14.join(currentDir, file);
5036
5049
  const baselinePath = path14.join(baselineDir, file);
5037
5050
  const diffPath = path14.join(diffDir, file);
5038
- if (!fs12.existsSync(baselinePath)) {
5051
+ if (!fs13.existsSync(baselinePath)) {
5039
5052
  createBaseline(currentPath, baselinePath);
5040
5053
  results.push({
5041
5054
  passed: true,
@@ -5067,7 +5080,7 @@ function compareDirectories(baselineDir, currentDir, diffDir, threshold = 0.1) {
5067
5080
  }
5068
5081
 
5069
5082
  // src/commands/visual.ts
5070
- import fs13 from "fs";
5083
+ import fs14 from "fs";
5071
5084
  import path15 from "path";
5072
5085
  function registerVisualCommand(program2) {
5073
5086
  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 +5161,7 @@ function findCurrentScreenshotsDir(baselineDir) {
5148
5161
  path15.join(process.cwd(), baselineDir, "..", "current")
5149
5162
  ];
5150
5163
  for (const dir of possibleDirs) {
5151
- if (fs13.existsSync(dir)) {
5164
+ if (fs14.existsSync(dir)) {
5152
5165
  const pngs = findPngFiles(dir);
5153
5166
  if (pngs.length > 0) return dir;
5154
5167
  }
@@ -5158,8 +5171,8 @@ function findCurrentScreenshotsDir(baselineDir) {
5158
5171
  function findPngFiles(dir) {
5159
5172
  const files = [];
5160
5173
  function walk(d) {
5161
- if (!fs13.existsSync(d)) return;
5162
- const entries = fs13.readdirSync(d, { withFileTypes: true });
5174
+ if (!fs14.existsSync(d)) return;
5175
+ const entries = fs14.readdirSync(d, { withFileTypes: true });
5163
5176
  for (const entry of entries) {
5164
5177
  const fullPath = path15.join(d, entry.name);
5165
5178
  if (entry.isDirectory() && entry.name !== "node_modules") {
@@ -5257,7 +5270,7 @@ import chalk9 from "chalk";
5257
5270
  import inquirer4 from "inquirer";
5258
5271
  import ora8 from "ora";
5259
5272
  import { execFile as execFile4 } from "child_process";
5260
- import fs14 from "fs";
5273
+ import fs15 from "fs";
5261
5274
  import path16 from "path";
5262
5275
  var DEPENDENCY_GROUPS = [
5263
5276
  {
@@ -5292,7 +5305,7 @@ function registerSetupCommand(program2) {
5292
5305
  async function executeSetup(options) {
5293
5306
  console.log(chalk9.cyan("\n QAT \u4F9D\u8D56\u5B89\u88C5\u5668\n"));
5294
5307
  const projectInfo = detectProject();
5295
- if (!fs14.existsSync(path16.join(process.cwd(), "package.json"))) {
5308
+ if (!fs15.existsSync(path16.join(process.cwd(), "package.json"))) {
5296
5309
  throw new Error("\u672A\u627E\u5230 package.json\uFF0C\u8BF7\u5728\u9879\u76EE\u6839\u76EE\u5F55\u6267\u884C\u6B64\u547D\u4EE4");
5297
5310
  }
5298
5311
  if (projectInfo.frameworkConfidence > 0) {
@@ -5327,7 +5340,7 @@ async function executeSetup(options) {
5327
5340
  ]);
5328
5341
  if (chooseDir !== "root") {
5329
5342
  installDir = path16.join(process.cwd(), chooseDir);
5330
- if (!fs14.existsSync(path16.join(installDir, "package.json"))) {
5343
+ if (!fs15.existsSync(path16.join(installDir, "package.json"))) {
5331
5344
  throw new Error(`${chooseDir} \u4E0B\u6CA1\u6709 package.json`);
5332
5345
  }
5333
5346
  }
@@ -5647,7 +5660,7 @@ async function executeChange(_options) {
5647
5660
  }
5648
5661
 
5649
5662
  // src/cli.ts
5650
- var VERSION = "0.3.01";
5663
+ var VERSION = "0.3.02";
5651
5664
  function printLogo() {
5652
5665
  const logo = `
5653
5666
  ${chalk12.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}