@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
package/docs/demo.sh
ADDED
|
@@ -0,0 +1,266 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
|
|
3
|
+
# 自动单元测试执行脚本
|
|
4
|
+
# 用途:逐个读取 serviceToTest.txt 中的类名,自动编写单元测试
|
|
5
|
+
# 支持 Claude Code 和 Codex 两种模式
|
|
6
|
+
|
|
7
|
+
set -uo pipefail
|
|
8
|
+
|
|
9
|
+
# ============================================
|
|
10
|
+
# 配置变量
|
|
11
|
+
# ============================================
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
13
|
+
WORKSPACE_DIR="${SCRIPT_DIR}"
|
|
14
|
+
SERVICE_LIST_FILE="${WORKSPACE_DIR}/serviceToTest.txt"
|
|
15
|
+
TEST_INFO_FILE="${WORKSPACE_DIR}/testInfo.txt"
|
|
16
|
+
TEMP_DIR="${WORKSPACE_DIR}/tmp_logs"
|
|
17
|
+
|
|
18
|
+
# 选择运行模式: "claude" 或 "codex"(如果未设置则由用户选择)
|
|
19
|
+
RUN_MODE=""
|
|
20
|
+
|
|
21
|
+
# Claude Code 配置
|
|
22
|
+
CLAUDE_CMD=(claude)
|
|
23
|
+
|
|
24
|
+
# Codex 配置
|
|
25
|
+
CODEX_CMD=(
|
|
26
|
+
codex exec
|
|
27
|
+
--dangerously-bypass-approvals-and-sandbox
|
|
28
|
+
--cd "${WORKSPACE_DIR}"
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
# ============================================
|
|
32
|
+
# 颜色输出
|
|
33
|
+
# ============================================
|
|
34
|
+
RED='\033[0;31m'
|
|
35
|
+
GREEN='\033[0;32m'
|
|
36
|
+
YELLOW='\033[1;33m'
|
|
37
|
+
BLUE='\033[0;34m'
|
|
38
|
+
NC='\033[0m'
|
|
39
|
+
|
|
40
|
+
log_info() { echo -e "${GREEN}[INFO]${NC} $1"; }
|
|
41
|
+
log_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
|
|
42
|
+
log_error() { echo -e "${RED}[ERROR]${NC} $1"; }
|
|
43
|
+
log_step() { echo -e "${BLUE}[STEP]${NC} $1"; }
|
|
44
|
+
|
|
45
|
+
# ============================================
|
|
46
|
+
# 解析命令行参数
|
|
47
|
+
# ============================================
|
|
48
|
+
parse_args() {
|
|
49
|
+
if [ $# -gt 0 ]; then
|
|
50
|
+
case "$1" in
|
|
51
|
+
claude|codex)
|
|
52
|
+
RUN_MODE="$1"
|
|
53
|
+
log_info "使用命令行指定的运行模式: $RUN_MODE"
|
|
54
|
+
;;
|
|
55
|
+
*)
|
|
56
|
+
echo "用法: $0 [claude|codex]"
|
|
57
|
+
echo " 不带参数时,会交互式选择运行模式"
|
|
58
|
+
echo " claude - 使用 Claude Code 执行"
|
|
59
|
+
echo " codex - 使用 Codex 执行"
|
|
60
|
+
exit 1
|
|
61
|
+
;;
|
|
62
|
+
esac
|
|
63
|
+
fi
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
# ============================================
|
|
67
|
+
# 交互式选择运行模式
|
|
68
|
+
# ============================================
|
|
69
|
+
select_run_mode() {
|
|
70
|
+
# 如果 RUN_MODE 已是有效值,直接使用
|
|
71
|
+
if [ "$RUN_MODE" = "claude" ] || [ "$RUN_MODE" = "codex" ]; then
|
|
72
|
+
log_info "使用已设置的运行模式: $RUN_MODE"
|
|
73
|
+
return
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
echo ""
|
|
77
|
+
echo "请选择运行模式:"
|
|
78
|
+
echo " 1) claude - 使用 Claude Code 执行"
|
|
79
|
+
echo " 2) codex - 使用 Codex 执行"
|
|
80
|
+
echo ""
|
|
81
|
+
read -p "请输入选项 [1]: " choice
|
|
82
|
+
|
|
83
|
+
case "$choice" in
|
|
84
|
+
1|"")
|
|
85
|
+
RUN_MODE="claude"
|
|
86
|
+
;;
|
|
87
|
+
2)
|
|
88
|
+
RUN_MODE="codex"
|
|
89
|
+
;;
|
|
90
|
+
*)
|
|
91
|
+
log_error "无效的选项: $choice"
|
|
92
|
+
exit 1
|
|
93
|
+
;;
|
|
94
|
+
esac
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# ============================================
|
|
98
|
+
# 检查前置条件
|
|
99
|
+
# ============================================
|
|
100
|
+
check_prerequisites() {
|
|
101
|
+
# 确保 RUN_MODE 已设置
|
|
102
|
+
if [ -z "$RUN_MODE" ]; then
|
|
103
|
+
log_error "运行模式未设置"
|
|
104
|
+
exit 1
|
|
105
|
+
fi
|
|
106
|
+
|
|
107
|
+
log_step "检查前置条件..."
|
|
108
|
+
|
|
109
|
+
if [ ! -f "$SERVICE_LIST_FILE" ]; then
|
|
110
|
+
log_error "待测试文件清单不存在: $SERVICE_LIST_FILE"
|
|
111
|
+
exit 1
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
if [ "$RUN_MODE" = "codex" ]; then
|
|
115
|
+
if ! command -v codex >/dev/null 2>&1; then
|
|
116
|
+
log_error "codex 命令未找到"
|
|
117
|
+
exit 1
|
|
118
|
+
fi
|
|
119
|
+
elif [ "$RUN_MODE" = "claude" ]; then
|
|
120
|
+
if ! command -v claude >/dev/null 2>&1; then
|
|
121
|
+
log_error "claude 命令未找到"
|
|
122
|
+
exit 1
|
|
123
|
+
fi
|
|
124
|
+
else
|
|
125
|
+
log_error "未知的运行模式: $RUN_MODE,请设置 RUN_MODE 为 'claude' 或 'codex'"
|
|
126
|
+
exit 1
|
|
127
|
+
fi
|
|
128
|
+
|
|
129
|
+
mkdir -p "${TEMP_DIR}"
|
|
130
|
+
touch "${TEST_INFO_FILE}"
|
|
131
|
+
|
|
132
|
+
cd "$WORKSPACE_DIR" || exit 1
|
|
133
|
+
|
|
134
|
+
log_info "运行模式: $RUN_MODE"
|
|
135
|
+
log_info "前置检查通过"
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
# ============================================
|
|
139
|
+
# 获取下一个待处理的类
|
|
140
|
+
# ============================================
|
|
141
|
+
get_next_service() {
|
|
142
|
+
awk 'NF { print; exit }' "$SERVICE_LIST_FILE"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
# ============================================
|
|
146
|
+
# 移除已完成的类
|
|
147
|
+
# ============================================
|
|
148
|
+
remove_completed_service() {
|
|
149
|
+
local tmp_file="${SERVICE_LIST_FILE}.tmp"
|
|
150
|
+
|
|
151
|
+
awk '
|
|
152
|
+
BEGIN { removed = 0 }
|
|
153
|
+
{
|
|
154
|
+
if (!removed && NF) {
|
|
155
|
+
removed = 1
|
|
156
|
+
next
|
|
157
|
+
}
|
|
158
|
+
print
|
|
159
|
+
}
|
|
160
|
+
' "$SERVICE_LIST_FILE" > "$tmp_file" && mv "$tmp_file" "$SERVICE_LIST_FILE"
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
# ============================================
|
|
164
|
+
# 构建 prompt
|
|
165
|
+
# ============================================
|
|
166
|
+
build_prompt() {
|
|
167
|
+
local service_class=$1
|
|
168
|
+
|
|
169
|
+
if [ "$RUN_MODE" = "codex" ]; then
|
|
170
|
+
cat <<EOF
|
|
171
|
+
参考单测Skill对给定的类${service_class}编写单元测试,将单测结论追加到testInfo.txt文件。
|
|
172
|
+
EOF
|
|
173
|
+
elif [ "$RUN_MODE" = "claude" ]; then
|
|
174
|
+
# Claude Code 使用相同的 prompt
|
|
175
|
+
echo "参考单测Skill对给定的类${service_class}编写单元测试,将单测结论追加到testInfo.txt文件。"
|
|
176
|
+
fi
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
# ============================================
|
|
180
|
+
# 执行单元测试编写
|
|
181
|
+
# ============================================
|
|
182
|
+
run_unit_test() {
|
|
183
|
+
local service_class=$1
|
|
184
|
+
local class_name
|
|
185
|
+
local prompt
|
|
186
|
+
local log_file
|
|
187
|
+
local exit_code
|
|
188
|
+
|
|
189
|
+
class_name=$(echo "$service_class" | rev | cut -d'.' -f1 | rev)
|
|
190
|
+
log_file="${TEMP_DIR}/${RUN_MODE}_output_${class_name}.log"
|
|
191
|
+
prompt=$(build_prompt "$service_class")
|
|
192
|
+
|
|
193
|
+
log_step "=========================================="
|
|
194
|
+
log_step "处理: $service_class"
|
|
195
|
+
log_step "=========================================="
|
|
196
|
+
log_info "日志文件: $log_file"
|
|
197
|
+
log_info "使用模式: $RUN_MODE"
|
|
198
|
+
|
|
199
|
+
if [ "$RUN_MODE" = "codex" ]; then
|
|
200
|
+
printf '%s\n' "$prompt" | "${CODEX_CMD[@]}" - 2>&1 | tee "$log_file"
|
|
201
|
+
exit_code=${PIPESTATUS[1]}
|
|
202
|
+
elif [ "$RUN_MODE" = "claude" ]; then
|
|
203
|
+
# Claude Code: 使用 claude -p 命令配合 prompt
|
|
204
|
+
"${CLAUDE_CMD[@]}" -p "$prompt" --verbose --output-format stream-json 2>&1 | tee "$log_file"
|
|
205
|
+
exit_code=${PIPESTATUS[0]}
|
|
206
|
+
fi
|
|
207
|
+
|
|
208
|
+
log_info "$RUN_MODE 执行完成,退出码: $exit_code"
|
|
209
|
+
|
|
210
|
+
if [ "$exit_code" -ne 0 ]; then
|
|
211
|
+
log_error "$RUN_MODE 执行失败,当前类未从 serviceToTest.txt 中移除"
|
|
212
|
+
return "$exit_code"
|
|
213
|
+
fi
|
|
214
|
+
|
|
215
|
+
remove_completed_service
|
|
216
|
+
log_info "已完成并从队列中移除: $service_class"
|
|
217
|
+
|
|
218
|
+
return 0
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
# ============================================
|
|
222
|
+
# 显示剩余待处理数量
|
|
223
|
+
# ============================================
|
|
224
|
+
show_remaining() {
|
|
225
|
+
local count
|
|
226
|
+
count=$(awk 'NF { count++ } END { print count + 0 }' "$SERVICE_LIST_FILE")
|
|
227
|
+
log_info "剩余待测试: $count"
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
# ============================================
|
|
231
|
+
# 主流程
|
|
232
|
+
# ============================================
|
|
233
|
+
main() {
|
|
234
|
+
local service
|
|
235
|
+
|
|
236
|
+
echo ""
|
|
237
|
+
log_info "=========================================="
|
|
238
|
+
log_info " 自动单元测试编写脚本"
|
|
239
|
+
log_info "=========================================="
|
|
240
|
+
echo ""
|
|
241
|
+
|
|
242
|
+
parse_args "$@"
|
|
243
|
+
select_run_mode
|
|
244
|
+
check_prerequisites
|
|
245
|
+
show_remaining
|
|
246
|
+
|
|
247
|
+
while true; do
|
|
248
|
+
service=$(get_next_service)
|
|
249
|
+
|
|
250
|
+
if [ -z "$service" ]; then
|
|
251
|
+
echo ""
|
|
252
|
+
log_info "=========================================="
|
|
253
|
+
log_info " 所有单元测试已完成"
|
|
254
|
+
log_info "=========================================="
|
|
255
|
+
break
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
if ! run_unit_test "$service"; then
|
|
259
|
+
exit 1
|
|
260
|
+
fi
|
|
261
|
+
|
|
262
|
+
show_remaining
|
|
263
|
+
done
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
main "$@"
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Skill 集成说明
|
|
2
|
+
|
|
3
|
+
建议将本工具作为 OpenClaw skill 的外部 CLI 入口使用。
|
|
4
|
+
|
|
5
|
+
推荐映射:
|
|
6
|
+
- `init`:初始化配置与基线
|
|
7
|
+
- `analyze`:分析测试框架与覆盖率现状
|
|
8
|
+
- `run`:调用 Claude CLI 完成补测
|
|
9
|
+
- `verify`:执行全量校验
|
|
10
|
+
- `status`:查看运行状态
|
|
11
|
+
- `report`:输出最终报告
|
|
12
|
+
|
|
13
|
+
建议外部调度:
|
|
14
|
+
- 直接调用 `run` 并在失败时重试或人工介入
|
|
15
|
+
- 需要周期性汇总时调用 `status/report`
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# 状态说明
|
|
2
|
+
|
|
3
|
+
阶段枚举:
|
|
4
|
+
- INIT
|
|
5
|
+
- ANALYZE_BASELINE
|
|
6
|
+
- WRITING_TESTS
|
|
7
|
+
- VERIFYING_FULL
|
|
8
|
+
- BLOCKED
|
|
9
|
+
- DONE
|
|
10
|
+
|
|
11
|
+
典型流程:
|
|
12
|
+
1. analyzeBaseline 进入 ANALYZE_BASELINE
|
|
13
|
+
2. runWithClaudeCli 进入 WRITING_TESTS
|
|
14
|
+
3. verifyAll 进入 VERIFYING_FULL
|
|
15
|
+
4. 验证通过进入 DONE,否则进入 BLOCKED
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@angli/unit-test-tool",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"testbot": "./dist/cli/index.js"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"build": "tsc -p tsconfig.json",
|
|
10
|
+
"dev": "tsx src/cli/index.ts",
|
|
11
|
+
"test": "vitest run",
|
|
12
|
+
"test:watch": "vitest",
|
|
13
|
+
"lint": "tsc -p tsconfig.json --noEmit"
|
|
14
|
+
},
|
|
15
|
+
"dependencies": {
|
|
16
|
+
"commander": "^14.0.0",
|
|
17
|
+
"execa": "^9.6.0",
|
|
18
|
+
"fast-glob": "^3.3.3",
|
|
19
|
+
"pino": "^10.0.0",
|
|
20
|
+
"zod": "^4.1.11"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^24.6.0",
|
|
24
|
+
"tsx": "^4.20.6",
|
|
25
|
+
"typescript": "^5.9.3",
|
|
26
|
+
"vitest": "^3.2.4"
|
|
27
|
+
},
|
|
28
|
+
"publishConfig": {
|
|
29
|
+
"access": "public"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { analyzeBaseline } from '../../core/tools/analyze-baseline.js'
|
|
4
|
+
import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
|
|
5
|
+
|
|
6
|
+
export function registerAnalyzeCommand(program: Command) {
|
|
7
|
+
program
|
|
8
|
+
.command('analyze')
|
|
9
|
+
.option('--project <path>', 'project path')
|
|
10
|
+
.option('--target <number>', 'coverage target', '80')
|
|
11
|
+
.option('--scan-dir <dir>', 'scan dir (default: src)')
|
|
12
|
+
.action(async (options) => {
|
|
13
|
+
const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
|
|
14
|
+
const ctx = await createContext({
|
|
15
|
+
projectPath: options.project ?? process.cwd(),
|
|
16
|
+
coverageTarget: Number(options.target),
|
|
17
|
+
configOverrides: include ? { include } : undefined
|
|
18
|
+
})
|
|
19
|
+
const result = await analyzeBaseline(ctx)
|
|
20
|
+
console.log(result.summary)
|
|
21
|
+
})
|
|
22
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { runGuard } from '../../core/tools/guard.js'
|
|
4
|
+
import { parseDuration } from '../../utils/duration.js'
|
|
5
|
+
|
|
6
|
+
export function registerGuardCommand(program: Command) {
|
|
7
|
+
program
|
|
8
|
+
.command('guard')
|
|
9
|
+
.option('--project <path>', 'project path')
|
|
10
|
+
.option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
|
|
11
|
+
.option('--max-restart <count>', 'max restart attempts (default: 3)')
|
|
12
|
+
.option('--cooldown <duration>', 'restart cooldown (default: 60s)')
|
|
13
|
+
.action(async (options) => {
|
|
14
|
+
const ctx = await createContext({
|
|
15
|
+
projectPath: options.project ?? process.cwd(),
|
|
16
|
+
coverageTarget: 80
|
|
17
|
+
})
|
|
18
|
+
const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
|
|
19
|
+
const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
|
|
20
|
+
const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
|
|
21
|
+
const result = await runGuard(ctx, {
|
|
22
|
+
staleTimeoutMs: staleTimeoutMs,
|
|
23
|
+
maxRestart,
|
|
24
|
+
cooldownMs
|
|
25
|
+
})
|
|
26
|
+
console.log(result.summary)
|
|
27
|
+
})
|
|
28
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
|
|
4
|
+
|
|
5
|
+
export function registerInitCommand(program: Command) {
|
|
6
|
+
program
|
|
7
|
+
.command('init')
|
|
8
|
+
.requiredOption('--project <path>', 'project path')
|
|
9
|
+
.option('--target <number>', 'coverage target', '80')
|
|
10
|
+
.option('--scan-dir <dir>', 'scan dir (default: src)')
|
|
11
|
+
.option('--verify-cmd <cmd>', 'override full verify command')
|
|
12
|
+
.option('--lint-cmd <cmd>', 'override lint command')
|
|
13
|
+
.option('--typecheck-cmd <cmd>', 'override typecheck command')
|
|
14
|
+
.option('--system-append <text>', 'append project-level system prompt guidance')
|
|
15
|
+
.option('--task-append <text>', 'append project-level task prompt guidance')
|
|
16
|
+
.option('--retry-append <text>', 'append project-level retry prompt guidance')
|
|
17
|
+
.action(async (options) => {
|
|
18
|
+
const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
|
|
19
|
+
const commandOverrides = {
|
|
20
|
+
...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
|
|
21
|
+
...(options.lintCmd ? { lint: options.lintCmd } : {}),
|
|
22
|
+
...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
|
|
23
|
+
}
|
|
24
|
+
const promptOverrides = {
|
|
25
|
+
...(options.systemAppend ? { systemAppend: options.systemAppend } : {}),
|
|
26
|
+
...(options.taskAppend ? { taskAppend: options.taskAppend } : {}),
|
|
27
|
+
...(options.retryAppend ? { retryAppend: options.retryAppend } : {})
|
|
28
|
+
}
|
|
29
|
+
const ctx = await createContext({
|
|
30
|
+
projectPath: options.project,
|
|
31
|
+
coverageTarget: Number(options.target),
|
|
32
|
+
configOverrides: {
|
|
33
|
+
...(include ? { include } : {}),
|
|
34
|
+
...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {}),
|
|
35
|
+
...(Object.keys(promptOverrides).length > 0 ? { promptOverrides } : {})
|
|
36
|
+
}
|
|
37
|
+
})
|
|
38
|
+
await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
|
|
39
|
+
console.log('config saved')
|
|
40
|
+
})
|
|
41
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
|
|
4
|
+
import { runWithClaudeCli } from '../../core/tools/run-with-claude-cli.js'
|
|
5
|
+
import { startLifecycle } from '../../utils/process-lifecycle.js'
|
|
6
|
+
|
|
7
|
+
export function registerRunCommand(program: Command) {
|
|
8
|
+
program
|
|
9
|
+
.command('run')
|
|
10
|
+
.option('--project <path>', 'project path')
|
|
11
|
+
.option('--target <number>', 'coverage target', '80')
|
|
12
|
+
.option('--scan-dir <dir>', 'scan dir (default: src)')
|
|
13
|
+
.option('--file <path>', 'explicit target source file')
|
|
14
|
+
.option('--topN <number>', 'recommendation pool size', '5')
|
|
15
|
+
.option('--permission-mode <mode>', 'Claude CLI permission mode')
|
|
16
|
+
.option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
|
|
17
|
+
.option('--timeout <ms>', 'Claude CLI timeout in ms')
|
|
18
|
+
.option('--verify-cmd <cmd>', 'override full verify command')
|
|
19
|
+
.option('--lint-cmd <cmd>', 'override lint command')
|
|
20
|
+
.option('--typecheck-cmd <cmd>', 'override typecheck command')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
|
|
23
|
+
const commandOverrides = {
|
|
24
|
+
...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
|
|
25
|
+
...(options.lintCmd ? { lint: options.lintCmd } : {}),
|
|
26
|
+
...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const ctx = await createContext({
|
|
30
|
+
projectPath: options.project ?? process.cwd(),
|
|
31
|
+
coverageTarget: Number(options.target),
|
|
32
|
+
configOverrides: {
|
|
33
|
+
...(include ? { include } : {}),
|
|
34
|
+
...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
|
|
35
|
+
}
|
|
36
|
+
})
|
|
37
|
+
|
|
38
|
+
await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
|
|
39
|
+
|
|
40
|
+
const lifecycle = await startLifecycle(ctx, {
|
|
41
|
+
command: 'run',
|
|
42
|
+
argv: process.argv.slice(2)
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
try {
|
|
46
|
+
const result = await runWithClaudeCli(ctx, {
|
|
47
|
+
targetFile: options.file,
|
|
48
|
+
topN: Number(options.topN),
|
|
49
|
+
permissionMode: options.permissionMode,
|
|
50
|
+
allowedTools: parseAllowedTools(options.allowedTools),
|
|
51
|
+
timeoutMs: options.timeout ? Number(options.timeout) : undefined
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
console.log(result.summary)
|
|
55
|
+
if (result.artifacts?.finalReportPath) {
|
|
56
|
+
console.log(`report: ${result.artifacts.finalReportPath}`)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
await lifecycle.stop()
|
|
60
|
+
} catch (error) {
|
|
61
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
62
|
+
await ctx.lifecycleStore.patch({
|
|
63
|
+
status: 'crashed',
|
|
64
|
+
lastError: message,
|
|
65
|
+
updatedAt: ctx.clock.nowIso()
|
|
66
|
+
})
|
|
67
|
+
throw error
|
|
68
|
+
}
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function parseAllowedTools(value?: string): string[] | undefined {
|
|
73
|
+
if (!value) return undefined
|
|
74
|
+
const tools = value
|
|
75
|
+
.split(',')
|
|
76
|
+
.map((item) => item.trim())
|
|
77
|
+
.filter(Boolean)
|
|
78
|
+
return tools.length > 0 ? tools : undefined
|
|
79
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { parseDuration } from '../../utils/duration.js'
|
|
4
|
+
import { runSchedule } from '../../core/tools/guard.js'
|
|
5
|
+
|
|
6
|
+
const DEFAULT_INTERVAL_MS = 5 * 60_000
|
|
7
|
+
|
|
8
|
+
export function registerScheduleCommand(program: Command) {
|
|
9
|
+
program
|
|
10
|
+
.command('schedule')
|
|
11
|
+
.option('--project <path>', 'project path')
|
|
12
|
+
.option('--interval <duration>', 'guard interval (default: 5m)')
|
|
13
|
+
.option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
|
|
14
|
+
.option('--max-restart <count>', 'max restart attempts (default: 3)')
|
|
15
|
+
.option('--cooldown <duration>', 'restart cooldown (default: 60s)')
|
|
16
|
+
.action(async (options) => {
|
|
17
|
+
const ctx = await createContext({
|
|
18
|
+
projectPath: options.project ?? process.cwd(),
|
|
19
|
+
coverageTarget: 80
|
|
20
|
+
})
|
|
21
|
+
const intervalMs = options.interval ? parseDuration(options.interval) ?? DEFAULT_INTERVAL_MS : DEFAULT_INTERVAL_MS
|
|
22
|
+
const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
|
|
23
|
+
const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
|
|
24
|
+
const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
|
|
25
|
+
console.log(`[schedule] interval=${intervalMs}ms`)
|
|
26
|
+
await runSchedule(ctx, intervalMs, {
|
|
27
|
+
staleTimeoutMs,
|
|
28
|
+
maxRestart,
|
|
29
|
+
cooldownMs
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { analyzeBaseline } from '../../core/tools/analyze-baseline.js'
|
|
4
|
+
import { runLoop } from '../../core/tools/run-loop.js'
|
|
5
|
+
import { reportStatus } from '../../core/reporter/index.js'
|
|
6
|
+
import { buildIncludeFromScanDir } from '../utils/scan-dir.js'
|
|
7
|
+
import { startLifecycle } from '../../utils/process-lifecycle.js'
|
|
8
|
+
import { runSchedule } from '../../core/tools/guard.js'
|
|
9
|
+
import { parseDuration } from '../../utils/duration.js'
|
|
10
|
+
|
|
11
|
+
const DEFAULT_GUARD_INTERVAL_MS = 5 * 60_000
|
|
12
|
+
|
|
13
|
+
export function registerStartCommand(program: Command) {
|
|
14
|
+
program
|
|
15
|
+
.command('start')
|
|
16
|
+
.option('--project <path>', 'project path')
|
|
17
|
+
.option('--target <number>', 'coverage target', '80')
|
|
18
|
+
.option('--topN <number>', 'recommendation pool size', '5')
|
|
19
|
+
.option('--scan-dir <dir>', 'scan dir (default: src)')
|
|
20
|
+
.option('--file <path>', 'explicit target source file')
|
|
21
|
+
.option('--permission-mode <mode>', 'Claude CLI permission mode')
|
|
22
|
+
.option('--allowed-tools <tools>', 'comma-separated Claude CLI allowed tools')
|
|
23
|
+
.option('--timeout <ms>', 'Claude CLI timeout in ms')
|
|
24
|
+
.option('--verify-cmd <cmd>', 'override full verify command')
|
|
25
|
+
.option('--lint-cmd <cmd>', 'override lint command')
|
|
26
|
+
.option('--typecheck-cmd <cmd>', 'override typecheck command')
|
|
27
|
+
.option('--schedule', 'run built-in guard schedule')
|
|
28
|
+
.option('--guard-interval <duration>', 'guard interval (default: 5m)')
|
|
29
|
+
.option('--stale-timeout <duration>', 'heartbeat stale timeout (default: 2m)')
|
|
30
|
+
.option('--max-restart <count>', 'max restart attempts (default: 3)')
|
|
31
|
+
.option('--cooldown <duration>', 'restart cooldown (default: 60s)')
|
|
32
|
+
.action(async (options) => {
|
|
33
|
+
const include = buildIncludeFromScanDir(options.scanDir ?? 'src')
|
|
34
|
+
const commandOverrides = {
|
|
35
|
+
...(options.verifyCmd ? { verifyFull: options.verifyCmd } : {}),
|
|
36
|
+
...(options.lintCmd ? { lint: options.lintCmd } : {}),
|
|
37
|
+
...(options.typecheckCmd ? { typecheck: options.typecheckCmd } : {})
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
console.log('[1/4] 初始化配置...')
|
|
41
|
+
const ctx = await createContext({
|
|
42
|
+
projectPath: options.project ?? process.cwd(),
|
|
43
|
+
coverageTarget: Number(options.target),
|
|
44
|
+
configOverrides: {
|
|
45
|
+
...(include ? { include } : {}),
|
|
46
|
+
...(Object.keys(commandOverrides).length > 0 ? { commandOverrides } : {})
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
await ctx.fileSystem.writeJson(ctx.paths.configPath, ctx.config)
|
|
50
|
+
console.log(`[1/4] 初始化完成:${ctx.paths.configPath}`)
|
|
51
|
+
|
|
52
|
+
const lifecycle = await startLifecycle(ctx, {
|
|
53
|
+
command: 'start',
|
|
54
|
+
argv: process.argv.slice(2)
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
if (options.schedule) {
|
|
59
|
+
const intervalMs = options.guardInterval ? parseDuration(options.guardInterval) ?? DEFAULT_GUARD_INTERVAL_MS : DEFAULT_GUARD_INTERVAL_MS
|
|
60
|
+
const staleTimeoutMs = options.staleTimeout ? parseDuration(options.staleTimeout) : undefined
|
|
61
|
+
const cooldownMs = options.cooldown ? parseDuration(options.cooldown) : undefined
|
|
62
|
+
const maxRestart = options.maxRestart ? Number(options.maxRestart) : undefined
|
|
63
|
+
await runSchedule(ctx, intervalMs, {
|
|
64
|
+
staleTimeoutMs,
|
|
65
|
+
maxRestart,
|
|
66
|
+
cooldownMs
|
|
67
|
+
})
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
console.log('[2/4] 开始基线分析...')
|
|
71
|
+
const baseline = await analyzeBaseline(ctx)
|
|
72
|
+
console.log(`[2/4] ${baseline.summary}`)
|
|
73
|
+
|
|
74
|
+
console.log('[3/4] 调用 Claude CLI 补测...')
|
|
75
|
+
const runResult = await runLoop(ctx, {
|
|
76
|
+
targetFile: options.file,
|
|
77
|
+
topN: Number(options.topN),
|
|
78
|
+
permissionMode: options.permissionMode,
|
|
79
|
+
allowedTools: parseAllowedTools(options.allowedTools),
|
|
80
|
+
timeoutMs: options.timeout ? Number(options.timeout) : undefined
|
|
81
|
+
})
|
|
82
|
+
console.log(`[3/4] ${runResult.summary}`)
|
|
83
|
+
|
|
84
|
+
console.log('[4/4] 输出状态...')
|
|
85
|
+
const status = await reportStatus(ctx)
|
|
86
|
+
console.log(`[4/4] ${status.summary}`)
|
|
87
|
+
if (runResult.artifacts?.finalReportPath) {
|
|
88
|
+
console.log(`[4/4] report generated: ${runResult.artifacts.finalReportPath}`)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await lifecycle.stop()
|
|
92
|
+
} catch (error) {
|
|
93
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
94
|
+
await ctx.lifecycleStore.patch({
|
|
95
|
+
status: 'crashed',
|
|
96
|
+
lastError: message,
|
|
97
|
+
updatedAt: ctx.clock.nowIso()
|
|
98
|
+
})
|
|
99
|
+
throw error
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function parseAllowedTools(value?: string): string[] | undefined {
|
|
105
|
+
if (!value) return undefined
|
|
106
|
+
const tools = value
|
|
107
|
+
.split(',')
|
|
108
|
+
.map((item) => item.trim())
|
|
109
|
+
.filter(Boolean)
|
|
110
|
+
return tools.length > 0 ? tools : undefined
|
|
111
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { Command } from 'commander'
|
|
2
|
+
import { createContext } from '../context/create-context.js'
|
|
3
|
+
import { reportStatus, buildFinalReport } from '../../core/reporter/index.js'
|
|
4
|
+
|
|
5
|
+
export function registerStatusCommand(program: Command) {
|
|
6
|
+
program
|
|
7
|
+
.command('status')
|
|
8
|
+
.option('--project <path>', 'project path')
|
|
9
|
+
.action(async (options) => {
|
|
10
|
+
const ctx = await createContext({
|
|
11
|
+
projectPath: options.project ?? process.cwd(),
|
|
12
|
+
coverageTarget: 80
|
|
13
|
+
})
|
|
14
|
+
const result = await reportStatus(ctx)
|
|
15
|
+
console.log(result.summary)
|
|
16
|
+
})
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.command('report')
|
|
20
|
+
.option('--project <path>', 'project path')
|
|
21
|
+
.action(async (options) => {
|
|
22
|
+
const ctx = await createContext({
|
|
23
|
+
projectPath: options.project ?? process.cwd(),
|
|
24
|
+
coverageTarget: 80
|
|
25
|
+
})
|
|
26
|
+
const report = await buildFinalReport(ctx)
|
|
27
|
+
await ctx.reportStore.saveFinalReport(report)
|
|
28
|
+
console.log('report generated')
|
|
29
|
+
})
|
|
30
|
+
}
|