@angli/unit-test-tool 0.1.4 → 0.1.5
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
CHANGED
|
@@ -201,21 +201,30 @@ testbot schedule --project /path/to/project --interval 5m
|
|
|
201
201
|
testbot start --project /path/to/project --schedule --guard-interval 5m
|
|
202
202
|
```
|
|
203
203
|
|
|
204
|
-
### 8) Claude CLI
|
|
204
|
+
### 8) Claude CLI 项目级配置(必填)
|
|
205
205
|
|
|
206
|
-
|
|
206
|
+
补测执行时会优先读取**待补测项目目录**下的 `.claude/settings.local.json`,并使用其中 `env` 的 `ANTHROPIC_AUTH_TOKEN` / `ANTHROPIC_BASE_URL` 覆盖全局环境变量。
|
|
207
207
|
|
|
208
|
-
|
|
208
|
+
如果目标项目没有配置 `env`,且全局环境变量也不可用,**所有非平凡目标(Vue 组件、HOC 等)的 Claude CLI 调用将全部失败**,导致只能处理 `.d.ts`、`.js` 等简单文件。
|
|
209
|
+
|
|
210
|
+
配置方式:在目标项目的 `.claude/settings.local.json` 中添加 `env` 字段:
|
|
209
211
|
|
|
210
212
|
```json
|
|
211
213
|
{
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
214
|
+
“env”: {
|
|
215
|
+
“ANTHROPIC_AUTH_TOKEN”: “sk-ant-...”,
|
|
216
|
+
“ANTHROPIC_BASE_URL”: “https://api.anthropic.com”
|
|
217
|
+
},
|
|
218
|
+
“permissions”: {
|
|
219
|
+
“allow”: [“Bash(*)”, “Edit(*)”, “Write(*)”, “Read(*)”],
|
|
220
|
+
“deny”: [],
|
|
221
|
+
“ask”: []
|
|
215
222
|
}
|
|
216
223
|
}
|
|
217
224
|
```
|
|
218
225
|
|
|
226
|
+
> **注意**:如果全局环境变量中已正确配置 `ANTHROPIC_AUTH_TOKEN`,可以不写 `env`,工具会使用全局环境变量。
|
|
227
|
+
|
|
219
228
|
### 9) config.json 中的 promptOverrides
|
|
220
229
|
|
|
221
230
|
`testbot init` 写入的 `.unit_test_tool_workspace/config.json` 支持项目级 prompt 覆盖。工具会先使用内置“通用单测策略 + 案例片段库”,再追加这里的覆盖内容。
|
|
@@ -21,6 +21,23 @@ export async function runLoop(ctx, input = {}) {
|
|
|
21
21
|
lastResult = result;
|
|
22
22
|
if (result.targetFile)
|
|
23
23
|
attemptedTargets.add(result.targetFile);
|
|
24
|
+
// 如果当前目标失败(CLI 失败或 no-op),且不是显式指定文件,则标记为跳过并继续下一轮
|
|
25
|
+
const isBlocked = !result.ok && !input.targetFile;
|
|
26
|
+
if (isBlocked) {
|
|
27
|
+
blockedTargets += 1;
|
|
28
|
+
await syncLoopSummary(ctx, {
|
|
29
|
+
iterationCount,
|
|
30
|
+
completedTargets,
|
|
31
|
+
blockedTargets,
|
|
32
|
+
noOpTargets,
|
|
33
|
+
lastCompletedTarget
|
|
34
|
+
});
|
|
35
|
+
if (blockedTargets >= Math.max(1, ctx.limits.maxRetryPerTask))
|
|
36
|
+
break;
|
|
37
|
+
if (iterationCount >= maxIterations)
|
|
38
|
+
break;
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
24
41
|
const coverageSummary = await ctx.reportStore.loadCoverageSummary();
|
|
25
42
|
const currentCoverage = coverageSummary?.coverageSnapshot?.lines;
|
|
26
43
|
lastCoverage = currentCoverage;
|
|
@@ -39,12 +56,7 @@ export async function runLoop(ctx, input = {}) {
|
|
|
39
56
|
});
|
|
40
57
|
continue;
|
|
41
58
|
}
|
|
42
|
-
|
|
43
|
-
noOpTargets += 1;
|
|
44
|
-
}
|
|
45
|
-
else {
|
|
46
|
-
blockedTargets += 1;
|
|
47
|
-
}
|
|
59
|
+
noOpTargets += 1;
|
|
48
60
|
await syncLoopSummary(ctx, {
|
|
49
61
|
iterationCount,
|
|
50
62
|
completedTargets,
|
|
@@ -54,8 +66,6 @@ export async function runLoop(ctx, input = {}) {
|
|
|
54
66
|
});
|
|
55
67
|
if (input.targetFile)
|
|
56
68
|
break;
|
|
57
|
-
if (blockedTargets >= Math.max(1, ctx.limits.maxRetryPerTask))
|
|
58
|
-
break;
|
|
59
69
|
if (noOpTargets >= Math.max(1, ctx.limits.loopThreshold))
|
|
60
70
|
break;
|
|
61
71
|
if (!result.targetFile)
|
|
@@ -118,7 +118,8 @@ export async function runWithClaudeCli(ctx, input = {}) {
|
|
|
118
118
|
summary: `Claude CLI 执行失败:${firstRun.failureCategory ?? 'unknown'}。`
|
|
119
119
|
}, 'cli_run_failed', {
|
|
120
120
|
stdoutPath: firstStdoutPath,
|
|
121
|
-
stderrPath: firstStderrPath
|
|
121
|
+
stderrPath: firstStderrPath,
|
|
122
|
+
failureMessage
|
|
122
123
|
});
|
|
123
124
|
await markRunFailed(ctx, target.task, failureMessage);
|
|
124
125
|
return {
|
|
@@ -244,7 +245,8 @@ export async function runWithClaudeCli(ctx, input = {}) {
|
|
|
244
245
|
summary: `Claude CLI 重试失败:${retryRun.failureCategory ?? 'unknown'}。`
|
|
245
246
|
}, 'cli_run_failed', {
|
|
246
247
|
stdoutPath: retryStdoutSaved,
|
|
247
|
-
stderrPath: retryStderrSaved
|
|
248
|
+
stderrPath: retryStderrSaved,
|
|
249
|
+
failureMessage
|
|
248
250
|
});
|
|
249
251
|
await markRunFailed(ctx, target.task, failureMessage);
|
|
250
252
|
return {
|
|
@@ -503,6 +505,19 @@ async function validateTestChanges(ctx, task, beforeSnapshot) {
|
|
|
503
505
|
relatedTestFiles
|
|
504
506
|
};
|
|
505
507
|
}
|
|
508
|
+
// Snapshot diff 未命中时,兜底检查:是否有新创建的文件匹配目标
|
|
509
|
+
const newCandidateFiles = await detectNewTestFiles(ctx, task);
|
|
510
|
+
if (newCandidateFiles.length > 0) {
|
|
511
|
+
ctx.logger.info(`[run] step=test-change status=new-files taskId=${task.taskId} target=${task.targetFile} new=${newCandidateFiles.join(',')}`);
|
|
512
|
+
return {
|
|
513
|
+
ok: true,
|
|
514
|
+
phase: 'DONE',
|
|
515
|
+
summary: '检测到新创建的测试文件。',
|
|
516
|
+
targetFile: task.targetFile,
|
|
517
|
+
changedTestFiles: newCandidateFiles,
|
|
518
|
+
relatedTestFiles: newCandidateFiles
|
|
519
|
+
};
|
|
520
|
+
}
|
|
506
521
|
ctx.logger.warn(`[run] step=test-change status=noop taskId=${task.taskId} target=${task.targetFile} changed=${changedTestFiles.join(',') || 'none'}`);
|
|
507
522
|
return {
|
|
508
523
|
ok: false,
|
|
@@ -518,6 +533,37 @@ async function validateTestChanges(ctx, task, beforeSnapshot) {
|
|
|
518
533
|
relatedTestFiles
|
|
519
534
|
};
|
|
520
535
|
}
|
|
536
|
+
async function detectNewTestFiles(ctx, task) {
|
|
537
|
+
const patterns = buildTestSnapshotPatterns(ctx.config.testDirNames);
|
|
538
|
+
const files = await fg(patterns, {
|
|
539
|
+
cwd: ctx.projectPath,
|
|
540
|
+
ignore: ctx.config.exclude,
|
|
541
|
+
onlyFiles: true,
|
|
542
|
+
absolute: false
|
|
543
|
+
});
|
|
544
|
+
const baseName = path.basename(task.targetFile, path.extname(task.targetFile));
|
|
545
|
+
const targetDir = path.dirname(task.targetFile);
|
|
546
|
+
const normalizedTargetDir = normalizePath(targetDir);
|
|
547
|
+
const candidates = files.filter((file) => {
|
|
548
|
+
const fileBaseName = path.basename(file, path.extname(file));
|
|
549
|
+
const normalizedFile = normalizePath(file);
|
|
550
|
+
// 匹配同名或同目录下的测试文件
|
|
551
|
+
return (fileBaseName === baseName ||
|
|
552
|
+
fileBaseName === `${baseName}.test` ||
|
|
553
|
+
fileBaseName === `${baseName}.spec` ||
|
|
554
|
+
normalizedFile.startsWith(`${normalizedTargetDir}/`));
|
|
555
|
+
});
|
|
556
|
+
// 只保留确实存在且非空的
|
|
557
|
+
const result = [];
|
|
558
|
+
for (const file of candidates) {
|
|
559
|
+
const fullPath = path.join(ctx.projectPath, file);
|
|
560
|
+
const content = await ctx.fileSystem.readText(fullPath);
|
|
561
|
+
if (content !== undefined && content.trim().length > 0) {
|
|
562
|
+
result.push(file);
|
|
563
|
+
}
|
|
564
|
+
}
|
|
565
|
+
return result.sort();
|
|
566
|
+
}
|
|
521
567
|
function findRelatedTestFiles(task, changedTestFiles) {
|
|
522
568
|
if (changedTestFiles.length === 0)
|
|
523
569
|
return [];
|
package/package.json
CHANGED
|
@@ -40,6 +40,21 @@ export async function runLoop(
|
|
|
40
40
|
|
|
41
41
|
if (result.targetFile) attemptedTargets.add(result.targetFile)
|
|
42
42
|
|
|
43
|
+
// 如果当前目标失败(CLI 失败或 no-op),且不是显式指定文件,则标记为跳过并继续下一轮
|
|
44
|
+
const isBlocked = !result.ok && !input.targetFile
|
|
45
|
+
if (isBlocked) {
|
|
46
|
+
blockedTargets += 1
|
|
47
|
+
await syncLoopSummary(ctx, {
|
|
48
|
+
iterationCount,
|
|
49
|
+
completedTargets,
|
|
50
|
+
blockedTargets,
|
|
51
|
+
noOpTargets,
|
|
52
|
+
lastCompletedTarget
|
|
53
|
+
})
|
|
54
|
+
if (blockedTargets >= Math.max(1, ctx.limits.maxRetryPerTask)) break
|
|
55
|
+
if (iterationCount >= maxIterations) break
|
|
56
|
+
continue
|
|
57
|
+
}
|
|
43
58
|
|
|
44
59
|
const coverageSummary = await ctx.reportStore.loadCoverageSummary<{ coverageSnapshot?: { lines?: number } }>()
|
|
45
60
|
const currentCoverage = coverageSummary?.coverageSnapshot?.lines
|
|
@@ -61,11 +76,7 @@ export async function runLoop(
|
|
|
61
76
|
continue
|
|
62
77
|
}
|
|
63
78
|
|
|
64
|
-
|
|
65
|
-
noOpTargets += 1
|
|
66
|
-
} else {
|
|
67
|
-
blockedTargets += 1
|
|
68
|
-
}
|
|
79
|
+
noOpTargets += 1
|
|
69
80
|
|
|
70
81
|
await syncLoopSummary(ctx, {
|
|
71
82
|
iterationCount,
|
|
@@ -76,7 +87,6 @@ export async function runLoop(
|
|
|
76
87
|
})
|
|
77
88
|
|
|
78
89
|
if (input.targetFile) break
|
|
79
|
-
if (blockedTargets >= Math.max(1, ctx.limits.maxRetryPerTask)) break
|
|
80
90
|
if (noOpTargets >= Math.max(1, ctx.limits.loopThreshold)) break
|
|
81
91
|
if (!result.targetFile) break
|
|
82
92
|
}
|
|
@@ -206,7 +206,8 @@ export async function runWithClaudeCli(
|
|
|
206
206
|
summary: `Claude CLI 执行失败:${firstRun.failureCategory ?? 'unknown'}。`
|
|
207
207
|
}, 'cli_run_failed', {
|
|
208
208
|
stdoutPath: firstStdoutPath,
|
|
209
|
-
stderrPath: firstStderrPath
|
|
209
|
+
stderrPath: firstStderrPath,
|
|
210
|
+
failureMessage
|
|
210
211
|
})
|
|
211
212
|
await markRunFailed(ctx, target.task, failureMessage)
|
|
212
213
|
return {
|
|
@@ -388,7 +389,8 @@ export async function runWithClaudeCli(
|
|
|
388
389
|
summary: `Claude CLI 重试失败:${retryRun.failureCategory ?? 'unknown'}。`
|
|
389
390
|
}, 'cli_run_failed', {
|
|
390
391
|
stdoutPath: retryStdoutSaved,
|
|
391
|
-
stderrPath: retryStderrSaved
|
|
392
|
+
stderrPath: retryStderrSaved,
|
|
393
|
+
failureMessage
|
|
392
394
|
})
|
|
393
395
|
await markRunFailed(ctx, target.task, failureMessage)
|
|
394
396
|
return {
|
|
@@ -742,6 +744,22 @@ async function validateTestChanges(
|
|
|
742
744
|
}
|
|
743
745
|
}
|
|
744
746
|
|
|
747
|
+
// Snapshot diff 未命中时,兜底检查:是否有新创建的文件匹配目标
|
|
748
|
+
const newCandidateFiles = await detectNewTestFiles(ctx, task)
|
|
749
|
+
if (newCandidateFiles.length > 0) {
|
|
750
|
+
ctx.logger.info(
|
|
751
|
+
`[run] step=test-change status=new-files taskId=${task.taskId} target=${task.targetFile} new=${newCandidateFiles.join(',')}`
|
|
752
|
+
)
|
|
753
|
+
return {
|
|
754
|
+
ok: true,
|
|
755
|
+
phase: 'DONE',
|
|
756
|
+
summary: '检测到新创建的测试文件。',
|
|
757
|
+
targetFile: task.targetFile,
|
|
758
|
+
changedTestFiles: newCandidateFiles,
|
|
759
|
+
relatedTestFiles: newCandidateFiles
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
|
|
745
763
|
ctx.logger.warn(
|
|
746
764
|
`[run] step=test-change status=noop taskId=${task.taskId} target=${task.targetFile} changed=${changedTestFiles.join(',') || 'none'}`
|
|
747
765
|
)
|
|
@@ -760,6 +778,43 @@ async function validateTestChanges(
|
|
|
760
778
|
}
|
|
761
779
|
}
|
|
762
780
|
|
|
781
|
+
async function detectNewTestFiles(ctx: AppContext, task: TaskItem): Promise<string[]> {
|
|
782
|
+
const patterns = buildTestSnapshotPatterns(ctx.config.testDirNames)
|
|
783
|
+
const files = await fg(patterns, {
|
|
784
|
+
cwd: ctx.projectPath,
|
|
785
|
+
ignore: ctx.config.exclude,
|
|
786
|
+
onlyFiles: true,
|
|
787
|
+
absolute: false
|
|
788
|
+
})
|
|
789
|
+
|
|
790
|
+
const baseName = path.basename(task.targetFile, path.extname(task.targetFile))
|
|
791
|
+
const targetDir = path.dirname(task.targetFile)
|
|
792
|
+
const normalizedTargetDir = normalizePath(targetDir)
|
|
793
|
+
|
|
794
|
+
const candidates = files.filter((file) => {
|
|
795
|
+
const fileBaseName = path.basename(file, path.extname(file))
|
|
796
|
+
const normalizedFile = normalizePath(file)
|
|
797
|
+
// 匹配同名或同目录下的测试文件
|
|
798
|
+
return (
|
|
799
|
+
fileBaseName === baseName ||
|
|
800
|
+
fileBaseName === `${baseName}.test` ||
|
|
801
|
+
fileBaseName === `${baseName}.spec` ||
|
|
802
|
+
normalizedFile.startsWith(`${normalizedTargetDir}/`)
|
|
803
|
+
)
|
|
804
|
+
})
|
|
805
|
+
|
|
806
|
+
// 只保留确实存在且非空的
|
|
807
|
+
const result: string[] = []
|
|
808
|
+
for (const file of candidates) {
|
|
809
|
+
const fullPath = path.join(ctx.projectPath, file)
|
|
810
|
+
const content = await ctx.fileSystem.readText(fullPath)
|
|
811
|
+
if (content !== undefined && content.trim().length > 0) {
|
|
812
|
+
result.push(file)
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
return result.sort()
|
|
816
|
+
}
|
|
817
|
+
|
|
763
818
|
function findRelatedTestFiles(task: TaskItem, changedTestFiles: string[]): string[] {
|
|
764
819
|
if (changedTestFiles.length === 0) return []
|
|
765
820
|
|