qat-cli 0.3.5 → 0.3.7
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/README.md +1 -1
- package/dist/cli.js +339 -77
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +122 -26
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +25 -2
- package/dist/index.d.ts +25 -2
- package/dist/index.js +122 -26
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**Quick Auto Testing — 面向 Vue 项目,集成 Vitest & Playwright,AI 驱动覆盖测试全流程**
|
|
6
6
|
|
|
7
|
-
[](https://www.npmjs.com/package/qat-cli)
|
|
8
8
|
[](https://nodejs.org/)
|
|
9
9
|
[](https://opensource.org/licenses/MIT)
|
|
10
10
|
[](https://www.typescriptlang.org/)
|
package/dist/cli.js
CHANGED
|
@@ -787,6 +787,37 @@ function debugLog(tag, ...args) {
|
|
|
787
787
|
const timestamp = (/* @__PURE__ */ new Date()).toISOString().slice(11, 19);
|
|
788
788
|
console.error(chalk2.gray(`[DEBUG ${timestamp}] [${tag}]`), ...args);
|
|
789
789
|
}
|
|
790
|
+
var ConcurrencyPool = class {
|
|
791
|
+
constructor(maxConcurrency) {
|
|
792
|
+
this.maxConcurrency = maxConcurrency;
|
|
793
|
+
this.running = 0;
|
|
794
|
+
this.queue = [];
|
|
795
|
+
}
|
|
796
|
+
async acquire() {
|
|
797
|
+
if (this.running < this.maxConcurrency) {
|
|
798
|
+
this.running++;
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
return new Promise((resolve) => {
|
|
802
|
+
this.queue.push({ resolve });
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
release() {
|
|
806
|
+
this.running--;
|
|
807
|
+
const next = this.queue.shift();
|
|
808
|
+
if (next) {
|
|
809
|
+
this.running++;
|
|
810
|
+
next.resolve();
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
get pending() {
|
|
814
|
+
return this.queue.length;
|
|
815
|
+
}
|
|
816
|
+
get active() {
|
|
817
|
+
return this.running;
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
var aiPool = new ConcurrencyPool(3);
|
|
790
821
|
var OpenAICompatibleProvider = class {
|
|
791
822
|
constructor(config) {
|
|
792
823
|
this.capabilities = {
|
|
@@ -902,9 +933,34 @@ ${errorDetails}`;
|
|
|
902
933
|
return { ok: false, message: `\u8FDE\u63A5\u5931\u8D25: ${error instanceof Error ? error.message : String(error)}`, latencyMs };
|
|
903
934
|
}
|
|
904
935
|
}
|
|
936
|
+
/**
|
|
937
|
+
* 核心 chat 方法
|
|
938
|
+
*
|
|
939
|
+
* 调度策略:
|
|
940
|
+
* 1. 始终使用流式(stream: true),避免长时间等待无响应导致超时
|
|
941
|
+
* 2. 分段超时:连接/首chunk/后续chunk 各自独立超时,只要 AI 在输出就不超时
|
|
942
|
+
* 3. 并发池控制:acquire → 请求 → release,避免同时太多请求
|
|
943
|
+
* 4. 指数退避重试:429/5xx/timeout 自动重试,最多 2 次
|
|
944
|
+
*/
|
|
905
945
|
async chat(systemPrompt, userPrompt, retries = 2) {
|
|
946
|
+
await aiPool.acquire();
|
|
947
|
+
try {
|
|
948
|
+
return await this._chatInner(systemPrompt, userPrompt, retries);
|
|
949
|
+
} finally {
|
|
950
|
+
aiPool.release();
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
async _chatInner(systemPrompt, userPrompt, retries) {
|
|
906
954
|
const url = `${this.baseUrl}/chat/completions`;
|
|
907
|
-
const
|
|
955
|
+
const TIMEOUT = {
|
|
956
|
+
/** 连接建立超时:fetch 请求本身的最大等待时间 */
|
|
957
|
+
CONNECT: 15e3,
|
|
958
|
+
/** 首个 chunk 超时:连接建好后等待 AI 开始输出的时间 */
|
|
959
|
+
FIRST_CHUNK: 3e4,
|
|
960
|
+
/** chunk 间超时:每个 chunk 之间的最大间隔 */
|
|
961
|
+
CHUNK_INTERVAL: 2e4
|
|
962
|
+
};
|
|
963
|
+
debugLog("SCHEDULER", `\u5E76\u53D1\u6C60: active=${aiPool.active} pending=${aiPool.pending} \u5206\u6BB5\u8D85\u65F6: connect=${TIMEOUT.CONNECT / 1e3}s first=${TIMEOUT.FIRST_CHUNK / 1e3}s interval=${TIMEOUT.CHUNK_INTERVAL / 1e3}s`);
|
|
908
964
|
const body = {
|
|
909
965
|
model: this.model,
|
|
910
966
|
messages: [
|
|
@@ -912,19 +968,16 @@ ${errorDetails}`;
|
|
|
912
968
|
{ role: "user", content: userPrompt }
|
|
913
969
|
],
|
|
914
970
|
temperature: 0.3,
|
|
915
|
-
max_tokens: 4096
|
|
971
|
+
max_tokens: 4096,
|
|
972
|
+
stream: true
|
|
916
973
|
};
|
|
917
|
-
if (useStream) {
|
|
918
|
-
body.stream = true;
|
|
919
|
-
}
|
|
920
974
|
const headers = {
|
|
921
975
|
"Content-Type": "application/json"
|
|
922
976
|
};
|
|
923
977
|
if (this.apiKey) {
|
|
924
978
|
headers["Authorization"] = `Bearer ${this.apiKey}`;
|
|
925
979
|
}
|
|
926
|
-
debugLog("REQUEST", `POST ${url}`);
|
|
927
|
-
debugLog("REQUEST", `model=${this.model} stream=${useStream}`);
|
|
980
|
+
debugLog("REQUEST", `POST ${url} model=${this.model} stream=true`);
|
|
928
981
|
debugLog("SYSTEM", systemPrompt.length > 500 ? `${systemPrompt.slice(0, 500)}...` : systemPrompt);
|
|
929
982
|
debugLog("USER", userPrompt.length > 1e3 ? `${userPrompt.slice(0, 1e3)}...` : userPrompt);
|
|
930
983
|
let lastError = null;
|
|
@@ -933,12 +986,12 @@ ${errorDetails}`;
|
|
|
933
986
|
if (attempt > 0) {
|
|
934
987
|
debugLog("RETRY", `\u7B2C${attempt}\u6B21\u91CD\u8BD5...`);
|
|
935
988
|
}
|
|
989
|
+
const connectStart = Date.now();
|
|
936
990
|
const response = await fetch(url, {
|
|
937
991
|
method: "POST",
|
|
938
992
|
headers,
|
|
939
993
|
body: JSON.stringify(body),
|
|
940
|
-
signal: AbortSignal.timeout(
|
|
941
|
-
// 120s timeout
|
|
994
|
+
signal: AbortSignal.timeout(TIMEOUT.CONNECT)
|
|
942
995
|
});
|
|
943
996
|
if (!response.ok) {
|
|
944
997
|
const text = await response.text().catch(() => "");
|
|
@@ -951,17 +1004,21 @@ ${errorDetails}`;
|
|
|
951
1004
|
}
|
|
952
1005
|
throw new Error(`AI API \u8BF7\u6C42\u5931\u8D25 (${response.status}): ${text.slice(0, 500)}`);
|
|
953
1006
|
}
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
1007
|
+
const connectMs = Date.now() - connectStart;
|
|
1008
|
+
debugLog("CONNECT", `\u8FDE\u63A5\u5EFA\u7ACB ${connectMs}ms`);
|
|
1009
|
+
if (response.body) {
|
|
1010
|
+
const content = await this.readStreamWithTimeout(
|
|
1011
|
+
response.body,
|
|
1012
|
+
TIMEOUT.FIRST_CHUNK,
|
|
1013
|
+
TIMEOUT.CHUNK_INTERVAL
|
|
1014
|
+
);
|
|
1015
|
+
debugLog("RESPONSE", `${content.length} chars, \u603B\u8017\u65F6 ${Date.now() - connectStart}ms`);
|
|
957
1016
|
return content;
|
|
958
1017
|
}
|
|
959
1018
|
const data = await response.json();
|
|
960
1019
|
if (!data.choices?.[0]?.message?.content) {
|
|
961
1020
|
throw new Error("AI API \u8FD4\u56DE\u7A7A\u54CD\u5E94");
|
|
962
1021
|
}
|
|
963
|
-
debugLog("RESPONSE", `tokens: prompt=${data.usage?.prompt_tokens} completion=${data.usage?.completion_tokens} total=${data.usage?.total_tokens}`);
|
|
964
|
-
debugLog("RESPONSE", data.choices[0].message.content.length > 500 ? `${data.choices[0].message.content.slice(0, 500)}...` : data.choices[0].message.content);
|
|
965
1022
|
return data.choices[0].message.content;
|
|
966
1023
|
} catch (error) {
|
|
967
1024
|
if (error instanceof Error && error.name === "TimeoutError" && attempt < retries) {
|
|
@@ -975,18 +1032,53 @@ ${errorDetails}`;
|
|
|
975
1032
|
throw lastError || new Error("AI API \u8BF7\u6C42\u5931\u8D25");
|
|
976
1033
|
}
|
|
977
1034
|
/**
|
|
978
|
-
*
|
|
1035
|
+
* 分段超时的流式读取
|
|
1036
|
+
*
|
|
1037
|
+
* 核心思路:每个 chunk 独立超时,只要 AI 持续输出就永远不会超时
|
|
1038
|
+
*
|
|
1039
|
+
* 三级超时:
|
|
1040
|
+
* - firstChunkTimeout: 连接建立后等待首个 AI 输出的超时(AI 思考时间)
|
|
1041
|
+
* - chunkIntervalTimeout: 每个 chunk 之间的超时(网络中断检测)
|
|
1042
|
+
* - 每个 read() 操作独立计时,收到 chunk 立即重置
|
|
979
1043
|
*/
|
|
980
|
-
async
|
|
1044
|
+
async readStreamWithTimeout(body, firstChunkTimeout, chunkIntervalTimeout) {
|
|
981
1045
|
const chunks = [];
|
|
982
1046
|
const decoder = new TextDecoder();
|
|
983
1047
|
const reader = body.getReader();
|
|
984
1048
|
let buffer = "";
|
|
985
|
-
let
|
|
1049
|
+
let isFirstChunk = true;
|
|
1050
|
+
let lastChunkTime = Date.now();
|
|
986
1051
|
try {
|
|
987
1052
|
while (true) {
|
|
988
|
-
const
|
|
1053
|
+
const currentTimeout = isFirstChunk ? firstChunkTimeout : chunkIntervalTimeout;
|
|
1054
|
+
const elapsed = Date.now() - lastChunkTime;
|
|
1055
|
+
if (elapsed > currentTimeout) {
|
|
1056
|
+
const phase = isFirstChunk ? "\u9996chunk" : "chunk\u95F4";
|
|
1057
|
+
debugLog("STREAM-TIMEOUT", `${phase}\u8D85\u65F6(${currentTimeout / 1e3}s)\uFF0C\u5DF2\u6536 ${chunks.length} chunks`);
|
|
1058
|
+
if (chunks.length > 0) {
|
|
1059
|
+
debugLog("STREAM-TIMEOUT", `\u5DF2\u6709\u5185\u5BB9\uFF0C\u8FD4\u56DE\u5DF2\u6536\u5230\u7684 ${chunks.join("").length} chars`);
|
|
1060
|
+
break;
|
|
1061
|
+
}
|
|
1062
|
+
throw new Error(`AI \u6D41\u5F0F\u54CD\u5E94${phase}\u8D85\u65F6(${currentTimeout / 1e3}s)`);
|
|
1063
|
+
}
|
|
1064
|
+
const remainingMs = currentTimeout - elapsed;
|
|
1065
|
+
let readResult;
|
|
1066
|
+
try {
|
|
1067
|
+
readResult = await Promise.race([
|
|
1068
|
+
reader.read(),
|
|
1069
|
+
new Promise(
|
|
1070
|
+
(_, reject) => setTimeout(() => reject(new Error("__CHUNK_TIMEOUT__")), remainingMs)
|
|
1071
|
+
)
|
|
1072
|
+
]);
|
|
1073
|
+
} catch (e) {
|
|
1074
|
+
if (e instanceof Error && e.message === "__CHUNK_TIMEOUT__") {
|
|
1075
|
+
continue;
|
|
1076
|
+
}
|
|
1077
|
+
throw e;
|
|
1078
|
+
}
|
|
1079
|
+
const { done, value } = readResult;
|
|
989
1080
|
if (done) break;
|
|
1081
|
+
lastChunkTime = Date.now();
|
|
990
1082
|
buffer += decoder.decode(value, { stream: true });
|
|
991
1083
|
const lines = buffer.split("\n");
|
|
992
1084
|
buffer = lines.pop() || "";
|
|
@@ -998,11 +1090,13 @@ ${errorDetails}`;
|
|
|
998
1090
|
const json = JSON.parse(trimmed.slice(6));
|
|
999
1091
|
const delta = json.choices?.[0]?.delta?.content;
|
|
1000
1092
|
if (delta) {
|
|
1093
|
+
if (isFirstChunk) {
|
|
1094
|
+
isFirstChunk = false;
|
|
1095
|
+
debugLog("STREAM", `\u9996chunk\u5230\u8FBE ${Date.now() - lastChunkTime}ms`);
|
|
1096
|
+
}
|
|
1001
1097
|
chunks.push(delta);
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
if (lineCount % 20 === 0) {
|
|
1005
|
-
process.stderr.write("\n");
|
|
1098
|
+
if (isDebug()) {
|
|
1099
|
+
process.stderr.write(chalk2.gray(delta));
|
|
1006
1100
|
}
|
|
1007
1101
|
}
|
|
1008
1102
|
if (json.choices?.[0]?.finish_reason === "stop") {
|
|
@@ -1015,7 +1109,7 @@ ${errorDetails}`;
|
|
|
1015
1109
|
} finally {
|
|
1016
1110
|
reader.releaseLock();
|
|
1017
1111
|
}
|
|
1018
|
-
if (chunks.length > 0) {
|
|
1112
|
+
if (isDebug() && chunks.length > 0) {
|
|
1019
1113
|
process.stderr.write("\n");
|
|
1020
1114
|
}
|
|
1021
1115
|
return chunks.join("");
|
|
@@ -1156,6 +1250,8 @@ ${this.compressSourceCode(req.context, 3e3, importPathRewrites)}
|
|
|
1156
1250
|
if (srcDirMatch) {
|
|
1157
1251
|
variations.add(`@/${srcDirMatch[1]}`);
|
|
1158
1252
|
variations.add(`@/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
|
|
1253
|
+
variations.add(`#/${srcDirMatch[1]}`);
|
|
1254
|
+
variations.add(`#/${srcDirMatch[1].replace(/\.(ts|js|tsx|jsx)$/, "")}`);
|
|
1159
1255
|
}
|
|
1160
1256
|
const pathParts = targetPath.replace(/^\.\//, "").split("/");
|
|
1161
1257
|
const fileName = pathParts[pathParts.length - 1];
|
|
@@ -1890,7 +1986,7 @@ function extractImports(content) {
|
|
|
1890
1986
|
function analyzeImportSignatures(imports, baseDir) {
|
|
1891
1987
|
const signatures = [];
|
|
1892
1988
|
for (const imp of imports) {
|
|
1893
|
-
if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("~")) {
|
|
1989
|
+
if (!imp.source.startsWith(".") && !imp.source.startsWith("@/") && !imp.source.startsWith("#/") && !imp.source.startsWith("~")) {
|
|
1894
1990
|
continue;
|
|
1895
1991
|
}
|
|
1896
1992
|
const resolvedPath = resolveImportPath(imp.source, baseDir);
|
|
@@ -1932,8 +2028,8 @@ function analyzeImportSignatures(imports, baseDir) {
|
|
|
1932
2028
|
}
|
|
1933
2029
|
function resolveImportPath(importSource, baseDir) {
|
|
1934
2030
|
let resolved;
|
|
1935
|
-
if (importSource.startsWith("@/") || importSource.startsWith("~")) {
|
|
1936
|
-
const relativePath = importSource.replace(/^[
|
|
2031
|
+
if (importSource.startsWith("@/") || importSource.startsWith("#/") || importSource.startsWith("~")) {
|
|
2032
|
+
const relativePath = importSource.replace(/^[@#~]\//, "src/");
|
|
1937
2033
|
resolved = path4.resolve(process.cwd(), relativePath);
|
|
1938
2034
|
} else if (importSource.startsWith(".")) {
|
|
1939
2035
|
resolved = path4.resolve(baseDir, importSource);
|
|
@@ -2718,6 +2814,7 @@ import ora2 from "ora";
|
|
|
2718
2814
|
import path8 from "path";
|
|
2719
2815
|
var MAX_RETRIES = 3;
|
|
2720
2816
|
var REVIEW_THRESHOLD = 0.6;
|
|
2817
|
+
var REVIEW_SKIP_SOURCE_SIZE = 8e3;
|
|
2721
2818
|
async function generateWithReview(params) {
|
|
2722
2819
|
const { testType, targetPath, sourceCode, analysis, aiConfig, framework, onAttempt, onProgress } = params;
|
|
2723
2820
|
const generatorProvider = createAIProvider(aiConfig);
|
|
@@ -2732,6 +2829,13 @@ async function generateWithReview(params) {
|
|
|
2732
2829
|
let approved = false;
|
|
2733
2830
|
let attempts = 0;
|
|
2734
2831
|
const targetName = path8.basename(targetPath);
|
|
2832
|
+
const skipReview = sourceCode.length > REVIEW_SKIP_SOURCE_SIZE;
|
|
2833
|
+
if (skipReview) {
|
|
2834
|
+
onProgress?.(`\u6E90\u7801\u8F83\u5927(${(sourceCode.length / 1024).toFixed(1)}KB)\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1 \u2190 ${targetName}`);
|
|
2835
|
+
if (process.env.QAT_DEBUG === "true") {
|
|
2836
|
+
console.error(chalk4.gray(`[DEBUG] \u6E90\u7801 ${sourceCode.length} chars > ${REVIEW_SKIP_SOURCE_SIZE}\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1\u73AF\u8282`));
|
|
2837
|
+
}
|
|
2838
|
+
}
|
|
2735
2839
|
for (let i = 0; i < MAX_RETRIES; i++) {
|
|
2736
2840
|
attempts = i + 1;
|
|
2737
2841
|
onAttempt?.(attempts, MAX_RETRIES);
|
|
@@ -2760,6 +2864,11 @@ ${lastReview.suggestions.map((s) => `- ${s}`).join("\n")}
|
|
|
2760
2864
|
currentCode = generateResponse.code;
|
|
2761
2865
|
currentDescription = generateResponse.description;
|
|
2762
2866
|
currentConfidence = generateResponse.confidence;
|
|
2867
|
+
if (skipReview) {
|
|
2868
|
+
approved = true;
|
|
2869
|
+
lastReview = { approved: true, score: 0.7, feedback: "\u6E90\u7801\u8F83\u5927\uFF0C\u8DF3\u8FC7\u5BA1\u8BA1", issues: [], suggestions: [] };
|
|
2870
|
+
break;
|
|
2871
|
+
}
|
|
2763
2872
|
onProgress?.(`\u5BA1\u8BA1\u5BA1\u67E5 \u2190 ${targetName}`);
|
|
2764
2873
|
if (process.env.QAT_DEBUG === "true") {
|
|
2765
2874
|
console.error(chalk4.gray(`[DEBUG] \u2500\u2500\u2500 \u5BA1\u8BA1\u5458 AI \u5BA1\u67E5 \u2500\u2500\u2500`));
|
|
@@ -2815,6 +2924,61 @@ function printReviewReport(report) {
|
|
|
2815
2924
|
}
|
|
2816
2925
|
|
|
2817
2926
|
// src/commands/create.ts
|
|
2927
|
+
var MultiProgressBar = class {
|
|
2928
|
+
constructor(labels) {
|
|
2929
|
+
this.lastRenderLines = 0;
|
|
2930
|
+
this.renderScheduled = false;
|
|
2931
|
+
this.labels = labels;
|
|
2932
|
+
this.statuses = labels.map(() => "waiting");
|
|
2933
|
+
this.statusLabels = labels.map(() => "\u7B49\u5F85\u4E2D");
|
|
2934
|
+
this.render();
|
|
2935
|
+
}
|
|
2936
|
+
update(index, status, label) {
|
|
2937
|
+
if (index < 0 || index >= this.labels.length) return;
|
|
2938
|
+
this.statuses[index] = status;
|
|
2939
|
+
this.statusLabels[index] = label;
|
|
2940
|
+
if (!this.renderScheduled) {
|
|
2941
|
+
this.renderScheduled = true;
|
|
2942
|
+
queueMicrotask(() => {
|
|
2943
|
+
this.renderScheduled = false;
|
|
2944
|
+
this.render();
|
|
2945
|
+
});
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
done() {
|
|
2949
|
+
this.render();
|
|
2950
|
+
process.stderr.write("\n");
|
|
2951
|
+
}
|
|
2952
|
+
render() {
|
|
2953
|
+
if (this.lastRenderLines > 0) {
|
|
2954
|
+
process.stderr.write(`\x1B[${this.lastRenderLines}A\x1B[0J`);
|
|
2955
|
+
}
|
|
2956
|
+
const lines = [];
|
|
2957
|
+
for (let i = 0; i < this.labels.length; i++) {
|
|
2958
|
+
const icon = this.getIcon(this.statuses[i]);
|
|
2959
|
+
const label = this.statusLabels[i];
|
|
2960
|
+
lines.push(` ${icon} ${chalk5.bold(this.labels[i])} ${chalk5.gray(label)}`);
|
|
2961
|
+
}
|
|
2962
|
+
process.stderr.write(lines.join("\n") + "\n");
|
|
2963
|
+
this.lastRenderLines = lines.length;
|
|
2964
|
+
}
|
|
2965
|
+
getIcon(status) {
|
|
2966
|
+
switch (status) {
|
|
2967
|
+
case "waiting":
|
|
2968
|
+
return chalk5.gray("\u25CB");
|
|
2969
|
+
case "generating":
|
|
2970
|
+
return chalk5.cyan("\u27F3");
|
|
2971
|
+
case "reviewing":
|
|
2972
|
+
return chalk5.yellow("\u27F3");
|
|
2973
|
+
case "done":
|
|
2974
|
+
return chalk5.green("\u25CF");
|
|
2975
|
+
case "failed":
|
|
2976
|
+
return chalk5.red("\u2717");
|
|
2977
|
+
case "paused":
|
|
2978
|
+
return chalk5.yellow("\u23F8");
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
};
|
|
2818
2982
|
var TEST_TYPE_LABELS = {
|
|
2819
2983
|
unit: "\u5355\u5143\u6D4B\u8BD5",
|
|
2820
2984
|
component: "\u7EC4\u4EF6\u6D4B\u8BD5",
|
|
@@ -2914,59 +3078,114 @@ async function executeCreate(options) {
|
|
|
2914
3078
|
]);
|
|
2915
3079
|
useAI = ai;
|
|
2916
3080
|
}
|
|
3081
|
+
const tasks = [];
|
|
3082
|
+
for (const testType of types) {
|
|
3083
|
+
for (const testTarget of targets) {
|
|
3084
|
+
tasks.push({ testType, target: testTarget });
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
2917
3087
|
const createdFiles = [];
|
|
2918
3088
|
let skippedCount = 0;
|
|
2919
3089
|
const reviewReportEntries = [];
|
|
2920
|
-
|
|
2921
|
-
|
|
2922
|
-
|
|
2923
|
-
|
|
2924
|
-
|
|
2925
|
-
|
|
2926
|
-
|
|
2927
|
-
|
|
2928
|
-
|
|
2929
|
-
|
|
2930
|
-
|
|
2931
|
-
|
|
3090
|
+
if (useAI && tasks.length > 0) {
|
|
3091
|
+
const fileLabels = tasks.map((t) => path9.basename(t.target));
|
|
3092
|
+
const progress = new MultiProgressBar(fileLabels);
|
|
3093
|
+
const results = await Promise.allSettled(
|
|
3094
|
+
tasks.map(async (task, idx) => {
|
|
3095
|
+
progress.update(idx, "generating", "\u751F\u6210\u6D4B\u8BD5\u4EE3\u7801...");
|
|
3096
|
+
const result = await generateWithAI(task.testType, name, task.target, config, (text) => {
|
|
3097
|
+
const isReview = text.includes("\u5BA1\u8BA1");
|
|
3098
|
+
const isRetry = text.includes("\u91CD\u65B0\u751F\u6210");
|
|
3099
|
+
if (isReview) {
|
|
3100
|
+
progress.update(idx, "reviewing", "\u5BA1\u8BA1\u5BA1\u67E5...");
|
|
3101
|
+
} else if (isRetry) {
|
|
3102
|
+
progress.update(idx, "generating", text);
|
|
3103
|
+
} else {
|
|
3104
|
+
progress.update(idx, "generating", text);
|
|
2932
3105
|
}
|
|
3106
|
+
});
|
|
3107
|
+
return { ...task, ...result };
|
|
3108
|
+
})
|
|
3109
|
+
);
|
|
3110
|
+
let approvedCount = 0;
|
|
3111
|
+
let failedCount = 0;
|
|
3112
|
+
for (let i = 0; i < results.length; i++) {
|
|
3113
|
+
const r = results[i];
|
|
3114
|
+
const task = tasks[i];
|
|
3115
|
+
if (r.status === "rejected") {
|
|
3116
|
+
progress.update(i, "failed", r.reason instanceof Error ? r.reason.message : String(r.reason));
|
|
3117
|
+
failedCount++;
|
|
3118
|
+
continue;
|
|
3119
|
+
}
|
|
3120
|
+
const { code, reviewEntry } = r.value;
|
|
3121
|
+
const outputDir = getTestOutputDir(task.testType);
|
|
3122
|
+
const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
|
|
3123
|
+
if (fs8.existsSync(filePath)) {
|
|
3124
|
+
progress.update(i, "paused", "\u6587\u4EF6\u5DF2\u5B58\u5728\uFF0C\u8DF3\u8FC7");
|
|
3125
|
+
skippedCount++;
|
|
3126
|
+
continue;
|
|
3127
|
+
}
|
|
3128
|
+
if (!fs8.existsSync(outputDir)) {
|
|
3129
|
+
fs8.mkdirSync(outputDir, { recursive: true });
|
|
3130
|
+
}
|
|
3131
|
+
fs8.writeFileSync(filePath, code, "utf-8");
|
|
3132
|
+
createdFiles.push({ type: task.testType, filePath });
|
|
3133
|
+
if (reviewEntry) {
|
|
3134
|
+
reviewReportEntries.push(reviewEntry);
|
|
3135
|
+
if (reviewEntry.approved) {
|
|
3136
|
+
progress.update(i, "done", `\u5BA1\u8BA1\u901A\u8FC7 (${(reviewEntry.score * 100).toFixed(0)}%)`);
|
|
3137
|
+
approvedCount++;
|
|
2933
3138
|
} else {
|
|
2934
|
-
|
|
2935
|
-
|
|
2936
|
-
name,
|
|
2937
|
-
target: testTarget || `./${config.project.srcDir}`,
|
|
2938
|
-
framework: projectInfo.framework,
|
|
2939
|
-
vueVersion: projectInfo.vueVersion,
|
|
2940
|
-
typescript: projectInfo.typescript,
|
|
2941
|
-
uiLibrary: projectInfo.uiLibrary,
|
|
2942
|
-
extraImports: projectInfo.componentTestSetup?.extraImports,
|
|
2943
|
-
globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
|
|
2944
|
-
globalStubs: projectInfo.componentTestSetup?.globalStubs,
|
|
2945
|
-
mountOptions: projectInfo.componentTestSetup?.mountOptions,
|
|
2946
|
-
// 注入源码分析结果
|
|
2947
|
-
exports: analysis?.exports.map((e) => ({
|
|
2948
|
-
name: e.name,
|
|
2949
|
-
kind: e.kind,
|
|
2950
|
-
params: e.params,
|
|
2951
|
-
isAsync: e.isAsync,
|
|
2952
|
-
returnType: e.returnType
|
|
2953
|
-
})),
|
|
2954
|
-
props: analysis?.vueAnalysis?.props.map((p) => ({
|
|
2955
|
-
name: p.name,
|
|
2956
|
-
type: p.type,
|
|
2957
|
-
required: p.required,
|
|
2958
|
-
defaultValue: p.defaultValue
|
|
2959
|
-
})),
|
|
2960
|
-
emits: analysis?.vueAnalysis?.emits.map((e) => ({
|
|
2961
|
-
name: e.name,
|
|
2962
|
-
params: e.params
|
|
2963
|
-
})),
|
|
2964
|
-
methods: analysis?.vueAnalysis?.methods,
|
|
2965
|
-
computed: analysis?.vueAnalysis?.computed
|
|
2966
|
-
});
|
|
3139
|
+
progress.update(i, "paused", `\u5BA1\u8BA1\u672A\u901A\u8FC7 (${(reviewEntry.score * 100).toFixed(0)}%) \u2014 \u6682\u505C`);
|
|
3140
|
+
failedCount++;
|
|
2967
3141
|
}
|
|
2968
|
-
|
|
2969
|
-
|
|
3142
|
+
} else {
|
|
3143
|
+
progress.update(i, "done", "\u5B8C\u6210");
|
|
3144
|
+
approvedCount++;
|
|
3145
|
+
}
|
|
3146
|
+
}
|
|
3147
|
+
progress.done();
|
|
3148
|
+
console.log(chalk5.cyan(`
|
|
3149
|
+
\u751F\u6210\u5B8C\u6210: ${chalk5.green(approvedCount + " \u901A\u8FC7")} ${failedCount > 0 ? chalk5.red(failedCount + " \u672A\u901A\u8FC7") : ""} ${skippedCount > 0 ? chalk5.yellow(skippedCount + " \u8DF3\u8FC7") : ""}`));
|
|
3150
|
+
} else {
|
|
3151
|
+
for (const task of tasks) {
|
|
3152
|
+
const spinner = ora3(`\u6B63\u5728\u751F\u6210 ${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}...`).start();
|
|
3153
|
+
try {
|
|
3154
|
+
const analysis = task.target ? analyzeFile(task.target) : void 0;
|
|
3155
|
+
const content = renderTemplate(task.testType, {
|
|
3156
|
+
name,
|
|
3157
|
+
target: task.target || `./${config.project.srcDir}`,
|
|
3158
|
+
framework: projectInfo.framework,
|
|
3159
|
+
vueVersion: projectInfo.vueVersion,
|
|
3160
|
+
typescript: projectInfo.typescript,
|
|
3161
|
+
uiLibrary: projectInfo.uiLibrary,
|
|
3162
|
+
extraImports: projectInfo.componentTestSetup?.extraImports,
|
|
3163
|
+
globalPlugins: projectInfo.componentTestSetup?.globalPlugins,
|
|
3164
|
+
globalStubs: projectInfo.componentTestSetup?.globalStubs,
|
|
3165
|
+
mountOptions: projectInfo.componentTestSetup?.mountOptions,
|
|
3166
|
+
// 注入源码分析结果
|
|
3167
|
+
exports: analysis?.exports.map((e) => ({
|
|
3168
|
+
name: e.name,
|
|
3169
|
+
kind: e.kind,
|
|
3170
|
+
params: e.params,
|
|
3171
|
+
isAsync: e.isAsync,
|
|
3172
|
+
returnType: e.returnType
|
|
3173
|
+
})),
|
|
3174
|
+
props: analysis?.vueAnalysis?.props.map((p) => ({
|
|
3175
|
+
name: p.name,
|
|
3176
|
+
type: p.type,
|
|
3177
|
+
required: p.required,
|
|
3178
|
+
defaultValue: p.defaultValue
|
|
3179
|
+
})),
|
|
3180
|
+
emits: analysis?.vueAnalysis?.emits.map((e) => ({
|
|
3181
|
+
name: e.name,
|
|
3182
|
+
params: e.params
|
|
3183
|
+
})),
|
|
3184
|
+
methods: analysis?.vueAnalysis?.methods,
|
|
3185
|
+
computed: analysis?.vueAnalysis?.computed
|
|
3186
|
+
});
|
|
3187
|
+
const outputDir = getTestOutputDir(task.testType);
|
|
3188
|
+
const filePath = path9.join(outputDir, `${name}.${task.testType === "e2e" ? "spec" : "test"}.ts`);
|
|
2970
3189
|
if (fs8.existsSync(filePath)) {
|
|
2971
3190
|
spinner.warn(`\u6D4B\u8BD5\u6587\u4EF6\u5DF2\u5B58\u5728: ${path9.relative(process.cwd(), filePath)}`);
|
|
2972
3191
|
skippedCount++;
|
|
@@ -2976,10 +3195,10 @@ async function executeCreate(options) {
|
|
|
2976
3195
|
fs8.mkdirSync(outputDir, { recursive: true });
|
|
2977
3196
|
}
|
|
2978
3197
|
fs8.writeFileSync(filePath, content, "utf-8");
|
|
2979
|
-
spinner.succeed(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(
|
|
2980
|
-
createdFiles.push({ type: testType, filePath });
|
|
3198
|
+
spinner.succeed(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)}`);
|
|
3199
|
+
createdFiles.push({ type: task.testType, filePath });
|
|
2981
3200
|
} catch (error) {
|
|
2982
|
-
spinner.fail(`${TEST_TYPE_LABELS[testType]} - ${path9.basename(
|
|
3201
|
+
spinner.fail(`${TEST_TYPE_LABELS[task.testType]} - ${path9.basename(task.target)} \u521B\u5EFA\u5931\u8D25`);
|
|
2983
3202
|
console.log(chalk5.gray(` ${error instanceof Error ? error.message : String(error)}`));
|
|
2984
3203
|
}
|
|
2985
3204
|
}
|
|
@@ -2988,6 +3207,49 @@ async function executeCreate(options) {
|
|
|
2988
3207
|
if (reviewReportEntries.length > 0) {
|
|
2989
3208
|
printReviewReport(reviewReportEntries);
|
|
2990
3209
|
}
|
|
3210
|
+
const failedEntries = reviewReportEntries.filter((e) => !e.approved);
|
|
3211
|
+
if (failedEntries.length > 0 && useAI) {
|
|
3212
|
+
console.log();
|
|
3213
|
+
console.log(chalk5.yellow(` \u23F8 ${failedEntries.length} \u4E2A\u6D4B\u8BD5\u7528\u4F8B\u5BA1\u8BA1\u672A\u901A\u8FC7:`));
|
|
3214
|
+
for (const entry of failedEntries) {
|
|
3215
|
+
console.log(chalk5.gray(` - ${path9.basename(entry.target)} (${(entry.score * 100).toFixed(0)}%) ${entry.feedback}`));
|
|
3216
|
+
}
|
|
3217
|
+
const { retry } = await inquirer2.prompt([
|
|
3218
|
+
{
|
|
3219
|
+
type: "confirm",
|
|
3220
|
+
name: "retry",
|
|
3221
|
+
message: `\u662F\u5426\u91CD\u65B0\u751F\u6210\u8FD9\u4E9B\u672A\u901A\u8FC7\u7684\u6D4B\u8BD5\u7528\u4F8B\uFF1F`,
|
|
3222
|
+
default: false
|
|
3223
|
+
}
|
|
3224
|
+
]);
|
|
3225
|
+
if (retry) {
|
|
3226
|
+
console.log();
|
|
3227
|
+
const retryLabels = failedEntries.map((e) => path9.basename(e.target));
|
|
3228
|
+
const retryProgress = new MultiProgressBar(retryLabels);
|
|
3229
|
+
const retryResults = await Promise.allSettled(
|
|
3230
|
+
failedEntries.map(async (entry, idx) => {
|
|
3231
|
+
retryProgress.update(idx, "generating", "\u91CD\u65B0\u751F\u6210...");
|
|
3232
|
+
const aiResult = await generateWithAI(entry.testType, name, entry.target, config, (text) => {
|
|
3233
|
+
const isReview = text.includes("\u5BA1\u8BA1");
|
|
3234
|
+
retryProgress.update(idx, isReview ? "reviewing" : "generating", text);
|
|
3235
|
+
});
|
|
3236
|
+
const outputDir = getTestOutputDir(entry.testType);
|
|
3237
|
+
const filePath = path9.join(outputDir, `${name}.${entry.testType === "e2e" ? "spec" : "test"}.ts`);
|
|
3238
|
+
fs8.writeFileSync(filePath, aiResult.code, "utf-8");
|
|
3239
|
+
if (aiResult.reviewEntry?.approved) {
|
|
3240
|
+
retryProgress.update(idx, "done", `\u5BA1\u8BA1\u901A\u8FC7 (${(aiResult.reviewEntry.score * 100).toFixed(0)}%)`);
|
|
3241
|
+
} else {
|
|
3242
|
+
retryProgress.update(idx, "failed", aiResult.reviewEntry ? `\u4ECD\u672A\u901A\u8FC7 (${(aiResult.reviewEntry.score * 100).toFixed(0)}%)` : "\u5931\u8D25");
|
|
3243
|
+
}
|
|
3244
|
+
return aiResult;
|
|
3245
|
+
})
|
|
3246
|
+
);
|
|
3247
|
+
retryProgress.done();
|
|
3248
|
+
const retryPassed = retryResults.filter((r) => r.status === "fulfilled" && r.value.reviewEntry?.approved).length;
|
|
3249
|
+
const retryFailed = retryResults.length - retryPassed;
|
|
3250
|
+
console.log(chalk5.cyan(` \u91CD\u8BD5\u5B8C\u6210: ${chalk5.green(retryPassed + " \u901A\u8FC7")} ${retryFailed > 0 ? chalk5.red(retryFailed + " \u4ECD\u9700\u5BA1\u67E5") : ""}`));
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
2991
3253
|
}
|
|
2992
3254
|
async function selectTargets(types, projectInfo, srcDir) {
|
|
2993
3255
|
const allTargets = [];
|
|
@@ -5922,7 +6184,7 @@ async function executeChange(_options) {
|
|
|
5922
6184
|
}
|
|
5923
6185
|
|
|
5924
6186
|
// src/cli.ts
|
|
5925
|
-
var VERSION = "0.3.
|
|
6187
|
+
var VERSION = "0.3.07";
|
|
5926
6188
|
function printLogo() {
|
|
5927
6189
|
const logo = `
|
|
5928
6190
|
${chalk13.bold.cyan(" ___ _ _ _ _ _____ _ _ ")}
|