mcpField
S
specs-e2e-verification

specs-e2e-verification

github.com/giuseppe-trisciuoglio测试与质量365874 次安装7 周前更新
代码仓库

关于 specs-e2e-verification

用于根据真实运行行为验证已实现规格的技能。它会检测并启动本地环境,根据 `[IMP]` 验收标准生成测试、映射结果、创建报告并清理环境。

主要功能

  1. 01检测应用类型与端口
  2. 02启动并检查本地运行环境
  3. 03根据 `[IMP]` 标准生成测试
  4. 04将测试结果映射到标准
  5. 05生成验证报告
  6. 06清理运行环境

适用场景

  1. 01验证实现后的功能
  2. 02检查运行中 API 的行为
  3. 03在关闭规格前收集验证记录

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/giuseppe-trisciuoglio/developer-kit --skill specs-e2e-verification

安装后在 Claude Code 中输入/specs-e2e-verification确认已加载。

附带文件references/test-execution-patterns.md

详细介绍

规格 E2E 验证

概述

在规格实现并清理完成后,执行真实环境验证。此技能弥合了单元测试代码与可观察运行时行为之间的差距,具体步骤如下:

  1. 根据项目文件检测应用类型
  2. 启动本地运行环境(Docker Compose、开发服务器、Spring Boot 等)
  3. 根据功能规格中的 [IMP] 验收标准推导测试
  4. 执行真实测试(curl、Playwright、计算机操作)
  5. 将结果对应到验收标准
  6. 生成验证报告
  7. 拆除运行环境

输入:docs/specs/[id]/(包含功能规格和任务的规格文件夹)
输出:docs/specs/[id]/e2e-report-YYYY-MM-DD-HHMMSS.md

适用场景

  • 在 specs.task-implementation 和 specs.code-cleanup 之后使用,以确认功能在真实环境中正常运行。
  • 当开发者说“实际测试一下”“验证 API 是否真的可用”“运行 E2E 检查”或“在线验证验收标准”时使用。
  • 在关闭规格前,用于生成功能完成的证据。
  • 不要用于单元测试、静态分析或代码审查——此技能仅用于运行时行为验证。

参数

参数必需说明
--spec是规格文件夹路径(例如:docs/specs/001-feature/)
--task否限定验证范围的特定任务 ID(例如:TASK-003)
--keep-alive否如果指定,则跳过拆除步骤并保持运行环境运行
--timeout否启动和测试超时时间,单位为秒(默认值:120)
--insecure否如果指定,则允许 curl 使用 -k / --insecure(选择绕过 TLS)

最佳实践

  • 安全第一:绝不运行破坏性命令(rm -rf、docker system prune、sudo)。
  • 检测而非假设:使用文件特征判断应用类型;仅在情况不明确时询问用户。
  • 以验收标准为依据:每项测试都必须追溯到功能规格中的 [IMP] 验收标准。
  • 清理环境:除非传入 --keep-alive,否则始终拆除环境;对于遗留进程发出警告。
  • 不修改代码:此技能对源代码只读。可以创建报告,但绝不修改逻辑。
  • 使用 TodoWrite:跟踪全部 8 个阶段的进度。

说明

阶段 1:解析参数并加载上下文
  1. 解析 $ARGUMENTS:

    • --spec(必需):规格文件夹路径。验证目录存在,且至少包含一个功能规格文件(YYYY-MM-DD--*.md)。如果路径缺失或无效,则报错并中止。
    • --task(可选):任务 ID 筛选器(例如:TASK-003)。如果提供,则验证规格文件夹内存在 tasks/<task-id>.md。
    • --keep-alive(可选):布尔标志。如果指定,则在结束时跳过拆除步骤。
    • --timeout(可选):以秒为单位的正整数。默认值为 120。验证该值为正整数;否则报错并中止。
    • --insecure(可选):布尔标志。如果指定,curl 命令可以使用 -k / --insecure,以便在本地开发环境中使用自签名证书。默认情况下禁止绕过 TLS(REQ-NR003)。
  2. 阅读功能规格并提取:

    • 所有带分类标签([IMP]、[SEF]、[EXT])的验收标准
    • 仅根据 [IMP] 标准生成运行时测试
  3. 如果提供了 --task,则阅读任务文件,并将范围限定为其 provides 文件及相关验收标准。

  4. 使用 TodoWrite 为全部 8 个阶段创建待办事项清单。

阶段 1.5:安全验证关卡

执行任何命令之前,运行以下安全检查:

  1. 命令白名单检查:

    • 第 3 阶段推导出的启动命令 MUST 匹配 references/test-execution-patterns.md 中记录的模式。
    • IF 命令不在白名单中 → 使用 AskUserQuestion 请求用户明确确认后再执行。
    • 白名单命令包括:docker compose up -d --build、./mvnw spring-boot:run、./gradlew bootRun、npm run dev、npm run start:dev、npm start、cargo tauri build --debug、cargo tauri dev、npm run electron:dev、npx electron .、open *.app 以及等效的本地进程启动器。
    • 任何包含 sudo、rm -rf、docker system prune、mkfs、dd 或类似破坏性操作的命令均不在白名单中,SHALL 被拒绝。
  2. 禁用模式扫描(REQ-NR001):

    • 扫描推导出的启动命令和所有生成的测试命令,检查是否包含:
      • sudo → 中止并提示:"禁止:E2E 验证期间不允许使用 sudo。"
      • rm -rf → 中止并提示:"禁止:E2E 验证期间不允许使用 rm -rf。"
      • docker system prune → 中止并提示:"禁止:E2E 验证期间不允许使用 docker system prune。"
      • 任何针对数据库、卷或本地数据执行 rm、drop、destroy、prune 操作的命令 → 中止并提示:"禁止:不允许执行破坏性数据操作。"
    • IF 检测到任何禁用模式 → 立即中止;不得继续启动。
  3. TLS 强制检查(REQ-NR003):

    • 对于任何生成的 curl 命令:
      • IF 命令包含 -k 或 --insecure 且未传入 --insecure → 中止并提示:"禁止:curl TLS 绕过(-k / --insecure)默认处于禁用状态。传入 --insecure 以选择启用。"
      • IF 已传入 --insecure → 记录警告:"WARNING:TLS 证书验证已禁用。仅用于本地开发。"
  4. 数据完整性预检(REQ-NR004):

    • 执行启动命令之前,检查命令是否包含会覆盖或删除现有数据的模式(例如 rm、drop、prune、卷删除标志)。
    • IF 命令会修改现有数据库、卷或本地数据目录 → 中止并提示:"禁止:启动命令不得覆盖或删除现有数据。"
阶段 2:检测应用类型并发现端口
  1. 将 PROJECT_ROOT 设置为包含 .git 的目录,或 --spec 的父目录。

  2. 使用以下启发式规则检查 PROJECT_ROOT 中的配置文件(按顺序执行):

    Docker 管理(优先级最高):

    [ -f "$PROJECT_ROOT/docker-compose.yml" ] || [ -f "$PROJECT_ROOT/docker-compose.yaml" ] || [ -f "$PROJECT_ROOT/compose.yml" ]
    

    如果存在以上任一文件,无论是否存在其他框架配置,都将其归类为 Docker 管理。

    JVM / Spring Boot:

    [ -f "$PROJECT_ROOT/pom.xml" ] || [ -f "$PROJECT_ROOT/build.gradle" ] || [ -f "$PROJECT_ROOT/build.gradle.kts" ]
    

    并验证源代码目录是否存在:

    [ -d "$PROJECT_ROOT/src/main/java" ]
    

    如果两个条件均成立,则将其归类为 基于 JVM 的服务。

    NestJS:

    [ -f "$PROJECT_ROOT/package.json" ] && grep -q '"@nestjs/core"' "$PROJECT_ROOT/package.json"
    

    如果成立,则将其归类为 NestJS。

    Web SPA(React / Vue / Angular):

    [ -f "$PROJECT_ROOT/package.json" ] && ( grep -q '"react"' "$PROJECT_ROOT/package.json" || grep -q '"vue"' "$PROJECT_ROOT/package.json" || grep -q '"@angular/core"' "$PROJECT_ROOT/package.json" )
    

    如果成立,则将其归类为 Web SPA。

    桌面应用:

    [ -f "$PROJECT_ROOT/src-tauri/Cargo.toml" ] || ( [ -f "$PROJECT_ROOT/package.json" ] && grep -q '"electron"' "$PROJECT_ROOT/package.json" ) || [ -n "$(find "$PROJECT_ROOT" -maxdepth 2 -name '*.csproj' -print -quit 2>/dev/null)" ]
    

    如果成立,则将其归类为 桌面应用。

    Python:

    [ -f "$PROJECT_ROOT/requirements.txt" ] || [ -f "$PROJECT_ROOT/pyproject.toml" ] || [ -f "$PROJECT_ROOT/app.py" ] || [ -f "$PROJECT_ROOT/manage.py" ]
    

    如果成立,则将其归类为 Python。

  3. 应用优先级规则:

    • 始终优先选择 Docker Compose:如果存在 Docker Compose 文件,且还存在任何框架配置,则将其归类为 Docker 管理。Compose 堆栈定义了运行时。
    • 如果检测到多个非 Docker 配置(例如,同时存在 pom.xml 和 package.json,但没有 Docker Compose):
      • 如果规范的领域明确指向后端或前端(例如,规范标题包含 "API"、"backend"、"service"),优先选择 JVM 或 NestJS。
      • 如果规范的领域明确指向前端(例如,规范标题包含 "UI"、"page"、"component"),优先选择 Web SPA。
      • 如果仍无法确定,则进入用户提示(第 4 步)。
  4. 如果未找到可识别的配置,或者存在多个非 Docker 配置且规范领域不明确,则使用 AskUserQuestion,选项必须完全如下:

    • "REST API"
    • "Web SPA"
    • "Desktop"
    • "Skip"
  5. 发现端口:确定应用类型后,按以下顺序检查框架配置文件,以确定目标端口:

    • Vite 项目(vite.config.ts 或 vite.config.js):

      grep -oE 'port:\s*[0-9]+' "$PROJECT_ROOT/vite.config.ts" 2>/dev/null | grep -oE '[0-9]+' || \
      grep -oE 'port:\s*[0-9]+' "$PROJECT_ROOT/vite.config.js" 2>/dev/null | grep -oE '[0-9]+'
      
    • Spring Boot(application.yml):

      grep -A5 '^server:' "$PROJECT_ROOT/src/main/resources/application.yml" 2>/dev/null | grep 'port:' | head -1 | tr -dc '0-9'
      
    • Spring Boot(application.properties):

      grep '^server.port=' "$PROJECT_ROOT/src/main/resources/application.properties" 2>/dev/null | cut -d= -f2 | tr -dc '0-9'
      
    • Node.js / package.json 脚本:

      grep -oE -- '--port [0-9]+' "$PROJECT_ROOT/package.json" 2>/dev/null | grep -oE '[0-9]+' | head -1
      

      还要检查脚本中的 PORT 环境变量:

      grep -oE 'PORT=[0-9]+' "$PROJECT_ROOT/package.json" 2>/dev/null | grep -oE '[0-9]+' | head -1
      
    • 回退默认值(如果任何配置文件中均未找到端口):

      应用类型默认端口
      Node.js / NestJS3000
      Spring Boot(JVM)8080
      Angular4200
      Vite(React/Vue)5173
      Python8000
  6. 记录检测到的类型和发现的端口;两者都会写入报告。

阶段 3:启动环境并等待就绪

阅读 references/test-execution-patterns.md(随此技能附带),了解命令映射。根据检测结果:

数据完整性预检(REQ-NR004):执行启动命令之前,确认该命令不包含会覆盖或删除现有数据库、卷或本地数据的模式(例如,rm、--volumes、prune、drop)。如果检测到破坏性模式,立即中止,并提示:"Startup aborted: command would destroy existing data."

  1. 初始化运行时状态:

    • STARTUP_COMMAND="" — 用于启动环境的确切命令
    • HEALTH_CHECK_METHOD="" — 就绪状态的判定方式说明
    • STARTUP_LOGS_FILE="$(mktemp)" — 用于捕获启动标准输出/标准错误的临时文件
    • STARTUP_TIMEOUT="${TIMEOUT:-120}" — 等待就绪的秒数
    • START_TIME="$(date +%s)"
    • STARTUP_PID="" — 后台进程 PID(用于本地进程)
  2. 启动前端口检查(所有类型):

    if lsof -i :"$TARGET_PORT" >/dev/null 2>&1 || nc -z localhost "$TARGET_PORT" 2>/dev/null; then
        echo "Port $TARGET_PORT is already in use."
        EXISTING_PID=$(lsof -ti:"$TARGET_PORT" | head -n1)
        EXISTING_CMD=$(ps -p "$EXISTING_PID" -o comm= 2>/dev/null || echo "unknown")
        if [ "$APP_TYPE" = "Docker-managed" ] && docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$EXISTING_CMD"; then
            echo "Existing Docker container detected on port $TARGET_PORT; reusing it."
            STARTUP_COMMAND="(existing container reused)"
        elif [ "$APP_TYPE" = "JVM-based service" ] && echo "$EXISTING_CMD" | grep -q "java"; then
            echo "Existing Java process detected on port $TARGET_PORT; reusing it."
            STARTUP_COMMAND="(existing Java process reused)"
        else
            echo "Port $TARGET_PORT is occupied by an unrelated process ($EXISTING_CMD). Free the port and retry."
            exit 1
        fi
    fi
    
  3. 由 Docker 管理的启动:

    • 验证 Docker 守护进程是否可访问:
      if ! docker info >/dev/null 2>&1; then
          echo "Docker is not available. Please start Docker Desktop or use direct framework startup."
          exit 1
      fi
      
    • 设置 STARTUP_COMMAND="docker compose up -d --build"
    • 执行:
      cd "$PROJECT_ROOT" && docker compose up -d --build >> "$STARTUP_LOGS_FILE" 2>&1
      
    • 健康检查循环(强制执行超时限制):
      HEALTH_CHECK_METHOD="docker compose ps --format json"
      READY=false
      while [ $(( $(date +%s) - START_TIME )) -lt "$STARTUP_TIMEOUT" ]; do
          # 选项 A:Docker 原生健康状态(支持 JSON 数组和 NDJSON)
          if docker compose ps --format json 2>/dev/null | jq -s -e '.[] | select(.Health=="healthy")' >/dev/null 2>&1; then
              READY=true
              HEALTH_CHECK_METHOD="docker compose ps (HEALTHCHECK=healthy)"
              break
          fi
          # 选项 B:如果未定义 HEALTHCHECK,则回退到轮询端口
          if curl -sf "http://localhost:${TARGET_PORT}" >/dev/null 2>&1; then
              READY=true
              HEALTH_CHECK_METHOD="port polling via curl on localhost:${TARGET_PORT} (every 3s)"
              break
          fi
          sleep 3
      done
      if [ "$READY" != "true" ]; then
          echo "Startup timeout (${STARTUP_TIMEOUT}s) exceeded for Docker Compose."
          docker compose logs --tail=50 >> "$STARTUP_LOGS_FILE" 2>&1
          # 尝试清理,避免遗留容器(REQ-NR006)
          docker compose down >/dev/null 2>&1 || true
          exit 1
      fi
      
  4. Spring Boot 启动:

    • 确定构建工具并在后台启动,同时捕获日志:
      if [ -f "$PROJECT_ROOT/pom.xml" ]; then
          cd "$PROJECT_ROOT" && ./mvnw spring-boot:run -Dspring-boot.run.profiles=e2e >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="./mvnw spring-boot:run -Dspring-boot.run.profiles=e2e"
      elif [ -f "$PROJECT_ROOT/build.gradle" ] || [ -f "$PROJECT_ROOT/build.gradle.kts" ]; then
          cd "$PROJECT_ROOT" && ./gradlew bootRun --args='--spring.profiles.active=e2e' >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="./gradlew bootRun --args='--spring.profiles.active=e2e'"
      fi
      
    • 健康检查循环(强制超时):
      HEALTH_CHECK_METHOD="Spring Boot actuator /actuator/health"
      READY=false
      while [ $(( $(date +%s) - START_TIME )) -lt "$STARTUP_TIMEOUT" ]; do
          # 选项 A:Actuator 健康端点
          if curl -sf "http://localhost:${TARGET_PORT}/actuator/health" >/dev/null 2>&1; then
              READY=true
              HEALTH_CHECK_METHOD="Spring Boot actuator /actuator/health"
              break
          fi
          # 选项 B:回退到直接检查端口是否就绪
          if nc -z localhost "$TARGET_PORT" 2>/dev/null; then
              READY=true
              HEALTH_CHECK_METHOD="port polling via nc on localhost:${TARGET_PORT} (every 3s)"
              break
          fi
          # 后台进程提前退出时快速失败
          if [ -n "$STARTUP_PID" ] && ! kill -0 "$STARTUP_PID" 2>/dev/null; then
              echo "Spring Boot 进程在达到健康状态前已退出。"
              break
          fi
          sleep 3
      done
      if [ "$READY" != "true" ]; then
          echo "Spring Boot 启动超时(${STARTUP_TIMEOUT}s)。"
          # 为报告收集启动日志的最后几行
          tail -n 100 "$STARTUP_LOGS_FILE" >> "$STARTUP_LOGS_FILE".final 2>&1 || true
          # 尝试清理(REQ-NR006)
          [ -n "$STARTUP_PID" ] && kill -TERM "$STARTUP_PID" 2>/dev/null || true
          exit 1
      fi
      
  5. NestJS / Node.js 启动:

    • 确认 node_modules 存在,以避免难以理解的错误:
      if [ ! -d "$PROJECT_ROOT/node_modules" ]; then
          echo "node_modules not found. Run 'npm install' before verification."
          exit 1
      fi
      
    • 根据 package.json 中的脚本确定启动命令:
      if [ "$APP_TYPE" = "NestJS" ] && grep -q '"start:dev"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
          cd "$PROJECT_ROOT" && npm run start:dev >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npm run start:dev"
      elif grep -q '"dev"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
          cd "$PROJECT_ROOT" && npm run dev >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npm run dev"
      else
          cd "$PROJECT_ROOT" && npm start >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npm start"
      fi
      
    • 健康检查循环(强制执行超时):
      HEALTH_CHECK_METHOD="port polling via nc/curl on localhost:${TARGET_PORT} (every 3s)"
      READY=false
      while [ $(( $(date +%s) - START_TIME )) -lt "$STARTUP_TIMEOUT" ]; do
          # Option A: HTTP readiness via curl
          if curl -sf "http://localhost:${TARGET_PORT}" >/dev/null 2>&1; then
              READY=true
              HEALTH_CHECK_METHOD="curl on localhost:${TARGET_PORT}"
              break
          fi
          # Option B: Raw port readiness via nc
          if nc -z localhost "$TARGET_PORT" 2>/dev/null; then
              READY=true
              HEALTH_CHECK_METHOD="nc -z localhost:${TARGET_PORT}"
              break
          fi
          # Fail fast if the background process exited early
          if [ -n "$STARTUP_PID" ] && ! kill -0 "$STARTUP_PID" 2>/dev/null; then
              echo "Node.js/NestJS process exited before reaching healthy state."
              break
          fi
          sleep 3
      done
      if [ "$READY" != "true" ]; then
          echo "Startup timeout (${STARTUP_TIMEOUT}s) exceeded for Node.js/NestJS."
          tail -n 100 "$STARTUP_LOGS_FILE" >> "$STARTUP_LOGS_FILE".final 2>&1 || true
          # Attempt cleanup (REQ-NR006)
          [ -n "$STARTUP_PID" ] && kill -TERM "$STARTUP_PID" 2>/dev/null || true
          exit 1
      fi
      
    • 控制台就绪检测:确认就绪后,扫描 $STARTUP_LOGS_FILE,查找常见的服务就绪消息(例如 Nest application successfully started、Local:、ready in、Server running),并将首条匹配行记录在报告中,作为启动成功的证据。
  6. Python(FastAPI / Django / Flask)(参见 references/test-execution-patterns.md):

    • 运行 uvicorn main:app --reload、python manage.py runserver 或 flask run
    • 等待端口就绪
  7. 桌面应用(Tauri / Electron / .NET MAUI)(参见 references/test-execution-patterns.md):

    框架检测:

    DESKTOP_FRAMEWORK=""
    if [ -f "$PROJECT_ROOT/src-tauri/Cargo.toml" ]; then
        DESKTOP_FRAMEWORK="tauri"
    elif [ -f "$PROJECT_ROOT/package.json" ] && grep -q '"electron"' "$PROJECT_ROOT/package.json"; then
        DESKTOP_FRAMEWORK="electron"
    elif [ -n "$(find "$PROJECT_ROOT" -maxdepth 2 -name '*.csproj' -print -quit 2>/dev/null)" ]; then
        DESKTOP_FRAMEWORK="dotnet-maui"
    fi
    

    构建步骤(不存在预构建的调试二进制文件,或源文件比目标文件更新时触发):

    • Tauri:
      if [ "$DESKTOP_FRAMEWORK" = "tauri" ]; then
          if ! command -v cargo >/dev/null 2>&1; then
              echo "ERROR: Rust/Cargo is not installed. Tauri build requires cargo."
              exit 1
          fi
          BUILD_COMMAND="cargo tauri build --debug"
          cd "$PROJECT_ROOT" && $BUILD_COMMAND >> "$STARTUP_LOGS_FILE" 2>&1
          BUILD_EXIT_CODE=$?
          if [ "$BUILD_EXIT_CODE" -ne 0 ]; then
              echo "Tauri build failed (exit code $BUILD_EXIT_CODE). See startup logs for details."
              exit 1
          fi
          STARTUP_COMMAND="$BUILD_COMMAND (build succeeded)"
      fi
      
    • Electron:
      if [ "$DESKTOP_FRAMEWORK" = "electron" ]; then
          if [ ! -d "$PROJECT_ROOT/node_modules" ]; then
              echo "ERROR: node_modules not found. Run 'npm install' before verification."
              exit 1
          fi
          BUILD_COMMAND="(no separate build required for dev mode)"
          STARTUP_COMMAND="npm run electron:dev"
          if grep -q '"electron:build"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
              BUILD_COMMAND="npm run electron:build"
              cd "$PROJECT_ROOT" && $BUILD_COMMAND >> "$STARTUP_LOGS_FILE" 2>&1
              BUILD_EXIT_CODE=$?
              if [ "$BUILD_EXIT_CODE" -ne 0 ]; then
                  echo "Electron build failed (exit code $BUILD_EXIT_CODE). See startup logs for details."
                  exit 1
              fi
          fi
      fi
      

    启动构建好的应用程序二进制文件:

    • Tauri(macOS):

      APP_BUNDLE=$(find "$PROJECT_ROOT/src-tauri/target/debug/bundle" -name "*.app" -print -quit 2>/dev/null)
      if [ -n "$APP_BUNDLE" ]; then
          open "$APP_BUNDLE" >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="open $APP_BUNDLE"
      else
          DEV_BINARY=$(find "$PROJECT_ROOT/src-tauri/target/debug" -maxdepth 1 -type f -executable ! -name '*.dylib' ! -name '*.so' -print -quit 2>/dev/null)
          if [ -n "$DEV_BINARY" ]; then
              "$DEV_BINARY" >> "$STARTUP_LOGS_FILE" 2>&1 &
              STARTUP_PID=$!
              STARTUP_COMMAND="$DEV_BINARY"
          else
              cargo tauri dev >> "$STARTUP_LOGS_FILE" 2>&1 &
              STARTUP_PID=$!
              STARTUP_COMMAND="cargo tauri dev"
          fi
      fi
      
    • Tauri(Linux):

      APP_BINARY=$(find "$PROJECT_ROOT/src-tauri/target/debug" -maxdepth 1 -type f -executable ! -name '*.so' -print -quit 2>/dev/null)
      if [ -n "$APP_BINARY" ]; then
          "$APP_BINARY" >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="$APP_BINARY"
      else
          cargo tauri dev >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="cargo tauri dev"
      fi
      
    • Electron:

      if grep -q '"electron:dev"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
          cd "$PROJECT_ROOT" && npm run electron:dev >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npm run electron:dev"
      elif grep -q '"start"' "$PROJECT_ROOT/package.json" 2>/dev/null; then
          cd "$PROJECT_ROOT" && npm start >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npm start"
      else
          cd "$PROJECT_ROOT" && npx electron . >> "$STARTUP_LOGS_FILE" 2>&1 &
          STARTUP_PID=$!
          STARTUP_COMMAND="npx electron ."
      fi
      

    健康检查(检测进程是否出现,并强制执行超时):

    HEALTH_CHECK_METHOD="process polling via ps/kill -0 (every 3s)"
    READY=false
    while [ $(( $(date +%s) - START_TIME )) -lt "$STARTUP_TIMEOUT" ]; do
        if [ -n "$STARTUP_PID" ] && kill -0 "$STARTUP_PID" 2>/dev/null; then
            READY=true
            HEALTH_CHECK_METHOD="process PID ${STARTUP_PID} confirmed alive"
            break
        fi
        sleep 3
    done
    if [ "$READY" != "true" ]; then
        echo "Startup timeout (${STARTUP_TIMEOUT}s) exceeded for Desktop app."
        tail -n 100 "$STARTUP_LOGS_FILE" >> "$STARTUP_LOGS_FILE".final 2>&1 || true
        [ -n "$STARTUP_PID" ] && kill -TERM "$STARTUP_PID" 2>/dev/null || true
        exit 1
    fi
    
  8. 启动后记录(所有类型):

    • 在报告元数据中记录 STARTUP_COMMAND 和 HEALTH_CHECK_METHOD(AC-010)。
    • 将 STARTUP_LOGS_FILE 的内容追加到报告的 原始输出 部分(REQ-020)。
    • 如果启动因任何原因失败,收集所有可用日志并中止,同时显示清晰的错误消息。
第 4 阶段:生成并执行测试

重要:仅测试 [IMP] 验收标准。将每项标准转换为一个或多个具体的运行时操作。

REST API 测试(curl)

前置检查:生成任何测试之前,验证已安装 curl:

if ! command -v curl >/dev/null 2>&1; then
    echo "ERROR: curl is not installed."
    echo "Install instructions:"
    echo "  macOS:    brew install curl"
    echo "  Ubuntu:   sudo apt-get install curl"
    echo "  Windows:  choco install curl   or   winget install curl"
    exit 1
fi
if ! command -v jq >/dev/null 2>&1; then
    echo "WARNING: jq is not installed. JSON body assertions will fall back to grep (less precise)."
    echo "Install instructions:"
    echo "  macOS:    brew install jq"
    echo "  Ubuntu:   sudo apt-get install jq"
    echo "  Windows:  choco install jq   or   winget install jqlang.jq"
fi

1. 解析 [IMP] AC 中的端点提示 对于规范中的每个 [IMP] 验收标准:

  • 搜索关键词 GET、POST、PUT、PATCH、DELETE(不区分大小写),提取 HTTP 方法。
  • 搜索以 / 开头、后接字母数字片段的模式,提取端点路径,例如 /api/users、/v1/health。
  • 搜索数字模式 2xx、3xx、4xx、5xx 或 200、201、204、400、401、403、404、500 等具体代码,提取预期的 HTTP 状态码。
  • 搜索 application/json、text/plain、text/html 等内容,提取预期的 Content-Type。
  • 从 AC 文本中提取预期响应正文的提示(字段名、数组是否存在、字符串值)。

如果 [IMP] AC 不包含可解析的端点路径和方法,则将其标记为 MANUAL CHECK REQUIRED,并跳到下一个验收标准。

2. 发现身份验证凭据

构建 curl 命令前,按以下顺序扫描 PROJECT_ROOT 中的文件,尝试查找测试凭据:

文件键模式
.env.testE2E_AUTH_TOKEN=...、E2E_USERNAME=...、E2E_PASSWORD=...
.env.localE2E_AUTH_TOKEN=...、E2E_USERNAME=...、E2E_PASSWORD=...
application-test.ymle2e.auth-token: ...、e2e.username: ...、e2e.password: ...
application-test.propertiese2e.auth-token=...、e2e.username=...、e2e.password=...
e2e.credentials.json顶层键 E2E_AUTH_TOKEN、E2E_USERNAME、E2E_PASSWORD

发现逻辑:

# .env files
[ -f "$PROJECT_ROOT/.env.test" ] && export $(grep -E '^(E2E_AUTH_TOKEN|E2E_USERNAME|E2E_PASSWORD)=' "$PROJECT_ROOT/.env.test" | xargs)
[ -f "$PROJECT_ROOT/.env.local" ] && export $(grep -E '^(E2E_AUTH_TOKEN|E2E_USERNAME|E2E_PASSWORD)=' "$PROJECT_ROOT/.env.local" | xargs)

# Spring YAML
[ -f "$PROJECT_ROOT/src/main/resources/application-test.yml" ] && \
  E2E_AUTH_TOKEN=$(grep -A1 'e2e:' "$PROJECT_ROOT/src/main/resources/application-test.yml" | grep 'auth-token:' | sed 's/.*: *//')

# Spring properties
[ -f "$PROJECT_ROOT/src/main/resources/application-test.properties" ] && \
  E2E_AUTH_TOKEN=$(grep '^e2e.auth-token=' "$PROJECT_ROOT/src/main/resources/application-test.properties" | cut -d= -f2-)

# JSON credentials file
[ -f "$PROJECT_ROOT/e2e.credentials.json" ] && \
  E2E_AUTH_TOKEN=$(jq -r '.E2E_AUTH_TOKEN // empty' "$PROJECT_ROOT/e2e.credentials.json")

如果扫描所有文件后仍未找到任何凭据,且 AC 文本表明需要身份验证(提及 "auth"、"login"、"token"、"protected"、"bearer"、"API key"),则使用 AskUserQuestion 提示用户:

  • "输入 E2E_AUTH_TOKEN(如果没有,请留空)"
  • "输入 E2E_USERNAME(如果没有,请留空)"
  • "输入 E2E_PASSWORD(如果没有,请留空)"

安全性:在 E2E 报告中对令牌值进行脱敏;只显示标头名称(例如 Authorization: Bearer <redacted>)。

3. 生成 curl 命令

对于每个可解析的 [IMP] AC,使用以下确切模式构建 curl 命令:

curl -s -w "\n%{http_code}" -o /tmp/e2e_resp.json \
  -X <METHOD> \
  -H "Content-Type: application/json" \
  <AUTH_HEADER> \
  -d '<REQUEST_BODY>' \
  "http://localhost:${TARGET_PORT}<PATH>"

规则:

  • 始终包含 -s -w "\n%{http_code}" -o /tmp/e2e_resp.json。
  • -X <METHOD>:仅当方法不是 GET 时添加。对于 GET,完全省略 -X。
  • -H "Content-Type: application/json":仅对 POST、PUT、PATCH 添加。
  • -d '<REQUEST_BODY>':仅当 AC 描述了请求正文时添加。如果未描述正文,则省略 -d。
  • <AUTH_HEADER>:
    • 如果设置了 E2E_AUTH_TOKEN:-H "Authorization: Bearer ${E2E_AUTH_TOKEN}"
    • 如果设置了 E2E_USERNAME 和 E2E_PASSWORD:-u "${E2E_USERNAME}:${E2E_PASSWORD}"
    • 如果未找到凭据:省略身份验证标头。
  • TLS 强制要求(REQ-NR003):除非调用技能时明确传入了 --insecure 标志,否则绝不向 curl 命令添加 -k 或 --insecure。如果传入了 --insecure,则记录 TLS 验证已禁用的警告。
  • 将原始响应正文保存到 /tmp/e2e_resp.json,并将状态码输出到标准输出的最后一行。

4. 执行 curl 并断言(不重试)

每条 curl 命令恰好执行一次(REQ-NR008)。失败后不要重试。

HTTP_CODE=$(curl -s -w "\n%{http_code}" -o /tmp/e2e_resp.json <curl args> | tail -n 1)

断言(全部通过后,该 AC 才能标记为 VERIFIED):

a. HTTP 状态码:

if [ "$HTTP_CODE" -ne "$EXPECTED_STATUS" ]; then
    echo "FAIL: Expected status $EXPECTED_STATUS, got $HTTP_CODE"
    STATUS="FAILED"
fi

b. Content-Type 标头(仅当 AC 中有指定时):

ACTUAL_CT=$(curl -s -o /dev/null -D - <curl args> | grep -i "Content-Type:" | head -1 | sed 's/Content-Type: //i' | tr -d '\r')
if [ -n "$EXPECTED_CT" ] && ! echo "$ACTUAL_CT" | grep -qi "$EXPECTED_CT"; then
    echo "FAIL: Expected Content-Type '$EXPECTED_CT', got '$ACTUAL_CT'"
    STATUS="FAILED"
fi

c. 响应正文结构:使用 jq(首选)或 grep(备用):

  • 如果已安装 jq 且响应为 JSON:
    # 断言字段存在
    jq -e '.fieldName' /tmp/e2e_resp.json >/dev/null || { echo "FAIL: Missing .fieldName"; STATUS="FAILED"; }
    
    # 断言字段等于预期值
    jq -e '.fieldName == "expectedValue"' /tmp/e2e_resp.json >/dev/null || { echo "FAIL: .fieldName mismatch"; STATUS="FAILED"; }
    
    # 断言数组长度
    jq -e '(.items | length) > 0' /tmp/e2e_resp.json >/dev/null || { echo "FAIL: .items is empty"; STATUS="FAILED"; }
    
    

    断言嵌套字段

    jq -e '.data.user.email' /tmp/e2e_resp.json >/dev/null || { echo "FAIL: 缺少 .data.user.email"; STATUS="FAILED"; }
  • 如果未安装 jq,请使用 grep 作为备用方案:
    grep -q '"fieldName"' /tmp/e2e_resp.json || { echo "FAIL: 缺少 fieldName"; STATUS="FAILED"; }
    

5. 记录结果

对于每项 curl 测试,记录:

  • AC ID 和文本(截断)
  • 生成的 curl 命令(将身份验证令牌脱敏)
  • 预期状态码、Content-Type、正文断言
  • 实际状态码、Content-Type、正文片段(前 500 个字符)
  • 通过/失败状态
  • 执行时间(可选,供报告参考)

如果任何断言失败,立即将该 AC 标记为 FAILED。不要重试。

Web SPA 测试(Playwright)

前置条件检查:生成任何 SPA 测试之前,请确认已安装 Playwright:

PLAYWRIGHT_VERSION=$(npx playwright --version 2>/dev/null || echo "")
if [ -z "$PLAYWRIGHT_VERSION" ]; then
    echo "ERROR: Playwright is not installed."
    echo "Install instructions:"
    echo "  npm install -D @playwright/test"
    echo "  npx playwright install chromium"
    echo ""
    echo "Skipping all Web SPA tests. Acceptance criteria for SPA interactions will be marked MANUAL CHECK REQUIRED."
    for ac_id in $(get_spa_ac_ids); do
        record_result "$ac_id" "MANUAL CHECK REQUIRED" "Playwright not installed"
    done
    # Continue to next test category; do not abort the whole verification
fi
echo "Playwright version: $PLAYWRIGHT_VERSION"

如果缺少 Playwright,技能 MUST 报告此缺项,并提供上述安装命令,然后跳过 SPA 测试。不要尝试自动安装。

1. 准备产物目录

ARTIFACT_DIR="${SPEC_FOLDER}/e2e-artifacts"
mkdir -p "$ARTIFACT_DIR"

2. 解析 [IMP] AC 中的 UI 行为提示

对于规范中每项与 Web SPA 行为相关的 [IMP] 验收标准:

  • 提取 UI 交互关键词:click、fill、type、select、submit、navigate、scroll、hover。
  • 通过搜索以下模式提取目标选择器:
    • data-testid="..." 或 data-testid='...' → [data-testid=...]
    • id="..." 或 id='...' → #...
    • class="..." 或 class='...' → .class-name(将空格替换为点)
    • 提及按钮/链接文本 → text=...
    • 提及 URL 路径 → /path
  • 提取预期的可见状态:文本内容预期、URL 预期、元素存在/不存在、数量预期。
  • 提取表单字段名称和预期输入值。

如果某项 [IMP] AC 不包含可解析的 UI 行为或可见状态提示,将其标记为 MANUAL CHECK REQUIRED,并跳到下一项标准。

3. 启动无头浏览器上下文

默认情况下,浏览器 MUST 以无头模式运行。仅当用户明确传入 --headed 时才使用有头模式。

对于每项 SPA 测试,生成一个临时 Playwright 脚本,并使用 node 执行:

DEV_SERVER_URL="http://localhost:${TARGET_PORT}"
TEST_SCRIPT="$(mktemp /tmp/e2e-spa-XXXXXX.js)"

cat > "$TEST_SCRIPT" << 'PLAYWRIGHT_EOF'
const { chromium } = require('playwright');
(async () => {
  const browser = await chromium.launch({ headless: true });
  const context = await browser.newContext({
    viewport: { width: 1280, height: 720 },
    userAgent: 'DeveloperKit-E2E/1.0'
  });
  const page = await context.newPage();
  // Actions and assertions injected here
  await browser.close();
})();
PLAYWRIGHT_EOF

node "$TEST_SCRIPT"

4. 将 AC 转换为 Playwright 操作

对于每项已解析的 UI 交互,在临时脚本中生成相应的 Playwright 操作:

AC 描述模式Playwright 操作
"点击 [selector]"await page.click('[data-testid=refresh]');
"使用 [value] 填写 [selector]"await page.fill('#username', 'testuser');
"在 [selector] 中输入 [value]"await page.type('input[name=search]', 'query');
"在 [selector] 中选择 [value]"await page.selectOption('select[name=country]', 'US');
"提交 [form]"await page.click('button[type=submit]');
"导航到 [path]"await page.goto('http://localhost:${TARGET_PORT}/path');
"将鼠标悬停在 [selector] 上"await page.hover('.tooltip-trigger');
"滚动到 [selector]"await page.locator('[data-testid=footer]').scrollIntoViewIfNeeded();

选择器优先级(从最具体到最不具体):

  1. [data-testid=...] — 首选,最稳定
  2. #id — 唯一元素 ID
  3. .class-name — CSS 类
  4. [name=...] — 表单元素名称
  5. text=... — 可见文本内容(备用方案)

5. 断言可见状态

对于每项预期的可见状态,在临时脚本中生成相应的断言:

AC 描述模式Playwright 断言
“页面显示 [text]”await expect(page.locator('body')).toContainText('text');
“[selector] 包含文本 [value]”await expect(page.locator('[data-testid=title]')).toHaveText('value');
“[selector] 包含 [text]”await expect(page.locator('.message')).toContainText('text');
“表格有 [N] 行”expect(await page.locator('table tbody tr').count()).toBe(N);
“URL 是 [path]”expect(page.url()).toBe('http://localhost:${TARGET_PORT}/path');
“URL 包含 [fragment]”expect(page.url()).toContain('/fragment');
“[selector] 可见”await expect(page.locator('[data-testid=modal]')).toBeVisible();
“[selector] 隐藏”await expect(page.locator('[data-testid=spinner]')).toBeHidden();

6. 执行测试并强制执行超时

每个 Playwright 测试都必须设置单项测试超时,以防止无限期挂起(REQ-NR007)。包装脚本使用 timeout 命令:

TEST_TIMEOUT_SEC=30

npx playwright --version >/dev/null 2>&1 || {
    echo "Playwright not installed; skipping SPA tests."
    record_spa_manual_check
    continue
}

# Build the inline test script
TEST_SCRIPT="$(mktemp /tmp/e2e-spa-XXXXXX.js)"
cat > "$TEST_SCRIPT" << EOF
const { chromium } = require('playwright');
(async () => {
  const browser = await chromium.launch({ headless: true });
  const page = await browser.newPage();
  page.setDefaultTimeout(${TEST_TIMEOUT_SEC}000);
  page.setDefaultNavigationTimeout(${TEST_TIMEOUT_SEC}000);
  try {
    await page.goto('${DEV_SERVER_URL}');
    // --- GENERATED ACTIONS ---
    // --- GENERATED ASSERTIONS ---
    console.log('RESULT: PASS');
  } catch (error) {
    console.error('RESULT: FAIL:', error.message);
    const screenshotPath = '${ARTIFACT_DIR}/screenshot-' + Date.now() + '-ac-${AC_ID}.png';
    await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => {});
    console.error('SCREENSHOT:', screenshotPath);
    process.exitCode = 1;
  } finally {
    await browser.close();
  }
})();
EOF

# Run with timeout wrapper; if the test hangs, it is killed and marked FAILED
if timeout --signal=TERM $((TEST_TIMEOUT_SEC + 5)) node "$TEST_SCRIPT"; then
    STATUS="VERIFIED"
else
    EXIT_CODE=$?
    if [ "$EXIT_CODE" -eq 124 ]; then
        echo "FAIL: Test hung and was terminated after ${TEST_TIMEOUT_SEC}s timeout"
    fi
    STATUS="FAILED"
fi
rm -f "$TEST_SCRIPT"

如果测试进程挂起超过超时时间,timeout 命令会发送 SIGTERM,将 AC 标记为 FAILED,并在证据中记录:“测试挂起,并在 ${TEST_TIMEOUT_SEC}s 后终止”。

7. 失败时捕获屏幕截图

任何断言或操作失败时:

  1. 使用 page.screenshot({ path: ..., fullPage: true }) 捕获整页屏幕截图。
  2. 保存到 ${ARTIFACT_DIR}/screenshot-<timestamp>-ac-<AC_ID>.png。
  3. 在测试结果证据中记录屏幕截图路径。

测试通过时,屏幕截图为可选项,只有传入 --capture-success 时才会捕获。

8. 记录结果

对于每项 SPA 测试,记录:

  • AC ID 和文本(截断)
  • 执行的 Playwright 操作
  • 执行的断言
  • 通过/失败状态
  • 屏幕截图路径(失败时)
  • 执行时间
  • 错误消息(失败时)

如果任何断言失败,立即将 AC 标记为 FAILED。不要重试。

边缘情况 — 错误处理:

  • 未安装 Playwright:报告清晰的错误,并建议运行 npm install -D @playwright/test 和 npx playwright install chromium。将所有 SPA AC 标记为 MANUAL CHECK REQUIRED。继续执行其他测试类别。
  • 开发服务器未运行:如果 page.goto() 抛出 net::ERR_CONNECTION_REFUSED 或类似错误,报告:“无法访问 ${DEV_SERVER_URL} 上的开发服务器。请确保在验证前启动服务器。”将受影响的 AC 标记为 FAILED。
  • 浏览器启动失败:如果 Chromium 无法启动(例如,缺少系统依赖项),报告错误并建议运行 npx playwright install-deps chromium。将受影响的 AC 标记为 MANUAL CHECK REQUIRED。
桌面测试(计算机操作 / MCP)

前置条件检查:生成任何桌面测试前,请确认计算机操作或基于 MCP 的 GUI 自动化工具可用。此技能不会自动安装这些工具。

GUI_TOOLS_AVAILABLE=false
GUI_TOOL_NAME=""

if [ -n "$CLAUDE_COMPUTER_USE_AVAILABLE" ] || command -v computer-use >/dev/null 2>&1; then
    GUI_TOOLS_AVAILABLE=true
    GUI_TOOL_NAME="computer-use"
fi

if [ -n "$MCP_GUI_SERVER_URL" ] || command -v mcp-gui >/dev/null 2>&1; then
    GUI_TOOLS_AVAILABLE=true
    GUI_TOOL_NAME="mcp-gui"
fi

if [ "$GUI_TOOLS_AVAILABLE" != "true" ]; then
    echo "ERROR: Desktop testing tools are not available."
    echo "This skill requires computer-use or MCP GUI automation tools to verify desktop applications."
    echo ""
    echo "To enable desktop verification:"
    echo "  - Run in an environment with computer-use support, OR"
    echo "  - Install and configure an MCP GUI automation server"
    echo ""
    echo "Aborting desktop verification. No tests will be executed."
    exit 1
fi
echo "Desktop GUI tool detected: $GUI_TOOL_NAME"

1. 准备产物目录

ARTIFACT_DIR="${SPEC_FOLDER}/e2e-artifacts"
mkdir -p "$ARTIFACT_DIR"

2. 解析 [IMP] AC 中与桌面行为相关的提示

对于规范中与桌面应用行为相关的每项 [IMP] 验收标准:

  • 提取窗口标题提示:查找“窗口打开”“对话框出现”“模态窗口显示”“标题包含”“屏幕显示”等短语。
  • 提取UI 元素描述:查找按钮标签、输入框占位符、菜单项、复选框标签和切换项名称。
  • 提取工作流步骤:查找诸如“点击……然后……”“选择……并按下……”“切换……”等操作序列。
  • 提取预期状态变化:查找诸如“主题变为深色”“状态指示灯变绿”“列表更新”等视觉变化。

如果某项 [IMP] AC 不包含可解析的桌面行为提示(没有窗口、元素或工作流描述),则将其标记为 MANUAL CHECK REQUIRED 并跳至下一项标准。

3. 确认应用正在运行

如果桌面应用未在 Phase 3 中启动,或其进程已退出,则使用 Phase 3 中的命令重新启动:

if [ -n "$STARTUP_PID" ] && ! kill -0 "$STARTUP_PID" 2>/dev/null; then
    echo "Desktop app process is not running. Re-launching..."
    # Re-run the appropriate launch command from Phase 3 based on DESKTOP_FRAMEWORK
fi

4. 通过视觉或无障碍树检查验证窗口/UI 元素

对于每条描述窗口或 UI 元素的 AC(AC-022):

  • 视觉检查(计算机使用):

    • 截取桌面屏幕截图。
    • 分析图像,检查预期的窗口标题、按钮标签或视觉元素。
    • 示例:“设置窗口打开” → 截取屏幕截图,确认可以看到标题为“设置”的窗口。
  • 无障碍树检查(MCP GUI 自动化):

    • 查询无障碍树,查找名称、角色或状态与预期相符的元素。
    • 示例:“设置窗口打开” → 查询名称包含“设置”的窗口元素。
    • 示例:“切换深色模式” → 查询标签包含“深色模式”的开关/复选框元素。

5. 通过 GUI 自动化模拟用户工作流

对于每条描述用户工作流的 AC(AC-023),将描述转换为 GUI 自动化操作:

AC 描述模式GUI 自动化操作
“点击 [按钮标签]”点击无障碍名称或标签匹配的 UI 元素
“在 [字段] 中填写 [值]”聚焦输入字段并输入该值
“切换 [开关/复选框]”点击开关或复选框元素
“从 [下拉菜单] 中选择 [选项]”打开下拉菜单并点击选项元素
“导航到 [菜单项]”点击标签匹配的菜单项
“在 [字段] 中输入 [值]”聚焦字段并输入该值

6. 每个验证步骤都截取屏幕截图

在每一步(操作前、操作后以及断言时)都截取屏幕截图:

SCREENSHOT_PATH="${ARTIFACT_DIR}/screenshot-$(date +%s)-ac-${AC_ID}-step-${STEP_NUM}.png"
# computer-use: capture screenshot and save to SCREENSHOT_PATH
# MCP: use the GUI automation server's screenshot capability

无论通过还是失败,都会为每一步保存屏幕截图,满足 AC-024(SEF)。

7. 强制执行每项测试的超时

每项桌面测试都必须强制执行单项测试超时,以防止无限期挂起(REQ-NR007):

TEST_TIMEOUT_SEC=60
TEST_START_TIME=$(date +%s)
TEST_STATUS="RUNNING"

while [ "$TEST_STATUS" = "RUNNING" ]; do
    if [ $(( $(date +%s) - TEST_START_TIME )) -gt "$TEST_TIMEOUT_SEC" ]; then
        echo "FAIL: Desktop test hung and was terminated after ${TEST_TIMEOUT_SEC}s timeout"
        STATUS="FAILED"
        TEST_STATUS="TIMEOUT"
        FINAL_SCREENSHOT="${ARTIFACT_DIR}/screenshot-$(date +%s)-ac-${AC_ID}-timeout.png"
        # capture screenshot to FINAL_SCREENSHOT
        break
    fi
    # Execute next workflow step; if all steps complete, set TEST_STATUS="COMPLETED"
done

如果测试超时:

  • 将 AC 标记为 FAILED
  • 截取最终超时屏幕截图
  • 记录证据:“测试挂起,并在 ${TEST_TIMEOUT_SEC}s 超时后终止”
  • 在下一个 AC 开始前,尝试重置应用状态(关闭并重新打开窗口)

8. 记录结果

对于每项桌面测试,记录:

  • AC ID 和文本(截断)
  • 已执行的 GUI 自动化操作(步骤列表)
  • 已截取的屏幕截图(包含步骤说明的路径列表)
  • 通过/失败状态
  • 错误消息、超时原因或断言不匹配情况(失败时)

如果任何断言或操作失败,立即将 AC 标记为 FAILED。不要重试。

边缘情况 — 错误处理:

  • 没有可用的计算机使用或 MCP 工具:以明确的错误消息中止验证。不要默默跳过桌面测试。
  • 构建失败:构建输出会捕获到 $STARTUP_LOGS_FILE 并报告。验证会在启动前中止。
  • 测试期间应用崩溃:如果桌面进程在测试期间退出,从日志中捕获任何崩溃输出,将当前 AC 标记为 FAILED,并尝试重新启动应用以执行下一个 AC。
  • 未找到窗口/元素:如果预期窗口或元素在测试超时时间内未出现,将 AC 标记为 FAILED,并截取当前桌面状态的屏幕截图。

回退方案:如果某条 AC 无法转换为自动化测试(例如需要人工进行审美判断),将其标记为 MANUAL CHECK REQUIRED 并继续。

阶段 5:将结果映射到验收标准

目标:将每项测试执行结果与功能规范中的 [IMP] 验收标准进行比较,并为每项标准给出确定的判定结果。

5.1 初始化结果表

创建一个以 AC ID 为键的关联结果结构(例如,Shell 关联数组、JSON 对象或临时文件)。对于第 1 阶段提取的每条 [IMP] 验收标准,预先填充一行:

字段初始值
ac_idAC 标识符(例如,AC-012)
ac_text完整的验收标准文本,显示时截断为 120 个字符
statusPENDING(在 5.2 中更新)
evidence空字符串(在 5.2 中更新)
actual空字符串(仅在不匹配时填充)
expected空字符串(仅在不匹配时填充)

遍历规格说明中的验收标准表,仅包含标签为 [IMP] 的行。[SEF] 和 [EXT] 验收标准不纳入运行时验证;可以在附录中列出,但无需给出状态判定。

5.2 应用判定规则

对于第 4 阶段测试所针对的每条 AC,按以下顺序应用规则:

  1. VERIFIED(REQ-016):

    • IF 测试完成,且未出现断言失败、超时或工具错误
    • THEN 将 status 设为 "VERIFIED"
    • 将 evidence 设为简明描述:
      • REST API:curl 返回了 HTTP <code>,耗时 <N>ms
      • Web SPA:Playwright 断言通过:<selector> <condition>
      • 桌面端:GUI 自动化确认:<window/element> 存在
  2. FAILED(REQ-018):

    • IF 任意断言失败、测试超时、工具崩溃,或开发服务器无法访问
    • THEN 将 status 设为 "FAILED"
    • 将 actual 设为观察到的值(例如,HTTP 400、未找到元素、30s 后超时)
    • 将 expected 设为 AC 要求的值(例如,HTTP 201、元素可见)
    • 将 evidence 设为人类可读的句子,说明 actual 与 expected 的对比;如果有捕获的工件(截图、响应转储),还应包含其路径。
  3. MANUAL CHECK REQUIRED(REQ-017):

    • IF 因以下原因未为该 AC 生成测试:
      • AC 文本缺少可解析的端点/UI/桌面行为提示
      • 缺少必需工具(例如,未安装 Playwright)
      • AC 描述的是需要人工判断的内容(例如,“UI 看起来正确”)
    • THEN 将 status 设为 "MANUAL CHECK REQUIRED"
    • 将 evidence 设为原因:无法推导出自动化测试:<reason>

重要:每条 [IMP] AC 都必须有最终状态 VERIFIED、FAILED 或 MANUAL CHECK REQUIRED。第 5 阶段结束时,不得有任何 AC 保持 PENDING。

5.3 计算汇总计数

填充所有行后,计算:

TOTAL_IMP=$(count_implementation_acs)
VERIFIED_COUNT=$(grep -c '"status":"VERIFIED"' "$RESULTS_FILE")
FAILED_COUNT=$(grep -c '"status":"FAILED"' "$RESULTS_FILE")
MANUAL_COUNT=$(grep -c '"status":"MANUAL CHECK REQUIRED"' "$RESULTS_FILE")

保存这些计数;报告的 Summary 部分需要包含这些计数(REQ-020)。

第 6 阶段:生成报告

目标:生成一份确定性、易于阅读的 Markdown 报告,遵循第 6.3 阶段定义的格式,且不包含任何机密信息。

6.1 准备报告路径和目录
REPORT_TIMESTAMP=$(date +%Y-%m-%d-%H%M%S)
REPORT_DIR="${SPEC_FOLDER}"
ARTIFACT_DIR="${REPORT_DIR}/e2e-artifacts"
REPORT_FILE="${REPORT_DIR}/e2e-report-${REPORT_TIMESTAMP}.md"

mkdir -p "$ARTIFACT_DIR"
  • SPEC_FOLDER 是 --spec 参数的值(例如,docs/specs/001-real-e2e-verification/)。
  • 报告文件名必须严格采用 e2e-report-YYYY-MM-DD-HHMMSS.md 格式(AC-026)。
  • 将任何工件路径写入报告之前,工件目录必须已存在。
6.2 从原始输出中删去机密信息

将任何命令输出写入报告之前,先对原始日志进行脱敏处理(REQ-NR002):

# Redact Authorization header values
sed -E 's/(Authorization:[[:space:]]*[Bb]earer[[:space:]]+)[^[:space:]]+/\1***REDACTED***/g' "$STARTUP_LOGS_FILE" > "$STARTUP_LOGS_FILE.redacted"

# Redact tokens in JSON bodies
sed -E 's/("token"[[:space:]]*:[[:space:]]*")[^"]+/\1***REDACTED***/g' "$STARTUP_LOGS_FILE.redacted" > "$STARTUP_LOGS_FILE.redacted2"

# Redact passwords in curl -u arguments
sed -E 's/(-u[[:space:]]+[^:]*:)[^[:space:]]+/\1***REDACTED***/g' "$STARTUP_LOGS_FILE.redacted2" > "$STARTUP_LOGS_FILE.redacted3"

# Redact E2E_* environment variables (REQ-NR002)
sed -E 's/(E2E_[A-Z_]+=)[^[:space:]]+/\1***REDACTED***/g' "$STARTUP_LOGS_FILE.redacted3" > "$STARTUP_LOGS_FILE.redacted_final"

报告中只包含脱敏后的版本($STARTUP_LOGS_FILE.redacted_final)。原始临时文件将被丢弃。

6.3 编写报告章节

按以下确切顺序追加各章节,生成报告。报告中的所有路径都必须相对于报告文件所在位置。

第 1 节 — 摘要

## Summary

| Metric | Count |
|--------|-------|
| Total AC Evaluated | ${TOTAL_IMP} |
| ✅ VERIFIED | ${VERIFIED_COUNT} |
| ❌ FAILED | ${FAILED_COUNT} |
| ⚠️ MANUAL CHECK REQUIRED | ${MANUAL_COUNT} |

第 2 节 — 环境

## Environment

| Property | Value |
|----------|-------|
| Application Type | ${APP_TYPE} |
| Startup Command | \`${STARTUP_COMMAND}\` |
| Target Port | ${TARGET_PORT} |
| Health Endpoint | ${HEALTH_CHECK_METHOD} |
| Runtime Version | ${RUNTIME_VERSION} |
| Verification Started | ${ISO8601_START_TIME} |
  • RUNTIME_VERSION:记录启动时检测到的运行时版本:
    • Docker:docker --version
    • Node.js:node --version
    • Java:java -version 2>&1 | head -1
    • Python:python --version 或 python3 --version
  • ISO8601_START_TIME:第 3 阶段开始时的时间戳,采用 ISO-8601 格式。

第 3 节 — 测试结果

## Test Results

| AC ID | Criterion (truncated) | Status | Evidence |
|-------|----------------------|--------|----------|
| AC-012 | WHEN the app is classified... | VERIFIED | \`curl\` returned 200 |
| AC-013 | WHEN a curl test executes... | FAILED | Expected 201, got 400 |
| AC-014 | WHEN authentication is required... | MANUAL CHECK REQUIRED | No credentials found |
  • “Criterion (truncated)”列必须截断至 120 个字符,超出时在末尾追加 ...。
  • “Status”列必须为 VERIFIED、FAILED 或 MANUAL CHECK REQUIRED 之一。
  • “Evidence”列在适用时必须使用相对路径引用产物(例如 ./e2e-artifacts/ac-019-screenshot.png)。

第 4 节 — 原始输出

## Raw Output

### Startup Logs
\`\`\`
[contents of $STARTUP_LOGS_FILE.redacted_final]
\`\`\`

### Test Commands
\`\`\`
[For each test: the exact command executed (auth redacted) and its first 2000 chars of stdout/stderr]
\`\`\`
  • 如果测试没有输出,请写入 (no output)。
  • 每个代码块最多包含 2000 行,避免报告内容过多;如果日志更长,请追加说明:(truncated; full logs available in <path>)。

第 5 节 — 产物

## Artifacts

| AC ID | Type | Path |
|-------|------|------|
| AC-019 | Screenshot | ./e2e-artifacts/screenshot-1717189200-ac-019.png |
| AC-014 | Response Dump | ./e2e-artifacts/ac-014-response.json |
  • 列出 $ARTIFACT_DIR 中存在的所有产物文件。
  • 如果没有生成任何产物,请写入 No artifacts captured for this run.

第 6 节 — 拆除状态

## Teardown Status

| Property | Value |
|----------|-------|
| Teardown Executed | ${TEARDOWN_EXECUTED} |
| Port Released | ${PORT_RELEASED} |
| Remaining Processes | ${REMAINING_PROCESSES} |
  • TEARDOWN_EXECUTED:true / false / skipped (--keep-alive)
  • PORT_RELEASED:true / false / unknown
  • REMAINING_PROCESSES:以逗号分隔的列表,列出拆除后仍在运行的 PID 或容器名称;如果没有,请写入 none。
6.4 完成并持久化报告
cat > "$REPORT_FILE" << 'REPORT_EOF'
[all sections assembled above]
REPORT_EOF

echo "E2E report saved to: $REPORT_FILE"
  • 报告必须是有效的 Markdown(符合基本的 markdownlint 规则):标题级别正确、无尾随空格、表格竖线分隔符一致。
  • 写入报告后不得再修改(REQ-NR005)。
  • 设置变量 REPORT_PATH="$REPORT_FILE",供第 8 阶段使用。
第 7 阶段:拆除环境

目标:优雅地停止第 3 阶段启动的所有运行时资源,确认端口和进程已完全释放,并记录结果。如果传入了 --keep-alive,则完全跳过拆除并向用户发出警告。

7.1 --keep-alive 守卫
if [ "$KEEP_ALIVE" = "true" ]; then
    TEARDOWN_EXECUTED="skipped (--keep-alive)"
    PORT_RELEASED="skipped"
    REMAINING_PROCESSES="skipped"
    echo "WARNING: --keep-alive was passed. The runtime environment is still running."
    echo "You are responsible for cleaning up:"
    if [ "$APP_TYPE" = "Docker-managed" ]; then
        echo "  docker compose -f '${PROJECT_ROOT}/docker-compose.yml' down"
    else
        echo "  kill $(lsof -ti:${TARGET_PORT})"
    fi
    echo "Report was saved before teardown; artifacts are in ${ARTIFACT_DIR}."
    # Skip remaining teardown steps and proceed to Phase 8
    # (Phase 7 cleanup is bypassed when --keep-alive is active)
fi
7.2 按应用类型优雅地拆除

Docker 管理的应用(AC-029):

TEARDOWN_EXECUTED="true"
DOCKER_TEARDOWN_FAILED=false

if [ "$APP_TYPE" = "Docker-managed" ]; then
    cd "$PROJECT_ROOT"
    if docker compose down >> "$STARTUP_LOGS_FILE" 2>&1; then
        echo "Docker Compose stack stopped successfully."
    else
        echo "WARNING: docker compose down exited with a non-zero code."
        DOCKER_TEARDOWN_FAILED=true
    fi
fi

本地进程(JVM、Node.js/NestJS、Python、桌面应用)(AC-029):

if [ -n "$STARTUP_PID" ] && kill -0 "$STARTUP_PID" 2>/dev/null; then
    echo "Sending SIGTERM to process $STARTUP_PID ..."
    kill -TERM "$STARTUP_PID" 2>/dev/null || true
    # Wait up to 10 seconds for graceful exit
    GRACEFUL_WAIT=0
    while [ "$GRACEFUL_WAIT" -lt 10 ] && kill -0 "$STARTUP_PID" 2>/dev/null; do
        sleep 1
        GRACEFUL_WAIT=$((GRACEFUL_WAIT + 1))
    done
    # If still alive, send SIGKILL
    if kill -0 "$STARTUP_PID" 2>/dev/null; then
        echo "Process $STARTUP_PID did not exit after 10s; sending SIGKILL."
        kill -KILL "$STARTUP_PID" 2>/dev/null || true
        sleep 2
    fi
fi

# Also terminate any child processes that may still hold the port
PORT_PIDS=$(lsof -ti:"$TARGET_PORT" 2>/dev/null || true)
if [ -n "$PORT_PIDS" ]; then
    echo "Additional PIDs holding port $TARGET_PORT: $PORT_PIDS"
    for pid in $PORT_PIDS; do
        kill -TERM "$pid" 2>/dev/null || true
    done
    sleep 3
    for pid in $PORT_PIDS; do
        if kill -0 "$pid" 2>/dev/null; then
            kill -KILL "$pid" 2>/dev/null || true
        fi
    done
fi

紧急清理 — 崩溃测试遗留的孤立进程:

# If the original STARTUP_PID is empty or already dead, but the port is still occupied,
# this indicates an orphan process from a crash or a detached child.
if [ -z "$STARTUP_PID" ] || ! kill -0 "$STARTUP_PID" 2>/dev/null; then
    ORPHAN_PIDS=$(lsof -ti:"$TARGET_PORT" 2>/dev/null || true)
    if [ -n "$ORPHAN_PIDS" ]; then
        echo "WARNING: Orphan process(es) detected on port $TARGET_PORT: $ORPHAN_PIDS"
        for pid in $ORPHAN_PIDS; do
            kill -TERM "$pid" 2>/dev/null || true
        done
        sleep 3
        for pid in $ORPHAN_PIDS; do
            if kill -0 "$pid" 2>/dev/null; then
                kill -KILL "$pid" 2>/dev/null || true
            fi
        done
    fi
fi
7.3 确认端口已释放(AC-030)
PORT_RELEASED="false"
PORT_CHECK_WAIT=0
MAX_PORT_CHECK_WAIT=15

while [ "$PORT_CHECK_WAIT" -lt "$MAX_PORT_CHECK_WAIT" ]; do
    if ! lsof -i :"$TARGET_PORT" >/dev/null 2>&1 && ! nc -z localhost "$TARGET_PORT" 2>/dev/null; then
        PORT_RELEASED="true"
        echo "Port $TARGET_PORT is free."
        break
    fi
    sleep 1
    PORT_CHECK_WAIT=$((PORT_CHECK_WAIT + 1))
done

if [ "$PORT_RELEASED" != "true" ]; then
    echo "WARNING: Port $TARGET_PORT is still occupied after teardown."
fi
7.4 识别剩余资源(REQ-023)
REMAINING_PROCESSES="none"

if [ "$APP_TYPE" = "Docker-managed" ]; then
    REMAINING_CONTAINERS=$(docker compose ps --format '{{.Name}}' 2>/dev/null | tr '\n' ',' | sed 's/,$//')
    if [ -n "$REMAINING_CONTAINERS" ]; then
        REMAINING_PROCESSES="containers: $REMAINING_CONTAINERS"
    fi
else
    REMAINING_PIDS=$(lsof -ti:"$TARGET_PORT" 2>/dev/null | tr '\n' ' ' | sed 's/ $//')
    if [ -n "$REMAINING_PIDS" ]; then
        REMAINING_PROCESSES="PIDs: $REMAINING_PIDS"
    fi
fi
7.5 未完全拆除时警告用户
if [ "$PORT_RELEASED" != "true" ] || [ "$DOCKER_TEARDOWN_FAILED" = "true" ]; then
    echo "WARNING: Teardown did not complete cleanly. Manual cleanup may be required."
    if [ "$APP_TYPE" = "Docker-managed" ]; then
        echo "  Remaining containers: ${REMAINING_PROCESSES}"
        echo "  Manual command: docker compose -f '${PROJECT_ROOT}/docker-compose.yml' down"
    else
        echo "  Remaining PIDs on port ${TARGET_PORT}: ${REMAINING_PIDS}"
        echo "  Manual command: kill ${REMAINING_PIDS}"
    fi
fi
7.6 记录拆除状态

就地更新报告文件(如果第 6 阶段未写入“拆除状态”章节,则追加该章节;或者在第 6 阶段完成前确保变量已正确设置)。推荐顺序如下:

  1. 第 6 阶段写入包含拆除状态占位符的报告。
  2. 第 7 阶段执行拆除。
  3. 第 7 阶段通过就地编辑(例如 sed)将占位符更新为实际值:
sed -i.bak \
    -e "s/\${TEARDOWN_EXECUTED}/${TEARDOWN_EXECUTED}/g" \
    -e "s/\${PORT_RELEASED}/${PORT_RELEASED}/g" \
    -e "s/\${REMAINING_PROCESSES}/${REMAINING_PROCESSES}/g" \
    "$REPORT_FILE" && rm -f "$REPORT_FILE.bak"

如果在拆除前报告已经定稿(这是有效的替代方案),则在报告末尾追加 拆除后更新 章节:

## Post-Teardown Update

| Property | Value |
|----------|-------|
| Teardown Executed | ${TEARDOWN_EXECUTED} |
| Port Released | ${PORT_RELEASED} |
| Remaining Processes | ${REMAINING_PROCESSES} |

重要:拆除期间不得删除报告或修改任何 spec/task 文件(REQ-NR005)。

第 8 阶段:呈现结果
  1. 向用户打印简明摘要:
    • ✅ 已验证:N | ❌ 失败:N | ⚠️ 需手动检查:N
    • 报告路径
    • 拆除状态(或 --keep-alive 提示)
  2. 如果有任何 AC 未通过:
    • 突出显示未通过的 AC 及相关证据
    • 建议针对失败的任务再次运行 /developer-kit-specs:specs.task-implementation
  3. 将所有待办事项标记为已完成。

示例

Spring Boot REST API 验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/001-user-auth/

操作:

  1. 检测 pom.xml + docker-compose.yml → 由 Docker 管理的 Spring Boot
  2. 运行 docker compose up -d
  3. 等待 localhost:8080/actuator/health
  4. 从规范中读取 AC;查找描述登录端点的 [IMP] AC
  5. 执行:curl -s -w "\n%{http_code}" -X POST http://localhost:8080/api/login -d '{"email":"[email protected]","password":"secret"}'
  6. 断言状态为 200,且响应包含 token
  7. 生成报告:docs/specs/001-user-auth/e2e-report-2026-05-31-143022.md
  8. 运行 docker compose down
React SPA 验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/002-dashboard/

操作:

  1. 检测包含 react 依赖项的 package.json
  2. 运行 npm run dev;等待 localhost:5173
  3. 启动 Playwright,导航至 /dashboard
  4. AC 指出“用户可以点击‘刷新’以重新加载数据” → page.click('[data-testid=refresh]'),断言表格行已更新
  5. 成功时截屏;失败时捕获现场
  6. 生成包含屏幕截图工件的报告
  7. 终止开发服务器进程
桌面应用验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/003-settings-app/

操作:

  1. 检测 src-tauri/Cargo.toml → Tauri 桌面应用
  2. 运行 cargo tauri build --debug,然后启动 .app/.exe
  3. 使用计算机操作验证设置窗口是否打开
  4. AC 指出“用户可以切换深色模式” → 点击切换开关、截屏并验证主题变化
  5. 生成包含各步骤屏幕截图的报告
  6. 关闭应用并验证进程已终止

命令白名单

只有 references/test-execution-patterns.md 中记录的命令才可自动执行。白名单涵盖:

类别允许的命令
Dockerdocker compose up -d --build、docker compose down、docker compose ps、docker compose logs
JVM / Spring Boot./mvnw spring-boot:run、./gradlew bootRun
Node.js / NestJSnpm run dev、npm run start:dev、npm start、node server.js
Pythonuvicorn main:app --reload、python manage.py runserver、flask run
桌面应用(Tauri)cargo tauri build --debug、cargo tauri dev、open *.app、直接执行二进制文件
桌面应用(Electron)npm run electron:dev、npx electron .
测试curl(不带 -k/--insecure,除非传入 --insecure)、npx playwright、node(用于内联 Playwright 脚本)
清理docker compose down、kill -TERM、kill -KILL(仅用于卡住的进程)、lsof、nc

非白名单命令策略:如果派生出的命令不在白名单中 → 使用 AskUserQuestion 请求用户明确确认后再执行。用户必须批准该命令;否则,中止操作。

安全防护与禁止事项

以下禁止事项(REQ-NR001 至 REQ-NR008)会在技能工作流的多个环节中强制执行:

REQ-NR001:禁止破坏性命令

系统在启动、测试或清理期间不得运行 sudo、rm -rf、docker system prune、mkfs、dd 或任何其他破坏性系统命令。

强制措施:

  • 阶段 1.5 会在执行前扫描所有命令,检查是否包含禁止模式。
  • 阶段 3 的数据完整性预检会拒绝可能破坏数据的命令。
  • 清理命令仅限于 docker compose down 和正常终止进程(仅对卡住的孤儿进程使用 kill -TERM / kill -KILL)。
REQ-NR002:报告中不得包含机密信息

系统不得在 E2E 报告中暴露机密信息、API 密钥或密码。

强制措施:

  • 阶段 6 的脱敏处理会移除:
    • Authorization: Bearer <token> → Authorization: Bearer ***REDACTED***
    • E2E_AUTH_TOKEN=...、E2E_USERNAME=...、E2E_PASSWORD=... → E2E_xxx=***REDACTED***
    • "token": "..." → "token": "***REDACTED***"
    • curl -u user:password 中的密码 → curl -u user:***REDACTED***
  • 报告中只会写入脱敏后的日志。
  • 脱敏后会立即丢弃未经脱敏的原始临时文件。
REQ-NR003:默认不绕过 TLS

系统 SHALL NOT 默认在 curl 中禁用 TLS 证书验证(-k / --insecure)。

执行措施:

  • 阶段 1.5 会拒绝包含 -k 或 --insecure 的 curl 命令,除非明确传入了 --insecure 标志。
  • 阶段 4 的 curl 命令生成规则明确禁止添加 -k / --insecure。
  • 如果传入 --insecure → 会记录警告:“TLS 证书验证已禁用。仅用于本地开发。”
REQ-NR004:启动时不覆盖数据

除非启动命令本身会执行此操作,否则系统 SHALL NOT 覆盖或删除现有数据库、卷或本地数据。

执行措施:

  • 阶段 1.5 的数据完整性预检查会拒绝包含 rm、--volumes、prune、drop 或类似破坏性数据模式的命令。
  • 如果命令会销毁现有数据,阶段 3 的数据完整性启动前检查会中止启动。
REQ-NR005:不修改规格或任务文件

系统 SHALL NOT 在生成报告或拆除过程中修改功能规格或任务文件。

执行措施:

  • 阶段 6 仅将报告写入新文件(e2e-report-YYYY-MM-DD-HHMMSS.md)。
  • 阶段 7 拆除时仅向报告追加“拆除后更新”部分;绝不修改规格或任务文件。
  • 此技能对源代码、规格和任务文件均为只读。
REQ-NR006:失败时不遗留孤立进程

启动失败时,系统 SHALL NOT 留下仍在运行的孤立进程或容器。

执行措施:

  • 阶段 3 Docker 启动:超时时,执行 docker compose down 后再退出。
  • 阶段 3 Spring Boot / Node.js 启动:超时时,执行 kill -TERM $STARTUP_PID 后再退出。
  • 阶段 7 拆除时会紧急清理仍占用目标端口的孤立进程。
REQ-NR007:强制执行单项测试超时限制

系统 SHALL NOT 因测试挂起而无限期阻塞。

执行措施:

  • 阶段 4 Playwright 测试:使用 timeout --signal=TERM <N> 包装;如果发生超时,则将 AC 标记为 FAILED。
  • 阶段 4 桌面测试:通过手动轮询循环,将经过时间与 TEST_TIMEOUT_SEC 进行比较;超过限制时,将 AC 标记为 FAILED。
  • 如果测试挂起,会终止测试并标记为 FAILED,绝不会标记为 VERIFIED 或 PENDING。
REQ-NR008:curl 测试失败时不重试

curl 请求失败时,系统 SHALL NOT 重试。

执行措施:

  • 阶段 4 curl 执行:每条命令恰好执行一次。
  • 不使用 --retry 标志,不使用重试循环,也不执行回退重试。
  • 立即记录首次结果;如果失败,则将 AC 标记为 FAILED。

约束和警告

  • 源代码只读:此技能绝不编辑应用程序源代码、测试文件、配置、功能规格或任务文件。
  • 不自动安装:如果缺少 Playwright 或 Docker,此技能会报告缺项并建议安装命令。它不会自动运行 npm install -g 或 brew install。
  • 仅限本地:测试针对 localhost 运行。远程 URL、预发布环境或生产环境端点不在范围内。
  • 破坏性操作防护:启动、测试和拆除命令均依据 references/test-execution-patterns.md 加入白名单。任何不在白名单中的命令都需要通过 AskUserQuestion 获得用户确认。
  • 密钥保护:报告会对授权标头、bearer 令牌、E2E_* 环境变量和密码进行脱敏。仅显示标头名称和脱敏后的值。
  • TLS 默认设置:curl 命令默认使用严格的 TLS 验证。仅在本地开发使用自签名证书时传入 --insecure。
  • 清理责任:如果传入 --keep-alive,用户需负责手动拆除。
  • 不创建持久化测试套件:此技能会生成临时测试以供验证,不会在项目中创建永久测试文件。

评论

支持 Markdown