关于 specs-e2e-verification
用于根据真实运行行为验证已实现规格的技能。它会检测并启动本地环境,根据 `[IMP]` 验收标准生成测试、映射结果、创建报告并清理环境。
主要功能
- 01检测应用类型与端口
- 02启动并检查本地运行环境
- 03根据 `[IMP]` 标准生成测试
- 04将测试结果映射到标准
- 05生成验证报告
- 06清理运行环境
适用场景
- 01验证实现后的功能
- 02检查运行中 API 的行为
- 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 验证
概述
在规格实现并清理完成后,执行真实环境验证。此技能弥合了单元测试代码与可观察运行时行为之间的差距,具体步骤如下:
- 根据项目文件检测应用类型
- 启动本地运行环境(Docker Compose、开发服务器、Spring Boot 等)
- 根据功能规格中的
[IMP]验收标准推导测试 - 执行真实测试(
curl、Playwright、计算机操作) - 将结果对应到验收标准
- 生成验证报告
- 拆除运行环境
输入: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:解析参数并加载上下文
-
解析
$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)。
-
阅读功能规格并提取:
- 所有带分类标签(
[IMP]、[SEF]、[EXT])的验收标准 - 仅根据
[IMP]标准生成运行时测试
- 所有带分类标签(
-
如果提供了
--task,则阅读任务文件,并将范围限定为其provides文件及相关验收标准。 -
使用
TodoWrite为全部 8 个阶段创建待办事项清单。
阶段 1.5:安全验证关卡
执行任何命令之前,运行以下安全检查:
-
命令白名单检查:
- 第 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 被拒绝。
- 第 3 阶段推导出的启动命令 MUST 匹配
-
禁用模式扫描(REQ-NR001):
- 扫描推导出的启动命令和所有生成的测试命令,检查是否包含:
sudo→ 中止并提示:"禁止:E2E 验证期间不允许使用 sudo。"rm -rf→ 中止并提示:"禁止:E2E 验证期间不允许使用 rm -rf。"docker system prune→ 中止并提示:"禁止:E2E 验证期间不允许使用 docker system prune。"- 任何针对数据库、卷或本地数据执行
rm、drop、destroy、prune操作的命令 → 中止并提示:"禁止:不允许执行破坏性数据操作。"
- IF 检测到任何禁用模式 → 立即中止;不得继续启动。
- 扫描推导出的启动命令和所有生成的测试命令,检查是否包含:
-
TLS 强制检查(REQ-NR003):
- 对于任何生成的 curl 命令:
- IF 命令包含
-k或--insecure且未传入--insecure→ 中止并提示:"禁止:curl TLS 绕过(-k / --insecure)默认处于禁用状态。传入 --insecure 以选择启用。" - IF 已传入
--insecure→ 记录警告:"WARNING:TLS 证书验证已禁用。仅用于本地开发。"
- IF 命令包含
- 对于任何生成的 curl 命令:
-
数据完整性预检(REQ-NR004):
- 执行启动命令之前,检查命令是否包含会覆盖或删除现有数据的模式(例如
rm、drop、prune、卷删除标志)。 - IF 命令会修改现有数据库、卷或本地数据目录 → 中止并提示:"禁止:启动命令不得覆盖或删除现有数据。"
- 执行启动命令之前,检查命令是否包含会覆盖或删除现有数据的模式(例如
阶段 2:检测应用类型并发现端口
-
将
PROJECT_ROOT设置为包含.git的目录,或--spec的父目录。 -
使用以下启发式规则检查
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。
-
应用优先级规则:
- 始终优先选择 Docker Compose:如果存在 Docker Compose 文件,且还存在任何框架配置,则将其归类为 Docker 管理。Compose 堆栈定义了运行时。
- 如果检测到多个非 Docker 配置(例如,同时存在
pom.xml和package.json,但没有 Docker Compose):- 如果规范的领域明确指向后端或前端(例如,规范标题包含 "API"、"backend"、"service"),优先选择 JVM 或 NestJS。
- 如果规范的领域明确指向前端(例如,规范标题包含 "UI"、"page"、"component"),优先选择 Web SPA。
- 如果仍无法确定,则进入用户提示(第 4 步)。
-
如果未找到可识别的配置,或者存在多个非 Docker 配置且规范领域不明确,则使用
AskUserQuestion,选项必须完全如下:- "REST API"
- "Web SPA"
- "Desktop"
- "Skip"
-
发现端口:确定应用类型后,按以下顺序检查框架配置文件,以确定目标端口:
-
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 / NestJS 3000 Spring Boot(JVM) 8080 Angular 4200 Vite(React/Vue) 5173 Python 8000
-
-
记录检测到的类型和发现的端口;两者都会写入报告。
阶段 3:启动环境并等待就绪
阅读 references/test-execution-patterns.md(随此技能附带),了解命令映射。根据检测结果:
数据完整性预检(REQ-NR004):执行启动命令之前,确认该命令不包含会覆盖或删除现有数据库、卷或本地数据的模式(例如,rm、--volumes、prune、drop)。如果检测到破坏性模式,立即中止,并提示:"Startup aborted: command would destroy existing data."
-
初始化运行时状态:
STARTUP_COMMAND=""— 用于启动环境的确切命令HEALTH_CHECK_METHOD=""— 就绪状态的判定方式说明STARTUP_LOGS_FILE="$(mktemp)"— 用于捕获启动标准输出/标准错误的临时文件STARTUP_TIMEOUT="${TIMEOUT:-120}"— 等待就绪的秒数START_TIME="$(date +%s)"STARTUP_PID=""— 后台进程 PID(用于本地进程)
-
启动前端口检查(所有类型):
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 -
由 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
- 验证 Docker 守护进程是否可访问:
-
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
- 确定构建工具并在后台启动,同时捕获日志:
-
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),并将首条匹配行记录在报告中,作为启动成功的证据。
- 确认
-
Python(FastAPI / Django / Flask)(参见
references/test-execution-patterns.md):- 运行
uvicorn main:app --reload、python manage.py runserver或flask run - 等待端口就绪
- 运行
-
桌面应用(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 - Tauri:
-
启动后记录(所有类型):
- 在报告元数据中记录
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.test | E2E_AUTH_TOKEN=...、E2E_USERNAME=...、E2E_PASSWORD=... |
.env.local | E2E_AUTH_TOKEN=...、E2E_USERNAME=...、E2E_PASSWORD=... |
application-test.yml | e2e.auth-token: ...、e2e.username: ...、e2e.password: ... |
application-test.properties | e2e.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(); |
选择器优先级(从最具体到最不具体):
[data-testid=...]— 首选,最稳定#id— 唯一元素 ID.class-name— CSS 类[name=...]— 表单元素名称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. 失败时捕获屏幕截图
任何断言或操作失败时:
- 使用
page.screenshot({ path: ..., fullPage: true })捕获整页屏幕截图。 - 保存到
${ARTIFACT_DIR}/screenshot-<timestamp>-ac-<AC_ID>.png。 - 在测试结果证据中记录屏幕截图路径。
测试通过时,屏幕截图为可选项,只有传入 --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_id | AC 标识符(例如,AC-012) |
ac_text | 完整的验收标准文本,显示时截断为 120 个字符 |
status | PENDING(在 5.2 中更新) |
evidence | 空字符串(在 5.2 中更新) |
actual | 空字符串(仅在不匹配时填充) |
expected | 空字符串(仅在不匹配时填充) |
遍历规格说明中的验收标准表,仅包含标签为 [IMP] 的行。[SEF] 和 [EXT] 验收标准不纳入运行时验证;可以在附录中列出,但无需给出状态判定。
5.2 应用判定规则
对于第 4 阶段测试所针对的每条 AC,按以下顺序应用规则:
-
VERIFIED(REQ-016):
- IF 测试完成,且未出现断言失败、超时或工具错误
- THEN 将
status设为"VERIFIED" - 将
evidence设为简明描述:- REST API:
curl 返回了 HTTP <code>,耗时 <N>ms - Web SPA:
Playwright 断言通过:<selector> <condition> - 桌面端:
GUI 自动化确认:<window/element> 存在
- REST API:
-
FAILED(REQ-018):
- IF 任意断言失败、测试超时、工具崩溃,或开发服务器无法访问
- THEN 将
status设为"FAILED" - 将
actual设为观察到的值(例如,HTTP 400、未找到元素、30s 后超时) - 将
expected设为 AC 要求的值(例如,HTTP 201、元素可见) - 将
evidence设为人类可读的句子,说明actual与expected的对比;如果有捕获的工件(截图、响应转储),还应包含其路径。
-
MANUAL CHECK REQUIRED(REQ-017):
- IF 因以下原因未为该 AC 生成测试:
- AC 文本缺少可解析的端点/UI/桌面行为提示
- 缺少必需工具(例如,未安装 Playwright)
- AC 描述的是需要人工判断的内容(例如,“UI 看起来正确”)
- THEN 将
status设为"MANUAL CHECK REQUIRED" - 将
evidence设为原因:无法推导出自动化测试:<reason>
- IF 因以下原因未为该 AC 生成测试:
重要:每条 [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
- Docker:
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/unknownREMAINING_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 阶段完成前确保变量已正确设置)。推荐顺序如下:
- 第 6 阶段写入包含拆除状态占位符的报告。
- 第 7 阶段执行拆除。
- 第 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 阶段:呈现结果
- 向用户打印简明摘要:
- ✅ 已验证:N | ❌ 失败:N | ⚠️ 需手动检查:N
- 报告路径
- 拆除状态(或
--keep-alive提示)
- 如果有任何 AC 未通过:
- 突出显示未通过的 AC 及相关证据
- 建议针对失败的任务再次运行
/developer-kit-specs:specs.task-implementation
- 将所有待办事项标记为已完成。
示例
Spring Boot REST API 验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/001-user-auth/
操作:
- 检测
pom.xml+docker-compose.yml→ 由 Docker 管理的 Spring Boot - 运行
docker compose up -d - 等待
localhost:8080/actuator/health - 从规范中读取 AC;查找描述登录端点的
[IMP]AC - 执行:
curl -s -w "\n%{http_code}" -X POST http://localhost:8080/api/login -d '{"email":"[email protected]","password":"secret"}' - 断言状态为
200,且响应包含token - 生成报告:
docs/specs/001-user-auth/e2e-report-2026-05-31-143022.md - 运行
docker compose down
React SPA 验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/002-dashboard/
操作:
- 检测包含
react依赖项的package.json - 运行
npm run dev;等待localhost:5173 - 启动 Playwright,导航至
/dashboard - AC 指出“用户可以点击‘刷新’以重新加载数据” →
page.click('[data-testid=refresh]'),断言表格行已更新 - 成功时截屏;失败时捕获现场
- 生成包含屏幕截图工件的报告
- 终止开发服务器进程
桌面应用验证
/developer-kit-specs:specs.e2e-verify --spec=docs/specs/003-settings-app/
操作:
- 检测
src-tauri/Cargo.toml→ Tauri 桌面应用 - 运行
cargo tauri build --debug,然后启动.app/.exe - 使用计算机操作验证设置窗口是否打开
- AC 指出“用户可以切换深色模式” → 点击切换开关、截屏并验证主题变化
- 生成包含各步骤屏幕截图的报告
- 关闭应用并验证进程已终止
命令白名单
只有 references/test-execution-patterns.md 中记录的命令才可自动执行。白名单涵盖:
| 类别 | 允许的命令 |
|---|---|
| Docker | docker compose up -d --build、docker compose down、docker compose ps、docker compose logs |
| JVM / Spring Boot | ./mvnw spring-boot:run、./gradlew bootRun |
| Node.js / NestJS | npm run dev、npm run start:dev、npm start、node server.js |
| Python | uvicorn 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,用户需负责手动拆除。 - 不创建持久化测试套件:此技能会生成临时测试以供验证,不会在项目中创建永久测试文件。