@angli/unit-test-tool 0.1.0
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/.claude/settings.local.json +14 -0
- package/README.md +232 -0
- package/dist/src/cli/commands/analyze.js +20 -0
- package/dist/src/cli/commands/guard.js +26 -0
- package/dist/src/cli/commands/init.js +39 -0
- package/dist/src/cli/commands/run.js +72 -0
- package/dist/src/cli/commands/schedule.js +29 -0
- package/dist/src/cli/commands/start.js +102 -0
- package/dist/src/cli/commands/status.js +27 -0
- package/dist/src/cli/commands/verify.js +15 -0
- package/dist/src/cli/context/create-context.js +101 -0
- package/dist/src/cli/index.js +23 -0
- package/dist/src/cli/utils/scan-dir.js +6 -0
- package/dist/src/core/analyzers/coverage-analyzer.js +8 -0
- package/dist/src/core/analyzers/dependency-complexity-analyzer.js +19 -0
- package/dist/src/core/analyzers/existing-test-analyzer.js +66 -0
- package/dist/src/core/analyzers/failure-history-analyzer.js +9 -0
- package/dist/src/core/analyzers/file-classifier-analyzer.js +24 -0
- package/dist/src/core/analyzers/index.js +37 -0
- package/dist/src/core/analyzers/llm-semantic-analyzer.js +3 -0
- package/dist/src/core/analyzers/path-priority-analyzer.js +34 -0
- package/dist/src/core/coverage/read-coverage-summary.js +183 -0
- package/dist/src/core/executor/claude-cli-executor.js +91 -0
- package/dist/src/core/middleware/loop-detection.js +6 -0
- package/dist/src/core/middleware/pre-completion-checklist.js +27 -0
- package/dist/src/core/middleware/silent-success-post-check.js +13 -0
- package/dist/src/core/planner/rank-candidates.js +3 -0
- package/dist/src/core/planner/rule-planner.js +41 -0
- package/dist/src/core/planner/score-candidate.js +49 -0
- package/dist/src/core/prompts/case-library.js +35 -0
- package/dist/src/core/prompts/edit-boundary-prompt.js +24 -0
- package/dist/src/core/prompts/retry-prompt.js +18 -0
- package/dist/src/core/prompts/system-prompt.js +27 -0
- package/dist/src/core/prompts/task-prompt.js +22 -0
- package/dist/src/core/reporter/index.js +48 -0
- package/dist/src/core/state-machine/index.js +12 -0
- package/dist/src/core/storage/defaults.js +16 -0
- package/dist/src/core/storage/event-store.js +16 -0
- package/dist/src/core/storage/lifecycle-store.js +18 -0
- package/dist/src/core/storage/report-store.js +16 -0
- package/dist/src/core/storage/state-store.js +11 -0
- package/dist/src/core/strategies/classify-failure.js +14 -0
- package/dist/src/core/strategies/switch-mock-strategy.js +9 -0
- package/dist/src/core/tools/analyze-baseline.js +54 -0
- package/dist/src/core/tools/guard.js +68 -0
- package/dist/src/core/tools/run-loop.js +108 -0
- package/dist/src/core/tools/run-with-claude-cli.js +645 -0
- package/dist/src/core/tools/verify-all.js +75 -0
- package/dist/src/core/worktrees/is-git-repo.js +10 -0
- package/dist/src/types/index.js +1 -0
- package/dist/src/types/logger.js +1 -0
- package/dist/src/utils/clock.js +10 -0
- package/dist/src/utils/command-runner.js +18 -0
- package/dist/src/utils/commands.js +28 -0
- package/dist/src/utils/duration.js +22 -0
- package/dist/src/utils/fs.js +53 -0
- package/dist/src/utils/logger.js +10 -0
- package/dist/src/utils/paths.js +21 -0
- package/dist/src/utils/process-lifecycle.js +74 -0
- package/dist/src/utils/prompts.js +20 -0
- package/dist/tests/core/create-context.test.js +41 -0
- package/dist/tests/core/default-state.test.js +10 -0
- package/dist/tests/core/failure-classification.test.js +7 -0
- package/dist/tests/core/loop-detection.test.js +7 -0
- package/dist/tests/core/paths.test.js +11 -0
- package/dist/tests/core/prompt-builders.test.js +33 -0
- package/dist/tests/core/score-candidate.test.js +28 -0
- package/dist/tests/core/state-machine.test.js +12 -0
- package/dist/tests/integration/status-report.test.js +21 -0
- package/docs/architecture.md +20 -0
- package/docs/demo.sh +266 -0
- package/docs/skill-integration.md +15 -0
- package/docs/state-machine.md +15 -0
- package/package.json +31 -0
- package/src/cli/commands/analyze.ts +22 -0
- package/src/cli/commands/guard.ts +28 -0
- package/src/cli/commands/init.ts +41 -0
- package/src/cli/commands/run.ts +79 -0
- package/src/cli/commands/schedule.ts +32 -0
- package/src/cli/commands/start.ts +111 -0
- package/src/cli/commands/status.ts +30 -0
- package/src/cli/commands/verify.ts +17 -0
- package/src/cli/context/create-context.ts +142 -0
- package/src/cli/index.ts +27 -0
- package/src/cli/utils/scan-dir.ts +5 -0
- package/src/core/analyzers/coverage-analyzer.ts +10 -0
- package/src/core/analyzers/dependency-complexity-analyzer.ts +25 -0
- package/src/core/analyzers/existing-test-analyzer.ts +76 -0
- package/src/core/analyzers/failure-history-analyzer.ts +12 -0
- package/src/core/analyzers/file-classifier-analyzer.ts +25 -0
- package/src/core/analyzers/index.ts +51 -0
- package/src/core/analyzers/llm-semantic-analyzer.ts +6 -0
- package/src/core/analyzers/path-priority-analyzer.ts +41 -0
- package/src/core/coverage/read-coverage-summary.ts +224 -0
- package/src/core/executor/claude-cli-executor.ts +94 -0
- package/src/core/middleware/loop-detection.ts +8 -0
- package/src/core/middleware/pre-completion-checklist.ts +32 -0
- package/src/core/middleware/silent-success-post-check.ts +16 -0
- package/src/core/planner/rank-candidates.ts +5 -0
- package/src/core/planner/rule-planner.ts +65 -0
- package/src/core/planner/score-candidate.ts +60 -0
- package/src/core/prompts/case-library.ts +36 -0
- package/src/core/prompts/edit-boundary-prompt.ts +26 -0
- package/src/core/prompts/retry-prompt.ts +22 -0
- package/src/core/prompts/system-prompt.ts +32 -0
- package/src/core/prompts/task-prompt.ts +26 -0
- package/src/core/reporter/index.ts +56 -0
- package/src/core/state-machine/index.ts +14 -0
- package/src/core/storage/defaults.ts +18 -0
- package/src/core/storage/event-store.ts +18 -0
- package/src/core/storage/lifecycle-store.ts +20 -0
- package/src/core/storage/report-store.ts +19 -0
- package/src/core/storage/state-store.ts +18 -0
- package/src/core/strategies/classify-failure.ts +9 -0
- package/src/core/strategies/switch-mock-strategy.ts +12 -0
- package/src/core/tools/analyze-baseline.ts +61 -0
- package/src/core/tools/guard.ts +89 -0
- package/src/core/tools/run-loop.ts +142 -0
- package/src/core/tools/run-with-claude-cli.ts +926 -0
- package/src/core/tools/verify-all.ts +83 -0
- package/src/core/worktrees/is-git-repo.ts +10 -0
- package/src/types/index.ts +291 -0
- package/src/types/logger.ts +6 -0
- package/src/utils/clock.ts +10 -0
- package/src/utils/command-runner.ts +24 -0
- package/src/utils/commands.ts +42 -0
- package/src/utils/duration.ts +20 -0
- package/src/utils/fs.ts +50 -0
- package/src/utils/logger.ts +12 -0
- package/src/utils/paths.ts +24 -0
- package/src/utils/process-lifecycle.ts +92 -0
- package/src/utils/prompts.ts +22 -0
- package/tests/core/create-context.test.ts +45 -0
- package/tests/core/default-state.test.ts +11 -0
- package/tests/core/failure-classification.test.ts +8 -0
- package/tests/core/loop-detection.test.ts +8 -0
- package/tests/core/paths.test.ts +13 -0
- package/tests/core/prompt-builders.test.ts +38 -0
- package/tests/core/score-candidate.test.ts +30 -0
- package/tests/core/state-machine.test.ts +14 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/logs/events.jsonl +10 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/plan.json +75 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/reports/coverage-summary.json +9 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/reports/final-report.json +14 -0
- package/tests/fixtures/simple-project/.openclaw-testbot/state.json +18 -0
- package/tests/fixtures/simple-project/coverage-summary.json +1 -0
- package/tests/fixtures/simple-project/package.json +8 -0
- package/tests/fixtures/simple-project/src/add.js +3 -0
- package/tests/fixtures/simple-project/test-runner.js +18 -0
- package/tests/integration/status-report.test.ts +24 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,645 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import fg from 'fast-glob';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { executeClaudeCli } from '../executor/claude-cli-executor.js';
|
|
5
|
+
import { verifyAll } from './verify-all.js';
|
|
6
|
+
import { rulePlanner } from '../planner/rule-planner.js';
|
|
7
|
+
import { buildFinalReport } from '../reporter/index.js';
|
|
8
|
+
export async function runWithClaudeCli(ctx, input = {}) {
|
|
9
|
+
ctx.logger.info('[run] step=select-target status=start');
|
|
10
|
+
const coverageSummary = await ctx.reportStore.loadCoverageSummary();
|
|
11
|
+
const target = input.targetFile
|
|
12
|
+
? await buildTaskFromExplicitTarget(ctx, input.targetFile)
|
|
13
|
+
: await pickRecommendedTask(ctx, input.topN ?? 5, coverageSummary?.coverageSnapshot, input.excludeTargets);
|
|
14
|
+
if (!target) {
|
|
15
|
+
ctx.logger.warn('[run] step=select-target status=empty');
|
|
16
|
+
return {
|
|
17
|
+
ok: false,
|
|
18
|
+
phase: 'WRITING_TESTS',
|
|
19
|
+
summary: '没有找到可执行的补测目标。请先运行 analyze 或通过 --file 指定目标文件。',
|
|
20
|
+
nextAction: 'analyze'
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
const beforeSnapshot = await captureTestSnapshot(ctx);
|
|
24
|
+
const firstAttemptTimestamp = ctx.clock.nowIso();
|
|
25
|
+
const promptText = buildExecutionPrompt(ctx, target.task);
|
|
26
|
+
const promptPath = await persistPrompt(ctx, target.task.taskId, 1, promptText, target.task.targetFile, firstAttemptTimestamp);
|
|
27
|
+
const stdoutPath = buildTaskOutputPath(ctx, target.task.taskId, 1, 'stdout', target.task.targetFile, firstAttemptTimestamp);
|
|
28
|
+
const stderrPath = buildTaskOutputPath(ctx, target.task.taskId, 1, 'stderr', target.task.targetFile, firstAttemptTimestamp);
|
|
29
|
+
const runPromptPath = await appendRunPrompt(ctx, target.task.taskId, 1, target.task.targetFile, firstAttemptTimestamp, promptPath);
|
|
30
|
+
await appendRunEvent(ctx, {
|
|
31
|
+
taskId: target.task.taskId,
|
|
32
|
+
targetFile: target.task.targetFile,
|
|
33
|
+
attemptCount: 1,
|
|
34
|
+
promptPath,
|
|
35
|
+
status: 'passed',
|
|
36
|
+
summary: 'prompt recorded'
|
|
37
|
+
}, 'cli_run_attempt', {
|
|
38
|
+
promptPreview: buildPreview(promptText),
|
|
39
|
+
stdoutPath,
|
|
40
|
+
stderrPath,
|
|
41
|
+
runPromptPath
|
|
42
|
+
});
|
|
43
|
+
ctx.logger.info(`[run] step=select-target status=ok taskId=${target.task.taskId} target=${target.task.targetFile}`);
|
|
44
|
+
await ctx.eventStore.append({
|
|
45
|
+
eventType: 'cli_run_started',
|
|
46
|
+
phase: 'WRITING_TESTS',
|
|
47
|
+
taskId: target.task.taskId,
|
|
48
|
+
message: `selected ${target.task.targetFile}`,
|
|
49
|
+
data: {
|
|
50
|
+
targetFile: target.task.targetFile,
|
|
51
|
+
recommendedTargets: target.recommendedTargets,
|
|
52
|
+
promptPath,
|
|
53
|
+
attemptCount: 1
|
|
54
|
+
},
|
|
55
|
+
timestamp: ctx.clock.nowIso()
|
|
56
|
+
});
|
|
57
|
+
await ctx.stateStore.save({
|
|
58
|
+
phase: 'WRITING_TESTS',
|
|
59
|
+
status: 'running',
|
|
60
|
+
currentTaskId: target.task.taskId,
|
|
61
|
+
coverageSnapshot: undefined,
|
|
62
|
+
totals: {
|
|
63
|
+
pending: 0,
|
|
64
|
+
running: 1,
|
|
65
|
+
passed: 0,
|
|
66
|
+
failed: 0,
|
|
67
|
+
blocked: 0
|
|
68
|
+
},
|
|
69
|
+
lastAction: 'run',
|
|
70
|
+
nextSuggestedAction: 'verify',
|
|
71
|
+
updatedAt: ctx.clock.nowIso()
|
|
72
|
+
});
|
|
73
|
+
ctx.logger.info(`[run] step=claude-cli status=start attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile}`);
|
|
74
|
+
const firstRun = await executeClaudeCli({
|
|
75
|
+
cwd: ctx.projectPath,
|
|
76
|
+
prompt: promptText,
|
|
77
|
+
allowedTools: input.allowedTools,
|
|
78
|
+
permissionMode: input.permissionMode,
|
|
79
|
+
outputFormat: 'text',
|
|
80
|
+
timeoutMs: input.timeoutMs,
|
|
81
|
+
onProgress: (elapsedMs) => {
|
|
82
|
+
ctx.logger.info(`[run] step=claude-cli status=waiting attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} elapsedMs=${elapsedMs}`);
|
|
83
|
+
},
|
|
84
|
+
taskMeta: {
|
|
85
|
+
taskId: target.task.taskId,
|
|
86
|
+
targetFile: target.task.targetFile,
|
|
87
|
+
strategy: target.task.strategy,
|
|
88
|
+
attemptCount: 1
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
const firstStdoutPath = await persistOutput(ctx, target.task.taskId, 1, 'stdout', target.task.targetFile, firstAttemptTimestamp, firstRun.stdout);
|
|
92
|
+
const firstStderrPath = await persistOutput(ctx, target.task.taskId, 1, 'stderr', target.task.targetFile, firstAttemptTimestamp, firstRun.stderr);
|
|
93
|
+
if (firstRun.stdout?.trim()) {
|
|
94
|
+
ctx.logger.info(`[run] step=claude-cli status=stdout attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} stdout=${truncateLog(firstRun.stdout)}`);
|
|
95
|
+
}
|
|
96
|
+
if (firstRun.stderr?.trim()) {
|
|
97
|
+
ctx.logger.warn(`[run] step=claude-cli status=stderr attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} stderr=${truncateLog(firstRun.stderr)}`);
|
|
98
|
+
}
|
|
99
|
+
if (!firstRun.ok) {
|
|
100
|
+
const stderrText = firstRun.stderr?.trim() || '';
|
|
101
|
+
const stdoutText = firstRun.stdout?.trim() || '';
|
|
102
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} category=${firstRun.failureCategory ?? 'unknown'}`);
|
|
103
|
+
if (stderrText) {
|
|
104
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} stderr=${truncateLog(stderrText)}`);
|
|
105
|
+
}
|
|
106
|
+
if (stdoutText) {
|
|
107
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} stdout=${truncateLog(stdoutText)}`);
|
|
108
|
+
}
|
|
109
|
+
const failureMessage = stderrText || stdoutText || 'unknown error';
|
|
110
|
+
await appendRunEvent(ctx, {
|
|
111
|
+
taskId: target.task.taskId,
|
|
112
|
+
targetFile: target.task.targetFile,
|
|
113
|
+
attemptCount: 1,
|
|
114
|
+
promptPath,
|
|
115
|
+
status: 'cli_failed',
|
|
116
|
+
stdout: stdoutText || undefined,
|
|
117
|
+
stderr: stderrText || undefined,
|
|
118
|
+
summary: `Claude CLI 执行失败:${firstRun.failureCategory ?? 'unknown'}。`
|
|
119
|
+
}, 'cli_run_failed', {
|
|
120
|
+
stdoutPath: firstStdoutPath,
|
|
121
|
+
stderrPath: firstStderrPath
|
|
122
|
+
});
|
|
123
|
+
await markRunFailed(ctx, target.task, failureMessage);
|
|
124
|
+
return {
|
|
125
|
+
ok: false,
|
|
126
|
+
phase: 'WRITING_TESTS',
|
|
127
|
+
summary: `Claude CLI 执行失败:${firstRun.failureCategory ?? 'unknown'}。`,
|
|
128
|
+
nextAction: 'manual-intervention',
|
|
129
|
+
targetFile: target.task.targetFile,
|
|
130
|
+
artifacts: {
|
|
131
|
+
stderr: stderrText || undefined,
|
|
132
|
+
stdout: stdoutText || undefined
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
ctx.logger.info(`[run] step=claude-cli status=done attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile} elapsedMs=${firstRun.durationMs}`);
|
|
137
|
+
ctx.logger.info('[run] step=verify status=start attempt=1');
|
|
138
|
+
const verifyResult = await verifyAll(ctx);
|
|
139
|
+
if (verifyResult.ok) {
|
|
140
|
+
ctx.logger.info('[run] step=verify status=ok attempt=1');
|
|
141
|
+
const noOpResult = await validateTestChanges(ctx, target.task, beforeSnapshot);
|
|
142
|
+
if (!noOpResult.ok) {
|
|
143
|
+
await appendRunEvent(ctx, {
|
|
144
|
+
taskId: target.task.taskId,
|
|
145
|
+
targetFile: target.task.targetFile,
|
|
146
|
+
attemptCount: 1,
|
|
147
|
+
promptPath,
|
|
148
|
+
changedTestFiles: noOpResult.changedTestFiles,
|
|
149
|
+
relatedTestFiles: noOpResult.relatedTestFiles,
|
|
150
|
+
verifyResult: 'passed',
|
|
151
|
+
status: 'noop',
|
|
152
|
+
summary: noOpResult.summary
|
|
153
|
+
}, 'cli_run_failed', {
|
|
154
|
+
stdoutPath: firstStdoutPath,
|
|
155
|
+
stderrPath: firstStderrPath
|
|
156
|
+
});
|
|
157
|
+
await markRunFailed(ctx, target.task, noOpResult.summary);
|
|
158
|
+
return noOpResult;
|
|
159
|
+
}
|
|
160
|
+
await appendRunEvent(ctx, {
|
|
161
|
+
taskId: target.task.taskId,
|
|
162
|
+
targetFile: target.task.targetFile,
|
|
163
|
+
attemptCount: 1,
|
|
164
|
+
promptPath,
|
|
165
|
+
changedTestFiles: noOpResult.changedTestFiles,
|
|
166
|
+
relatedTestFiles: noOpResult.relatedTestFiles,
|
|
167
|
+
verifyResult: 'passed',
|
|
168
|
+
status: 'passed',
|
|
169
|
+
summary: `Claude CLI 已完成 ${target.task.targetFile} 的补测并通过全量验证。`
|
|
170
|
+
}, 'cli_run_completed', {
|
|
171
|
+
stdoutPath: firstStdoutPath,
|
|
172
|
+
stderrPath: firstStderrPath
|
|
173
|
+
});
|
|
174
|
+
await markRunPassed(ctx, target.task);
|
|
175
|
+
return await buildSuccessResult(ctx, target.task.targetFile, target.recommendedTargets, noOpResult.changedTestFiles, noOpResult.relatedTestFiles);
|
|
176
|
+
}
|
|
177
|
+
ctx.logger.warn(`[run] step=verify status=failed attempt=1 taskId=${target.task.taskId} target=${target.task.targetFile}`);
|
|
178
|
+
ctx.logger.info(`[run] step=claude-cli status=start attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile}`);
|
|
179
|
+
const retryAttemptTimestamp = ctx.clock.nowIso();
|
|
180
|
+
const retryPromptText = buildRetryExecutionPrompt(ctx, target.task, verifyResult.artifacts?.stderr);
|
|
181
|
+
const retryPromptPath = await persistPrompt(ctx, target.task.taskId, 2, retryPromptText, target.task.targetFile, retryAttemptTimestamp);
|
|
182
|
+
const retryStdoutPath = buildTaskOutputPath(ctx, target.task.taskId, 2, 'stdout', target.task.targetFile, retryAttemptTimestamp);
|
|
183
|
+
const retryStderrPath = buildTaskOutputPath(ctx, target.task.taskId, 2, 'stderr', target.task.targetFile, retryAttemptTimestamp);
|
|
184
|
+
const retryRunPromptPath = await appendRunPrompt(ctx, target.task.taskId, 2, target.task.targetFile, retryAttemptTimestamp, retryPromptPath);
|
|
185
|
+
await appendRunEvent(ctx, {
|
|
186
|
+
taskId: target.task.taskId,
|
|
187
|
+
targetFile: target.task.targetFile,
|
|
188
|
+
attemptCount: 2,
|
|
189
|
+
promptPath: retryPromptPath,
|
|
190
|
+
status: 'passed',
|
|
191
|
+
verifyResult: 'failed',
|
|
192
|
+
summary: 'retry prompt recorded'
|
|
193
|
+
}, 'cli_run_attempt', {
|
|
194
|
+
promptPreview: buildPreview(retryPromptText),
|
|
195
|
+
stdoutPath: retryStdoutPath,
|
|
196
|
+
stderrPath: retryStderrPath,
|
|
197
|
+
runPromptPath: retryRunPromptPath
|
|
198
|
+
});
|
|
199
|
+
const retryRun = await executeClaudeCli({
|
|
200
|
+
cwd: ctx.projectPath,
|
|
201
|
+
prompt: retryPromptText,
|
|
202
|
+
allowedTools: input.allowedTools,
|
|
203
|
+
permissionMode: input.permissionMode,
|
|
204
|
+
outputFormat: 'text',
|
|
205
|
+
timeoutMs: input.timeoutMs,
|
|
206
|
+
onProgress: (elapsedMs) => {
|
|
207
|
+
ctx.logger.info(`[run] step=claude-cli status=waiting attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} elapsedMs=${elapsedMs}`);
|
|
208
|
+
},
|
|
209
|
+
taskMeta: {
|
|
210
|
+
taskId: target.task.taskId,
|
|
211
|
+
targetFile: target.task.targetFile,
|
|
212
|
+
strategy: target.task.strategy,
|
|
213
|
+
attemptCount: 2
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
const retryStdoutSaved = await persistOutput(ctx, target.task.taskId, 2, 'stdout', target.task.targetFile, retryAttemptTimestamp, retryRun.stdout);
|
|
217
|
+
const retryStderrSaved = await persistOutput(ctx, target.task.taskId, 2, 'stderr', target.task.targetFile, retryAttemptTimestamp, retryRun.stderr);
|
|
218
|
+
if (retryRun.stdout?.trim()) {
|
|
219
|
+
ctx.logger.info(`[run] step=claude-cli status=stdout attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} stdout=${truncateLog(retryRun.stdout)}`);
|
|
220
|
+
}
|
|
221
|
+
if (retryRun.stderr?.trim()) {
|
|
222
|
+
ctx.logger.warn(`[run] step=claude-cli status=stderr attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} stderr=${truncateLog(retryRun.stderr)}`);
|
|
223
|
+
}
|
|
224
|
+
if (!retryRun.ok) {
|
|
225
|
+
const stderrText = retryRun.stderr?.trim() || '';
|
|
226
|
+
const stdoutText = retryRun.stdout?.trim() || '';
|
|
227
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} category=${retryRun.failureCategory ?? 'unknown'}`);
|
|
228
|
+
if (stderrText) {
|
|
229
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} stderr=${truncateLog(stderrText)}`);
|
|
230
|
+
}
|
|
231
|
+
if (stdoutText) {
|
|
232
|
+
ctx.logger.error(`[run] step=claude-cli status=failed attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} stdout=${truncateLog(stdoutText)}`);
|
|
233
|
+
}
|
|
234
|
+
const failureMessage = stderrText || stdoutText || 'unknown error';
|
|
235
|
+
await appendRunEvent(ctx, {
|
|
236
|
+
taskId: target.task.taskId,
|
|
237
|
+
targetFile: target.task.targetFile,
|
|
238
|
+
attemptCount: 2,
|
|
239
|
+
promptPath: retryPromptPath,
|
|
240
|
+
status: 'cli_failed',
|
|
241
|
+
verifyResult: 'failed',
|
|
242
|
+
stdout: stdoutText || undefined,
|
|
243
|
+
stderr: stderrText || undefined,
|
|
244
|
+
summary: `Claude CLI 重试失败:${retryRun.failureCategory ?? 'unknown'}。`
|
|
245
|
+
}, 'cli_run_failed', {
|
|
246
|
+
stdoutPath: retryStdoutSaved,
|
|
247
|
+
stderrPath: retryStderrSaved
|
|
248
|
+
});
|
|
249
|
+
await markRunFailed(ctx, target.task, failureMessage);
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
phase: 'WRITING_TESTS',
|
|
253
|
+
summary: `Claude CLI 重试失败:${retryRun.failureCategory ?? 'unknown'}。`,
|
|
254
|
+
nextAction: 'manual-intervention',
|
|
255
|
+
targetFile: target.task.targetFile,
|
|
256
|
+
artifacts: {
|
|
257
|
+
stderr: stderrText || undefined,
|
|
258
|
+
stdout: stdoutText || undefined
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
ctx.logger.info(`[run] step=claude-cli status=done attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile} elapsedMs=${retryRun.durationMs}`);
|
|
263
|
+
ctx.logger.info('[run] step=verify status=start attempt=2');
|
|
264
|
+
const retriedVerify = await verifyAll(ctx);
|
|
265
|
+
if (!retriedVerify.ok) {
|
|
266
|
+
ctx.logger.error(`[run] step=verify status=failed attempt=2 taskId=${target.task.taskId} target=${target.task.targetFile}`);
|
|
267
|
+
await appendRunEvent(ctx, {
|
|
268
|
+
taskId: target.task.taskId,
|
|
269
|
+
targetFile: target.task.targetFile,
|
|
270
|
+
attemptCount: 2,
|
|
271
|
+
promptPath: retryPromptPath,
|
|
272
|
+
status: 'verify_failed',
|
|
273
|
+
verifyResult: 'failed',
|
|
274
|
+
summary: 'Claude CLI 已重试一次,但全量验证仍失败。'
|
|
275
|
+
}, 'cli_run_failed', {
|
|
276
|
+
stdoutPath: retryStdoutSaved,
|
|
277
|
+
stderrPath: retryStderrSaved
|
|
278
|
+
});
|
|
279
|
+
await markRunFailed(ctx, target.task, retriedVerify.artifacts?.stderr);
|
|
280
|
+
return {
|
|
281
|
+
ok: false,
|
|
282
|
+
phase: 'VERIFYING_FULL',
|
|
283
|
+
summary: 'Claude CLI 已重试一次,但全量验证仍失败。',
|
|
284
|
+
nextAction: 'manual-intervention',
|
|
285
|
+
targetFile: target.task.targetFile,
|
|
286
|
+
artifacts: retriedVerify.artifacts
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
const noOpResult = await validateTestChanges(ctx, target.task, beforeSnapshot);
|
|
290
|
+
if (!noOpResult.ok) {
|
|
291
|
+
await appendRunEvent(ctx, {
|
|
292
|
+
taskId: target.task.taskId,
|
|
293
|
+
targetFile: target.task.targetFile,
|
|
294
|
+
attemptCount: 2,
|
|
295
|
+
promptPath: retryPromptPath,
|
|
296
|
+
changedTestFiles: noOpResult.changedTestFiles,
|
|
297
|
+
relatedTestFiles: noOpResult.relatedTestFiles,
|
|
298
|
+
verifyResult: 'passed',
|
|
299
|
+
status: 'noop',
|
|
300
|
+
summary: noOpResult.summary
|
|
301
|
+
}, 'cli_run_failed', {
|
|
302
|
+
stdoutPath: retryStdoutSaved,
|
|
303
|
+
stderrPath: retryStderrSaved
|
|
304
|
+
});
|
|
305
|
+
await markRunFailed(ctx, target.task, noOpResult.summary);
|
|
306
|
+
return noOpResult;
|
|
307
|
+
}
|
|
308
|
+
await appendRunEvent(ctx, {
|
|
309
|
+
taskId: target.task.taskId,
|
|
310
|
+
targetFile: target.task.targetFile,
|
|
311
|
+
attemptCount: 2,
|
|
312
|
+
promptPath: retryPromptPath,
|
|
313
|
+
changedTestFiles: noOpResult.changedTestFiles,
|
|
314
|
+
relatedTestFiles: noOpResult.relatedTestFiles,
|
|
315
|
+
verifyResult: 'passed',
|
|
316
|
+
status: 'passed',
|
|
317
|
+
summary: `Claude CLI 已完成 ${target.task.targetFile} 的补测并通过全量验证。`
|
|
318
|
+
}, 'cli_run_completed', {
|
|
319
|
+
stdoutPath: retryStdoutSaved,
|
|
320
|
+
stderrPath: retryStderrSaved
|
|
321
|
+
});
|
|
322
|
+
await markRunPassed(ctx, target.task);
|
|
323
|
+
return await buildSuccessResult(ctx, target.task.targetFile, target.recommendedTargets, noOpResult.changedTestFiles, noOpResult.relatedTestFiles);
|
|
324
|
+
}
|
|
325
|
+
async function pickRecommendedTask(ctx, topN, coverageSnapshot, excludeTargets = []) {
|
|
326
|
+
const testDirNames = await detectTestDirNames(ctx);
|
|
327
|
+
ctx.logger.info('[run] step=scan status=start');
|
|
328
|
+
ctx.logger.debug('[run] step=scan params', { include: ctx.config.include, exclude: ctx.config.exclude });
|
|
329
|
+
const files = await fg(ctx.config.include, {
|
|
330
|
+
cwd: ctx.projectPath,
|
|
331
|
+
ignore: ctx.config.exclude,
|
|
332
|
+
onlyFiles: true,
|
|
333
|
+
absolute: false
|
|
334
|
+
});
|
|
335
|
+
const candidates = files
|
|
336
|
+
.filter((file) => !isTestFile(file) && !excludeTargets.includes(file))
|
|
337
|
+
.map((file) => ({ targetFile: file }));
|
|
338
|
+
ctx.logger.info(`[run] step=scan status=ok total=${files.length} candidates=${candidates.length} topN=${topN}`);
|
|
339
|
+
ctx.logger.info(`[run] step=rank status=start candidates=${candidates.length}`);
|
|
340
|
+
const ranked = await rulePlanner(ctx, candidates, coverageSnapshot);
|
|
341
|
+
const shortlist = ranked.slice(0, topN);
|
|
342
|
+
const selected = shortlist[0];
|
|
343
|
+
if (!selected)
|
|
344
|
+
return undefined;
|
|
345
|
+
return {
|
|
346
|
+
task: toTaskItem(selected, testDirNames),
|
|
347
|
+
recommendedTargets: shortlist.map((item) => item.targetFile)
|
|
348
|
+
};
|
|
349
|
+
}
|
|
350
|
+
async function buildTaskFromExplicitTarget(ctx, targetFile) {
|
|
351
|
+
const testDirNames = await detectTestDirNames(ctx);
|
|
352
|
+
const ranked = await rulePlanner(ctx, [{ targetFile }]);
|
|
353
|
+
const selected = ranked[0];
|
|
354
|
+
if (!selected)
|
|
355
|
+
return undefined;
|
|
356
|
+
return {
|
|
357
|
+
task: toTaskItem(selected, testDirNames),
|
|
358
|
+
recommendedTargets: [selected.targetFile]
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
function toTaskItem(candidate, testDirNames) {
|
|
362
|
+
return {
|
|
363
|
+
taskId: candidate.taskId,
|
|
364
|
+
targetFile: candidate.targetFile,
|
|
365
|
+
testFiles: candidate.analyzerFacts.existingTest.testFilePaths,
|
|
366
|
+
score: candidate.baseScore,
|
|
367
|
+
reasons: candidate.reasons,
|
|
368
|
+
estimatedDifficulty: candidate.estimatedDifficulty,
|
|
369
|
+
strategy: candidate.suggestedStrategy ?? 'pure-logic',
|
|
370
|
+
status: 'pending',
|
|
371
|
+
attemptCount: 0,
|
|
372
|
+
testDirNames,
|
|
373
|
+
analyzerFacts: candidate.analyzerFacts
|
|
374
|
+
};
|
|
375
|
+
}
|
|
376
|
+
function buildExecutionPrompt(ctx, task) {
|
|
377
|
+
return [
|
|
378
|
+
ctx.prompts.buildSystemPrompt(),
|
|
379
|
+
ctx.prompts.buildTaskPrompt(task),
|
|
380
|
+
ctx.prompts.buildEditBoundaryPrompt(task),
|
|
381
|
+
'直接在当前工作区完成修改。完成后请给出简洁摘要,包括修改了哪些测试文件、验证结果和遗留问题。'
|
|
382
|
+
].join('\n\n');
|
|
383
|
+
}
|
|
384
|
+
function buildRetryExecutionPrompt(ctx, task, failure) {
|
|
385
|
+
const failureText = Array.isArray(failure) ? failure.join('\n') : failure ?? '未知验证失败';
|
|
386
|
+
return [
|
|
387
|
+
buildExecutionPrompt(ctx, task),
|
|
388
|
+
ctx.prompts.buildRetryPrompt(task, failureText),
|
|
389
|
+
'下面是上一轮全量验证失败日志,请只针对这些失败修复测试,不要扩展范围:',
|
|
390
|
+
failureText
|
|
391
|
+
].join('\n\n');
|
|
392
|
+
}
|
|
393
|
+
function truncateLog(text, maxLen = 2000) {
|
|
394
|
+
if (text.length <= maxLen)
|
|
395
|
+
return JSON.stringify(text);
|
|
396
|
+
return JSON.stringify(`${text.slice(0, maxLen)}...<truncated>`);
|
|
397
|
+
}
|
|
398
|
+
async function persistPrompt(ctx, taskId, attemptCount, prompt, targetFile, timestamp) {
|
|
399
|
+
await ctx.fileSystem.ensureDir(ctx.paths.promptsDir);
|
|
400
|
+
const fileName = buildRunArtifactName(taskId, attemptCount, targetFile, timestamp, 'prompt');
|
|
401
|
+
const promptPath = path.join(ctx.paths.promptsDir, fileName);
|
|
402
|
+
await ctx.fileSystem.writeText(promptPath, prompt);
|
|
403
|
+
return promptPath;
|
|
404
|
+
}
|
|
405
|
+
async function appendRunPrompt(ctx, taskId, attemptCount, targetFile, timestamp, promptPath) {
|
|
406
|
+
await ctx.fileSystem.ensureDir(ctx.paths.promptRunsDir);
|
|
407
|
+
const runPromptPath = path.join(ctx.paths.promptRunsDir, `run-${ctx.runId}.prompt.log`);
|
|
408
|
+
const attemptLabel = `a${attemptCount}`;
|
|
409
|
+
const content = [
|
|
410
|
+
'===== TASK PROMPT START =====',
|
|
411
|
+
`taskId: ${taskId}`,
|
|
412
|
+
`attempt: ${attemptCount}`,
|
|
413
|
+
`attemptLabel: ${attemptLabel}`,
|
|
414
|
+
`targetFile: ${targetFile}`,
|
|
415
|
+
`timestamp: ${timestamp}`,
|
|
416
|
+
`promptPath: ${promptPath}`,
|
|
417
|
+
`promptFile: ${path.basename(promptPath)}`,
|
|
418
|
+
'===== TASK PROMPT END =====',
|
|
419
|
+
''
|
|
420
|
+
].join('\n');
|
|
421
|
+
await ctx.fileSystem.appendText(runPromptPath, content);
|
|
422
|
+
return runPromptPath;
|
|
423
|
+
}
|
|
424
|
+
async function appendRunEvent(ctx, record, eventType = 'cli_run_attempt', extraData) {
|
|
425
|
+
const targetBaseName = path.basename(record.targetFile, path.extname(record.targetFile));
|
|
426
|
+
const attemptLabel = `a${record.attemptCount}`;
|
|
427
|
+
await ctx.eventStore.append({
|
|
428
|
+
eventType,
|
|
429
|
+
phase: record.status === 'verify_failed' ? 'VERIFYING_FULL' : record.status === 'passed' ? 'DONE' : 'WRITING_TESTS',
|
|
430
|
+
taskId: record.taskId,
|
|
431
|
+
message: record.summary,
|
|
432
|
+
data: {
|
|
433
|
+
runId: ctx.runId,
|
|
434
|
+
taskId: record.taskId,
|
|
435
|
+
attemptLabel,
|
|
436
|
+
targetBaseName,
|
|
437
|
+
targetFile: record.targetFile,
|
|
438
|
+
attemptCount: record.attemptCount,
|
|
439
|
+
promptPath: record.promptPath,
|
|
440
|
+
changedTestFiles: record.changedTestFiles,
|
|
441
|
+
relatedTestFiles: record.relatedTestFiles,
|
|
442
|
+
verifyResult: record.verifyResult,
|
|
443
|
+
status: record.status,
|
|
444
|
+
stdout: record.stdout ? truncateLog(record.stdout, 500) : undefined,
|
|
445
|
+
stderr: record.stderr ? truncateLog(record.stderr, 500) : undefined,
|
|
446
|
+
...extraData
|
|
447
|
+
},
|
|
448
|
+
timestamp: ctx.clock.nowIso()
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
function buildPreview(prompt, maxLen = 240) {
|
|
452
|
+
const compact = prompt.replace(/\s+/g, ' ').trim();
|
|
453
|
+
return compact.length <= maxLen ? compact : `${compact.slice(0, maxLen)}...`;
|
|
454
|
+
}
|
|
455
|
+
function buildRunArtifactName(taskId, attemptCount, targetFile, timestamp, suffix) {
|
|
456
|
+
const baseName = path.basename(targetFile, path.extname(targetFile));
|
|
457
|
+
const safeBaseName = baseName.replace(/[^a-zA-Z0-9_-]/g, '-').replace(/-+/g, '-');
|
|
458
|
+
const pathHash = hashContent(normalizePath(targetFile)).slice(0, 8);
|
|
459
|
+
const safeTimestamp = formatArtifactTimestamp(timestamp);
|
|
460
|
+
return `${taskId}-a${attemptCount}-${safeBaseName}-${pathHash}-${safeTimestamp}.${suffix}${suffix === 'prompt' ? '.txt' : '.log'}`;
|
|
461
|
+
}
|
|
462
|
+
function formatArtifactTimestamp(timestamp) {
|
|
463
|
+
return timestamp.replace(/[-:]/g, '').replace(/\.\d+Z$/, 'Z');
|
|
464
|
+
}
|
|
465
|
+
function buildTaskOutputPath(ctx, taskId, attemptCount, stream, targetFile, timestamp) {
|
|
466
|
+
return path.join(ctx.paths.logsPath, buildRunArtifactName(taskId, attemptCount, targetFile, timestamp, stream));
|
|
467
|
+
}
|
|
468
|
+
async function persistOutput(ctx, taskId, attemptCount, stream, targetFile, timestamp, content) {
|
|
469
|
+
if (!content?.trim())
|
|
470
|
+
return undefined;
|
|
471
|
+
await ctx.fileSystem.ensureDir(ctx.paths.logsPath);
|
|
472
|
+
const outputPath = buildTaskOutputPath(ctx, taskId, attemptCount, stream, targetFile, timestamp);
|
|
473
|
+
await ctx.fileSystem.writeText(outputPath, content);
|
|
474
|
+
return outputPath;
|
|
475
|
+
}
|
|
476
|
+
async function captureTestSnapshot(ctx) {
|
|
477
|
+
const files = await fg(buildTestSnapshotPatterns(ctx.config.testDirNames), {
|
|
478
|
+
cwd: ctx.projectPath,
|
|
479
|
+
ignore: ctx.config.exclude,
|
|
480
|
+
onlyFiles: true,
|
|
481
|
+
absolute: false
|
|
482
|
+
});
|
|
483
|
+
const snapshotEntries = await Promise.all(files.map(async (file) => {
|
|
484
|
+
const content = await ctx.fileSystem.readText(path.join(ctx.projectPath, file));
|
|
485
|
+
return [file, hashContent(content ?? '')];
|
|
486
|
+
}));
|
|
487
|
+
return Object.fromEntries(snapshotEntries);
|
|
488
|
+
}
|
|
489
|
+
async function validateTestChanges(ctx, task, beforeSnapshot) {
|
|
490
|
+
const afterSnapshot = await captureTestSnapshot(ctx);
|
|
491
|
+
const changedTestFiles = Object.keys(afterSnapshot)
|
|
492
|
+
.filter((file) => beforeSnapshot[file] !== afterSnapshot[file])
|
|
493
|
+
.sort();
|
|
494
|
+
const relatedTestFiles = findRelatedTestFiles(task, changedTestFiles);
|
|
495
|
+
if (relatedTestFiles.length > 0) {
|
|
496
|
+
ctx.logger.info(`[run] step=test-change status=ok taskId=${task.taskId} target=${task.targetFile} changed=${relatedTestFiles.join(',')}`);
|
|
497
|
+
return {
|
|
498
|
+
ok: true,
|
|
499
|
+
phase: 'DONE',
|
|
500
|
+
summary: '检测到目标相关测试变更。',
|
|
501
|
+
targetFile: task.targetFile,
|
|
502
|
+
changedTestFiles,
|
|
503
|
+
relatedTestFiles
|
|
504
|
+
};
|
|
505
|
+
}
|
|
506
|
+
ctx.logger.warn(`[run] step=test-change status=noop taskId=${task.taskId} target=${task.targetFile} changed=${changedTestFiles.join(',') || 'none'}`);
|
|
507
|
+
return {
|
|
508
|
+
ok: false,
|
|
509
|
+
phase: 'WRITING_TESTS',
|
|
510
|
+
summary: `Claude CLI 未产出 ${task.targetFile} 的相关测试变更,本次视为 no-op。`,
|
|
511
|
+
nextAction: 'manual-intervention',
|
|
512
|
+
targetFile: task.targetFile,
|
|
513
|
+
artifacts: {
|
|
514
|
+
changedTestFiles: changedTestFiles.length > 0 ? changedTestFiles : undefined,
|
|
515
|
+
relatedTestFiles: relatedTestFiles.length > 0 ? relatedTestFiles : undefined
|
|
516
|
+
},
|
|
517
|
+
changedTestFiles,
|
|
518
|
+
relatedTestFiles
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function findRelatedTestFiles(task, changedTestFiles) {
|
|
522
|
+
if (changedTestFiles.length === 0)
|
|
523
|
+
return [];
|
|
524
|
+
const existingMatches = new Set(task.testFiles);
|
|
525
|
+
const baseName = path.basename(task.targetFile, path.extname(task.targetFile));
|
|
526
|
+
const targetDir = path.dirname(task.targetFile);
|
|
527
|
+
const normalizedTargetDir = normalizePath(targetDir);
|
|
528
|
+
const moduleDirName = path.basename(targetDir);
|
|
529
|
+
return changedTestFiles.filter((file) => {
|
|
530
|
+
if (existingMatches.has(file))
|
|
531
|
+
return true;
|
|
532
|
+
const normalizedFile = normalizePath(file);
|
|
533
|
+
const fileName = path.basename(file);
|
|
534
|
+
const fileBaseName = path.basename(fileName, path.extname(fileName));
|
|
535
|
+
if (fileBaseName === baseName || fileBaseName === `${baseName}.test` || fileBaseName === `${baseName}.spec`) {
|
|
536
|
+
return true;
|
|
537
|
+
}
|
|
538
|
+
if (normalizedFile.startsWith(`${normalizedTargetDir}/`))
|
|
539
|
+
return true;
|
|
540
|
+
if (normalizedFile.includes(`/${moduleDirName}/`) && isTestFile(file))
|
|
541
|
+
return true;
|
|
542
|
+
return false;
|
|
543
|
+
});
|
|
544
|
+
}
|
|
545
|
+
function buildTestSnapshotPatterns(testDirNames) {
|
|
546
|
+
const configured = testDirNames?.filter(Boolean) ?? ['__test__', '__tests__'];
|
|
547
|
+
return [
|
|
548
|
+
'**/*.test.*',
|
|
549
|
+
'**/*.spec.*',
|
|
550
|
+
...configured.map((dir) => `**/${dir}/**/*.*`)
|
|
551
|
+
];
|
|
552
|
+
}
|
|
553
|
+
function hashContent(content) {
|
|
554
|
+
return createHash('sha1').update(content).digest('hex');
|
|
555
|
+
}
|
|
556
|
+
function normalizePath(filePath) {
|
|
557
|
+
return filePath.replace(/\\/g, '/');
|
|
558
|
+
}
|
|
559
|
+
async function markRunFailed(ctx, task, error, loopSummary) {
|
|
560
|
+
const message = Array.isArray(error) ? error.join('\n') : error ?? 'unknown error';
|
|
561
|
+
await ctx.stateStore.save({
|
|
562
|
+
phase: 'BLOCKED',
|
|
563
|
+
status: 'failed',
|
|
564
|
+
currentTaskId: task.taskId,
|
|
565
|
+
lastError: message,
|
|
566
|
+
loopSummary,
|
|
567
|
+
totals: {
|
|
568
|
+
pending: 0,
|
|
569
|
+
running: 0,
|
|
570
|
+
passed: 0,
|
|
571
|
+
failed: 1,
|
|
572
|
+
blocked: 0
|
|
573
|
+
},
|
|
574
|
+
lastAction: 'run',
|
|
575
|
+
nextSuggestedAction: 'manual-intervention',
|
|
576
|
+
updatedAt: ctx.clock.nowIso()
|
|
577
|
+
});
|
|
578
|
+
await ctx.eventStore.append({
|
|
579
|
+
eventType: 'cli_run_failed',
|
|
580
|
+
phase: 'BLOCKED',
|
|
581
|
+
taskId: task.taskId,
|
|
582
|
+
message,
|
|
583
|
+
timestamp: ctx.clock.nowIso()
|
|
584
|
+
});
|
|
585
|
+
}
|
|
586
|
+
async function markRunPassed(ctx, task, loopSummary) {
|
|
587
|
+
await ctx.stateStore.save({
|
|
588
|
+
phase: 'DONE',
|
|
589
|
+
status: 'done',
|
|
590
|
+
currentTaskId: task.taskId,
|
|
591
|
+
loopSummary,
|
|
592
|
+
totals: {
|
|
593
|
+
pending: 0,
|
|
594
|
+
running: 0,
|
|
595
|
+
passed: 1,
|
|
596
|
+
failed: 0,
|
|
597
|
+
blocked: 0
|
|
598
|
+
},
|
|
599
|
+
lastAction: 'run',
|
|
600
|
+
nextSuggestedAction: 'report',
|
|
601
|
+
updatedAt: ctx.clock.nowIso()
|
|
602
|
+
});
|
|
603
|
+
await ctx.eventStore.append({
|
|
604
|
+
eventType: 'cli_run_completed',
|
|
605
|
+
phase: 'DONE',
|
|
606
|
+
taskId: task.taskId,
|
|
607
|
+
message: `completed ${task.targetFile}`,
|
|
608
|
+
timestamp: ctx.clock.nowIso()
|
|
609
|
+
});
|
|
610
|
+
}
|
|
611
|
+
async function buildSuccessResult(ctx, targetFile, recommendedTargets, changedTestFiles, relatedTestFiles) {
|
|
612
|
+
const report = await buildFinalReport(ctx);
|
|
613
|
+
await ctx.reportStore.saveFinalReport(report);
|
|
614
|
+
return {
|
|
615
|
+
ok: true,
|
|
616
|
+
phase: 'DONE',
|
|
617
|
+
summary: `Claude CLI 已完成 ${targetFile} 的补测并通过全量验证。`,
|
|
618
|
+
nextAction: 'report',
|
|
619
|
+
targetFile,
|
|
620
|
+
artifacts: {
|
|
621
|
+
finalReportPath: ctx.paths.finalReportPath,
|
|
622
|
+
recommendedTargets,
|
|
623
|
+
changedTestFiles,
|
|
624
|
+
relatedTestFiles
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
function isTestFile(file) {
|
|
629
|
+
return /(\.test|\.spec)\.[jt]sx?$/.test(file) || file.includes('__test__/') || file.includes('__tests__/');
|
|
630
|
+
}
|
|
631
|
+
async function detectTestDirNames(ctx) {
|
|
632
|
+
if (ctx.cache.detectedTestDirNames)
|
|
633
|
+
return ctx.cache.detectedTestDirNames;
|
|
634
|
+
const configured = ctx.config.testDirNames?.filter(Boolean) ?? ['__test__', '__tests__'];
|
|
635
|
+
const jestConfigPath = path.join(ctx.projectPath, 'jest.config.js');
|
|
636
|
+
const jestConfig = await ctx.fileSystem.readText(jestConfigPath);
|
|
637
|
+
if (!jestConfig) {
|
|
638
|
+
ctx.cache.detectedTestDirNames = configured;
|
|
639
|
+
return configured;
|
|
640
|
+
}
|
|
641
|
+
const detected = configured.filter((dir) => jestConfig.includes(dir));
|
|
642
|
+
const result = detected.length > 0 ? detected : configured;
|
|
643
|
+
ctx.cache.detectedTestDirNames = result;
|
|
644
|
+
return result;
|
|
645
|
+
}
|