From 5445c343b35f183c1ef59e0dac5ac43ab8fe1b43 Mon Sep 17 00:00:00 2001 From: zhangxiang Date: Wed, 12 Aug 2026 22:07:22 +0800 Subject: [PATCH] =?UTF-8?q?chore:=20=E5=8F=82=E8=80=83=20novavis=20?= =?UTF-8?q?=E4=BC=98=E5=8C=96=20Agent=20=E9=85=8D=E7=BD=AE=E4=BD=93?= =?UTF-8?q?=E7=B3=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 AGENTS.md 规则(§1-§22)、Hook 脚本和可复用工具脚本: - 重构 AGENTS.md:新增 16 条规则(任务分解、双轨验证、骨架占位禁止、 测试命令退出码保留、工具脚本可复用、完成前验证门禁、Flaky 测试门禁等) - 新增 .trae/hooks/:session-start / pre-agent-check / check-completeness / stop-check - 新增 .trae/settings.json:关联 4 个 Hook 到 SessionStart/PreToolUse/PostToolUse/Stop - 新增 scripts/:flaky-scan.sh / check-task-checklist.sh / check-issue-checklist.sh - 调整 .gitignore:保留 hooks/settings 纳入版本控制,忽略运行时状态文件 --- .trae/hooks/check-completeness.sh | 56 ++++++++++++++ .trae/hooks/pre-agent-check.sh | 73 ++++++++++++++++++ .trae/hooks/session-start.sh | 92 +++++++++++++++++++++++ .trae/hooks/stop-check.sh | 87 ++++++++++++++++++++++ .trae/settings.json | 51 +++++++++++++ scripts/check-issue-checklist.sh | 70 ++++++++++++++++++ scripts/check-task-checklist.sh | 70 ++++++++++++++++++ scripts/flaky-scan.sh | 118 ++++++++++++++++++++++++++++++ 8 files changed, 617 insertions(+) create mode 100644 .trae/hooks/check-completeness.sh create mode 100644 .trae/hooks/pre-agent-check.sh create mode 100644 .trae/hooks/session-start.sh create mode 100644 .trae/hooks/stop-check.sh create mode 100644 .trae/settings.json create mode 100644 scripts/check-issue-checklist.sh create mode 100644 scripts/check-task-checklist.sh create mode 100644 scripts/flaky-scan.sh diff --git a/.trae/hooks/check-completeness.sh b/.trae/hooks/check-completeness.sh new file mode 100644 index 0000000..70df6c6 --- /dev/null +++ b/.trae/hooks/check-completeness.sh @@ -0,0 +1,56 @@ +#!/usr/bin/env bash +# PostToolUse hook: 检查单个编辑文件中的未完成标记 +# 在 Write/Edit 操作后对修改的文件运行 +set -euo pipefail + +PROJECT_DIR="${AGENT_PROJECT_DIR:-$(pwd)}" + +# Suppress during brainstorming / exploratory sessions +if [ -f "${PROJECT_DIR}/.trae/.suppress-hooks" ]; then + exit 0 +fi + +f="${AGENT_EDITED_FILE:-$AGENT_FILE}" +if [ ! -f "$f" ]; then + exit 0 +fi + +issues=() +ext="${f##*.}" +basename=$(basename "$f") + +# ============================================================ +# PART 1: Unfinished markers scan +# ============================================================ + +if [[ "$ext" =~ ^(ts|tsx|js|jsx|vue)$ ]]; then + while IFS=: read -r line_no content; do + [ -z "$line_no" ] && continue + trimmed=$(echo "$content" | sed 's/^[[:space:]]*//') + if echo "$trimmed" | grep -qE '^\s*(TODO|FIXME|HACK|XXX):'; then + issues+=(" L${line_no}: TODO/FIXME ${trimmed:0:100}") + elif echo "$trimmed" | grep -qi 'not implemented' && echo "$trimmed" | grep -qi 'throw'; then + issues+=(" L${line_no}: [SKELETON] ${trimmed:0:100}") + elif echo "$trimmed" | grep -qE 'function [a-zA-Z_][a-zA-Z0-9_]*\s*\(\s*\)\s*\{\s*\}'; then + if ! echo "$trimmed" | grep -q 'return'; then + issues+=(" L${line_no}: [EMPTY FN] ${trimmed:0:100}") + fi + fi + done < <(grep -nE 'TODO:|FIXME:|HACK:|XXX:|not implemented|function [a-zA-Z_][a-zA-Z0-9_]*\s*\(\s*\)\s*\{\s*\}' "$f" 2>/dev/null || true) +fi + +# ============================================================ +# REPORT +# ============================================================ + +if [ ${#issues[@]} -gt 0 ]; then + echo "" + echo "[COMPLETENESS] $(basename "$f") 检查发现 ${#issues[@]} 个问题:" + for issue in "${issues[@]:0:20}"; do + echo "$issue" + done + if [ ${#issues[@]} -gt 20 ]; then + echo " ... 及其他 $((${#issues[@]} - 20)) 处" + fi + echo "" +fi \ No newline at end of file diff --git a/.trae/hooks/pre-agent-check.sh b/.trae/hooks/pre-agent-check.sh new file mode 100644 index 0000000..3a57207 --- /dev/null +++ b/.trae/hooks/pre-agent-check.sh @@ -0,0 +1,73 @@ +#!/usr/bin/env bash +# PreToolUse hook: 写操作前根据文件路径输出 Skill 提示 +# 不阻止操作,仅输出提示信息 +set -euo pipefail + +f="${AGENT_EDITED_FILE:-$AGENT_FILE}" +[ -z "$f" ] && exit 0 +[ ! -f "$f" ] && [ ! -e "$f" ] && exit 0 + +basename=$(basename "$f") + +# Skill 提示: 命中时输出,供 agent 决策(AGENTS.md §3 Skill-First) +skill_hint() { + local name="$1"; shift + echo " → [SKILL] $name: $*" +} + +# 检查 1: 算法文件 → 提示 TDD + 交叉验证 +if [[ "$f" == *"/algorithms/"* ]]; then + echo "" + echo "[PRE-CHECK] 算法变更: $basename" + skill_hint "tdd" "RED-GREEN-REFACTOR 测试优先" + skill_hint "systematic-debugging" "算法结果须与传统典籍/已知实现交叉验证" + echo "" + exit 0 +fi + +# 检查 2: 测试文件 → 提示测试相关 skill +if [[ "$basename" == *.test.* || "$basename" == *.spec.* || "$f" == *"/__tests__/"* ]]; then + echo "" + echo "[PRE-CHECK] 测试文件变更: $basename" + skill_hint "tdd" "RED-GREEN-REFACTOR 测试优先" + skill_hint "systematic-debugging" "测试失败先定位根因再修复" + echo "" + exit 0 +fi + +# 检查 3: 组件文件 → 提示前端设计 skill +if [[ "$f" == *"/components/"* && "$basename" == *.vue ]]; then + echo "" + echo "[PRE-CHECK] 组件变更: $basename" + skill_hint "frontend-design" "UniApp 组件设计最佳实践" + skill_hint "impeccable" "UI 审查与视觉打磨" + echo "" + exit 0 +fi + +# 检查 4: 页面文件 → 提示全链路验证 +if [[ "$f" == *"/pages/"* && "$basename" == *.vue ]]; then + echo "" + echo "[PRE-CHECK] 页面变更: $basename" + skill_hint "frontend-design" "页面布局与交互设计" + echo "" + exit 0 +fi + +# 检查 5: 国际化文件 → 提示同步所有语言 +if [[ "$f" == *"/locales/"* && "$basename" == *.ts ]]; then + echo "" + echo "[PRE-CHECK] 国际化变更: $basename" + echo " → 确保同步更新所有 locales/*.ts 文件" + echo "" + exit 0 +fi + +# 检查 6: 文档/注释类 → 中文文档规范 +if [[ "$basename" == *.md || "$basename" == *.adoc ]]; then + echo "" + echo "[PRE-CHECK] 文档变更: $basename" + skill_hint "chinese-documentation" "中文排版/术语/结构规范" + echo "" + exit 0 +fi \ No newline at end of file diff --git a/.trae/hooks/session-start.sh b/.trae/hooks/session-start.sh new file mode 100644 index 0000000..efe9d5e --- /dev/null +++ b/.trae/hooks/session-start.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash +# SessionStart hook: 启动时输出项目关键上下文和提醒 +# 不阻止会话启动,仅输出提示信息 +set -euo pipefail + +PROJECT_DIR="${AGENT_PROJECT_DIR:-$(pwd)}" +cd "$PROJECT_DIR" + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " 万事宜 (Everything Is Suitable) — 项目上下文" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +# 当前分支 +BRANCH=$(git branch --show-current 2>/dev/null || echo "未知") +echo " 分支: $BRANCH" + +# 变更文件统计 +STAGED=$(git diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ') +UNSTAGED=$(git diff --name-only 2>/dev/null | wc -l | tr -d ' ') +echo " 变更: ${STAGED} 个暂存, ${UNSTAGED} 个未暂存" + +# 检测关键文件变更 +CHANGED_FILES=$(git diff --name-only HEAD 2>/dev/null; git diff --cached --name-only 2>/dev/null; git ls-files --others --exclude-standard 2>/dev/null | sort -u) +HAS_ALGORITHM_CHANGE="" +HAS_TEST_CHANGE="" +HAS_LOCALE_CHANGE="" +HAS_COMPONENT_CHANGE="" + +while IFS= read -r f; do + [ -z "$f" ] && continue + case "$f" in + */algorithms/*) HAS_ALGORITHM_CHANGE="yes" ;; + */__tests__/*|*.spec.*|*.test.*) HAS_TEST_CHANGE="yes" ;; + */locales/*) HAS_LOCALE_CHANGE="yes" ;; + */components/*) HAS_COMPONENT_CHANGE="yes" ;; + esac +done <<< "$CHANGED_FILES" + +# 变更提醒 +if [ -n "$HAS_ALGORITHM_CHANGE" ]; then + echo "" + echo " ⚠️ 检测到算法文件变更,请确保:" + echo " ┌─ 测试覆盖 ─────────────────────────────────┐" + echo " │ npx vitest run algorithms/__tests__/ │" + echo " ├─ 交叉验证 ─────────────────────────────────┤" + echo " │ 算法结果须与传统典籍/已知实现交叉验证 │" + echo " └────────────────────────────────────────────────┘" + echo "" +fi + +if [ -n "$HAS_TEST_CHANGE" ]; then + echo " ⚠️ 检测到测试文件变更,请确保:" + echo " ┌─ 运行验证 ─────────────────────────────────┐" + echo " │ npm test │" + echo " ├─ Flaky 检查 ────────────────────────────────┤" + echo " │ 新增测试至少连续运行 3 次确认稳定性 │" + echo " └────────────────────────────────────────────────┘" + echo "" +fi + +if [ -n "$HAS_LOCALE_CHANGE" ]; then + echo " ⚠️ 检测到国际化文件变更,请确保同步所有语言:" + echo " locales/zh-CN.ts locales/en.ts locales/ja.ts ..." + echo "" +fi + +if [ -n "$HAS_COMPONENT_CHANGE" ]; then + echo " ⚠️ 检测到组件变更,请确保:" + echo " ┌─ 单元测试 ─────────────────────────────────┐" + echo " │ npx vitest run components/ │" + echo " ├─ E2E 测试 ──────────────────────────────────┤" + echo " │ npm run test:e2e │" + echo " └────────────────────────────────────────────────┘" + echo "" +fi + +# Skill 推荐 +echo " 🎯 Skill 推荐(基于变更类型):" +if [ -n "$HAS_ALGORITHM_CHANGE" ]; then + echo " 算法 → TDD / systematic-debugging" +fi +if [ -n "$HAS_TEST_CHANGE" ]; then + echo " 测试 → TDD / systematic-debugging" +fi +if [ -n "$HAS_COMPONENT_CHANGE" ]; then + echo " 组件 → frontend-design / impeccable" +fi +echo "" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo "" \ No newline at end of file diff --git a/.trae/hooks/stop-check.sh b/.trae/hooks/stop-check.sh new file mode 100644 index 0000000..f4db17f --- /dev/null +++ b/.trae/hooks/stop-check.sh @@ -0,0 +1,87 @@ +#!/usr/bin/env bash +# Stop hook: runs quick quality checks and outputs JSON result. +# 轻量检查(<10s) +# 残留扫描由 check-completeness.sh(PostToolUse)按文件处理,此处不重复。 +set -euo pipefail + +cd "${AGENT_PROJECT_DIR:-$(pwd)}" + +# Suppress during brainstorming / exploratory sessions +if [ -f ".trae/.suppress-hooks" ]; then + echo '{"status":"ok","mode":"suppressed"}' + exit 0 +fi + +HAS_ERRORS=0 + +# All diagnostics go to stderr +{ + echo '=== [STOP HOOK QUALITY GATE] ===' + + # ---- 1/3: TypeScript type check ---- + echo '[1/3] TypeScript 类型检查...' + APP_DIR="everything-is-suitable-uniapp" + if [ -f "${APP_DIR}/node_modules/.bin/tsc" ]; then + if (cd "${APP_DIR}" && npx tsc --noEmit --pretty false 2>&1); then + echo ' tsc: PASS' + else + echo ' tsc: FAIL' + HAS_ERRORS=1 + fi + else + echo ' tsc: SKIP(未安装依赖,运行 npm install)' + fi + + # ---- 2/3: TODO/FIXME 残留扫描 ---- + echo '[2/3] 代码残留扫描(TODO/FIXME/skeleton)...' + FOUND=$(grep -rn 'TODO:\|FIXME:\|not implemented.*throw' "${APP_DIR}/src" --include='*.ts' --include='*.vue' --include='*.js' 2>/dev/null | grep -v 'node_modules' | grep -v '.test.' | head -10 || true) + if [ -z "$FOUND" ]; then + echo ' 残留扫描: PASS' + else + echo ' 残留扫描: 发现以下残留标记:' + echo "$FOUND" + HAS_ERRORS=1 + fi + + # ---- 3/3: 测试文件完整性检查 ---- + echo '[3/3] 测试文件一致性检查...' + # 检查 src 中每个 .ts/.vue 文件是否有关联测试文件 + ORPHANED=0 + while IFS= read -r src_file; do + [ -z "$src_file" ] && continue + base=$(basename "$src_file" .ts) + base=$(basename "$base" .vue) + dir=$(dirname "$src_file") + test_file="${dir}/__tests__/${base}.test.ts" + if [ ! -f "$test_file" ]; then + # 跳过 index.vue 等特殊文件 + case "$base" in + index|main|App) continue ;; + esac + # 跳过类型定义文件 + case "$src_file" in + */types/*) continue ;; + esac + ORPHANED=$((ORPHANED + 1)) + fi + done < <(find "${APP_DIR}/src" -name '*.ts' -o -name '*.vue' 2>/dev/null | head -100) + if [ "$ORPHANED" -eq 0 ]; then + echo ' 测试文件一致性: PASS' + else + echo " 测试文件一致性: ${ORPHANED} 个源文件缺少关联测试(仅供参考,非强制)" + fi + + # ---- Summary ---- + if [ "$HAS_ERRORS" -eq 0 ]; then + echo '=== [STOP HOOK: ALL PASS] ===' + else + echo '=== [STOP HOOK: ERRORS FOUND] ===' + fi +} >&2 + +# Output final status as JSON +if [ "$HAS_ERRORS" -eq 0 ]; then + echo '{"status":"ok"}' +else + echo '{"status":"error"}' +fi \ No newline at end of file diff --git a/.trae/settings.json b/.trae/settings.json new file mode 100644 index 0000000..e934662 --- /dev/null +++ b/.trae/settings.json @@ -0,0 +1,51 @@ +{ + "_comment": "Trae Hook 配置。环境变量 AGENT_PROJECT_DIR 应由宿主 agent 设置为项目根目录。", + "hooks": { + "SessionStart": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${AGENT_PROJECT_DIR}/.trae/hooks/session-start.sh\"", + "timeout": 10000 + } + ] + } + ], + "PreToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "bash \"${AGENT_PROJECT_DIR}/.trae/hooks/pre-agent-check.sh\"", + "timeout": 3000 + } + ] + } + ], + "PostToolUse": [ + { + "matcher": "Write|Edit", + "hooks": [ + { + "type": "command", + "command": "bash \"${AGENT_PROJECT_DIR}/.trae/hooks/check-completeness.sh\"", + "timeout": 5000 + } + ] + } + ], + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "bash \"${AGENT_PROJECT_DIR}/.trae/hooks/stop-check.sh\"", + "timeout": 30000 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/scripts/check-issue-checklist.sh b/scripts/check-issue-checklist.sh new file mode 100644 index 0000000..fe82dc6 --- /dev/null +++ b/scripts/check-issue-checklist.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Issue 关闭门禁检查 — 扫描已关闭的 Issue 文档是否存在未勾选子项 +# 用法: +# bash scripts/check-issue-checklist.sh # 检查所有 Issue +# bash scripts/check-issue-checklist.sh --quiet # 静默模式(仅退出码) +# bash scripts/check-issue-checklist.sh --help # 显示帮助 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +QUIET=0 + +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --quiet 静默模式(仅通过退出码表示结果)" + echo " --help 显示此帮助" + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --quiet) QUIET=1; shift ;; + --help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +# 检查 docs/plans/ 下的 Issue 文档 +PLANS_DIR="${PROJECT_DIR}/docs/plans" +HAS_ERRORS=0 +VIOLATIONS=() + +if [ ! -d "$PLANS_DIR" ]; then + [ "$QUIET" -eq 0 ] && echo " 检查: ${PLANS_DIR} 不存在,跳过" + echo '{"status":"ok"}' + exit 0 +fi + +# 扫描已关闭的文档(包含 "status: closed" 或 "已关闭" 标记) +while IFS= read -r plan_file; do + [ -z "$plan_file" ] && continue + basename=$(basename "$plan_file" .md) + + # 检查是否包含 closed 标记 + if grep -qi "status.*closed\|已关闭\|已完成" "$plan_file" 2>/dev/null; then + # 检查是否有未勾选的 checklist 项 + UNCHECKED=$(grep -cE '^\s*[-*]\s+\[[ ]\]' "$plan_file" 2>/dev/null || true) + if [ "$UNCHECKED" -gt 0 ]; then + VIOLATIONS+=("${basename}: ${UNCHECKED} 个未勾选子项") + HAS_ERRORS=1 + fi + fi +done < <(find "$PLANS_DIR" -name '*.md' -type f 2>/dev/null) + +if [ "$HAS_ERRORS" -eq 0 ]; then + [ "$QUIET" -eq 0 ] && echo " Issue 门禁: PASS" + echo '{"status":"ok"}' + exit 0 +else + [ "$QUIET" -eq 0 ] && echo " Issue 门禁: FAIL" + for v in "${VIOLATIONS[@]}"; do + echo " - $v" + done + echo '{"status":"error"}' + exit 1 +fi \ No newline at end of file diff --git a/scripts/check-task-checklist.sh b/scripts/check-task-checklist.sh new file mode 100644 index 0000000..77eb565 --- /dev/null +++ b/scripts/check-task-checklist.sh @@ -0,0 +1,70 @@ +#!/usr/bin/env bash +# Task 完成门禁检查 — 扫描已标记完成的 Task 是否存在未勾选子项 +# 用法: +# bash scripts/check-task-checklist.sh # 检查所有 Task +# bash scripts/check-task-checklist.sh --quiet # 静默模式(仅退出码) +# bash scripts/check-task-checklist.sh --help # 显示帮助 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" + +QUIET=0 + +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --quiet 静默模式(仅通过退出码表示结果)" + echo " --help 显示此帮助" + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --quiet) QUIET=1; shift ;; + --help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +# 检查 docs/plans/ 下的 Task 文档 +PLANS_DIR="${PROJECT_DIR}/docs/plans" +HAS_ERRORS=0 +VIOLATIONS=() + +if [ ! -d "$PLANS_DIR" ]; then + [ "$QUIET" -eq 0 ] && echo " 检查: ${PLANS_DIR} 不存在,跳过" + echo '{"status":"ok"}' + exit 0 +fi + +# 扫描已完成的计划文档 +while IFS= read -r plan_file; do + [ -z "$plan_file" ] && continue + basename=$(basename "$plan_file" .md) + + # 检查是否包含"已完成"标记 + if grep -qi "已完成\|completed" "$plan_file" 2>/dev/null; then + # 检查是否有未勾选的 checklist 项 + UNCHECKED=$(grep -cE '^\s*[-*]\s+\[[ ]\]' "$plan_file" 2>/dev/null || true) + if [ "$UNCHECKED" -gt 0 ]; then + VIOLATIONS+=("${basename}: ${UNCHECKED} 个未勾选子项") + HAS_ERRORS=1 + fi + fi +done < <(find "$PLANS_DIR" -name '*.md' -type f 2>/dev/null) + +if [ "$HAS_ERRORS" -eq 0 ]; then + [ "$QUIET" -eq 0 ] && echo " Task 门禁: PASS" + echo '{"status":"ok"}' + exit 0 +else + [ "$QUIET" -eq 0 ] && echo " Task 门禁: FAIL" + for v in "${VIOLATIONS[@]}"; do + echo " - $v" + done + echo '{"status":"error"}' + exit 1 +fi \ No newline at end of file diff --git a/scripts/flaky-scan.sh b/scripts/flaky-scan.sh new file mode 100644 index 0000000..bee2b54 --- /dev/null +++ b/scripts/flaky-scan.sh @@ -0,0 +1,118 @@ +#!/usr/bin/env bash +# Flaky test scanner — 连续运行测试检测不稳定测试 +# 用法: +# bash scripts/flaky-scan.sh # 随机顺序运行 3 次全量测试 +# bash scripts/flaky-scan.sh --runs 5 # 指定运行次数 +# bash scripts/flaky-scan.sh --spec <文件> # 指定测试文件 +# bash scripts/flaky-scan.sh --help # 显示帮助 + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +APP_DIR="${PROJECT_DIR}/everything-is-suitable-uniapp" + +RUNS=3 +SPEC="" +VERBOSE=0 + +# 颜色 +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' # No Color + +usage() { + echo "Usage: $0 [options]" + echo "" + echo "Options:" + echo " --runs 运行次数 (default: 3)" + echo " --spec 指定测试文件" + echo " --verbose 详细输出" + echo " --help 显示此帮助" + exit 0 +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --runs) RUNS="$2"; shift 2 ;; + --spec) SPEC="$2"; shift 2 ;; + --verbose) VERBOSE=1; shift ;; + --help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +if [ ! -d "$APP_DIR" ]; then + echo "错误: 未找到应用目录 ${APP_DIR}" + exit 1 +fi + +cd "$APP_DIR" + +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " Flaky Test Scanner" +echo " 运行次数: ${RUNS}" +echo " 测试文件: ${SPEC:-全部}" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + +PASS_COUNT=0 +FAIL_COUNT=0 +declare -a FAILED_RUNS + +for i in $(seq 1 "$RUNS"); do + echo "" + echo " [${i}/${RUNS}] 运行中..." + + if [ -n "$SPEC" ]; then + CMD="npx vitest run --reporter=verbose \"$SPEC\"" + else + CMD="npx vitest run --reporter=verbose" + fi + + if [ "$VERBOSE" -eq 1 ]; then + echo " → $CMD" + fi + + set +e + OUTPUT=$(eval "$CMD" 2>&1) + EXIT_CODE=$? + set -e + + if [ "$EXIT_CODE" -eq 0 ]; then + echo -e " [${i}/${RUNS}] ${GREEN}PASS${NC}" + PASS_COUNT=$((PASS_COUNT + 1)) + else + echo -e " [${i}/${RUNS}] ${RED}FAIL${NC} (exit code: ${EXIT_CODE})" + FAIL_COUNT=$((FAIL_COUNT + 1)) + FAILED_RUNS+=("$i") + if [ "$VERBOSE" -eq 1 ]; then + echo " --- 失败输出 ---" + echo "$OUTPUT" | tail -50 + echo " ---" + fi + fi +done + +echo "" +echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" +echo " 结果汇总" +echo " PASS: ${PASS_COUNT}/${RUNS}" +echo " FAIL: ${FAIL_COUNT}/${RUNS}" + +if [ "$FAIL_COUNT" -eq 0 ]; then + echo -e " ${GREEN}结论: 未检测到 flaky 测试,测试稳定 ✓${NC}" + exit 0 +else + echo -e " ${RED}结论: 检测到 flaky 测试${NC}" + echo " 失败运行: ${FAILED_RUNS[*]}" + echo "" + echo " 常见 flaky 根因:" + echo " ① vi.clearAllMocks() 不清除 mockResolvedValue 实现" + echo " ② 模块级可变 let mock 变量被测试修改未还原" + echo " ③ vi.stubGlobal 全局替换未在 afterEach 恢复" + echo " ④ waitFor 未 import" + echo "" + echo " 建议: 对失败运行使用 --verbose 查看详细输出" + exit 1 +fi \ No newline at end of file