# Test Env Setup

> 测试环境自动准备 Skill。在执行任何测试之前，自动检查并准备完整的测试环境：服务健康检查、自动启动后端、数据库就绪、测试账号创建、依赖验证。确保后续测试在稳定环境中执行。触发词：「准备测试环境」「环境预检」「初始化测试环境」「测试前准备」。也被 role-测试工程师 Step 0 和 fixer Step A0/B0 自动调用。

- Skill: `tashangkd/test-env-setup` (Agent Skill)
- Install (CLI): `npx skillmds@latest add tashangkd/test-env-setup`
- Raw SKILL.md: https://api.skillmd.com/api/skills/tashangkd/test-env-setup/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: TashanGKD (https://skillmd.com/u/tashangkd)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/tashangkd/test-env-setup

---


# 测试环境自动准备（test-env-setup）

> **使命**：让「说一句话就能开始测试」成为现实。
> 在测试智能体开始执行任何测试用例之前，自动完成所有环境准备工作。
> 若环境不可修复，输出明确的人工干预指引，不静默失败。

---

## 知识导航表

| 层级 | 文档 | 用途 |
|---|---|---|
| **D1 项目配置** | `项目群/[项目]/.cursor/test-config.md`（若存在）| 项目专属配置：端口/账号/技术栈 |
| **D2 技术架构** | `项目群/[项目]/2_技术架构/技术框架.md` | 了解服务架构和启动方式 |
| **D3 部署文档** | `项目群/[项目]/DEPLOY_ARCH.md` | 生产/测试账号信息 |

---

## 激活后立即执行

```
Step 0  【读取项目配置】
        先尝试读取 .cursor/test-config.md（项目专属配置）
        IF 不存在：使用智能推断（见后文「自动推断逻辑」）

Step 1  【后端服务健康检查 + 自动启动】

        1.1 检查服务是否在目标端口响应：
        ```bash
        curl -s --max-time 3 http://localhost:[PORT]/health 2>/dev/null
        ```
        
        IF 响应正常（含 "ok" 或 HTTP 200）：
          → 输出「✅ 后端服务正常（端口 [PORT]）」
          → 跳到 Step 2
        
        IF 无响应：
          1.2 检查是否有进程占用端口：
          ```bash
          lsof -i :[PORT] 2>/dev/null | head -5
          ```
          
          IF 有进程但未响应 → kill 并重启：
          ```bash
          pkill -f "uvicorn.*[PORT]" 2>/dev/null || true
          pkill -f "node.*[PORT]" 2>/dev/null || true
          sleep 2
          ```
          
          1.3 自动启动服务（根据技术栈）：
          
          FastAPI/Python 项目：
          ```bash
          cd [项目根目录]
          # 激活虚拟环境（优先 .venv，其次 venv，其次系统 Python）
          PYTHON=$([ -f .venv/bin/python ] && echo .venv/bin/python || \
                   [ -f venv/bin/python ] && echo venv/bin/python || \
                   echo python3)
          # 后台启动，日志写入 /tmp/test-backend.log
          nohup $PYTHON -m uvicorn backend.app.main:app \
            --host 0.0.0.0 --port [PORT] --workers 1 \
            > /tmp/test-backend.log 2>&1 &
          echo "启动PID: $!"
          ```
          
          Node.js/Next.js 项目：
          ```bash
          cd [项目根目录]
          nohup npm run dev > /tmp/test-frontend.log 2>&1 &
          ```
          
          1.4 等待服务就绪（最多 30 秒）：
          ```bash
          for i in $(seq 1 10); do
            sleep 3
            curl -s http://localhost:[PORT]/health && echo "✅ 服务就绪" && break
            echo "等待中... ($i/10)"
          done
          ```
          
          IF 30 秒后仍无响应：
          → 读取启动日志诊断：cat /tmp/test-backend.log | tail -20
          → 输出错误信息和诊断建议
          → 停止，输出：「❌ 后端服务无法自动启动，需要人工检查」
          → 附上日志内容供人工排查

Step 2  【数据库连接验证】
        ```bash
        # 通过服务健康端点间接验证 DB
        curl -s http://localhost:[PORT]/config-check 2>/dev/null | python3 -c "
        import sys, json
        d = json.load(sys.stdin)
        print('DB:', '✅' if d.get('db_configured') else '❌')
        " 2>/dev/null || echo "config-check 端点不可用（跳过DB验证）"
        ```
        
        IF DB 未连接（db_configured=false）：
          → 尝试启动数据库（如有 docker-compose）：
          ```bash
          docker-compose -f docker-compose.dev.yml up -d postgres 2>/dev/null || true
          sleep 5
          ```
          → 重新检查
          IF 仍然无法连接 → 输出「❌ 数据库连接失败，需要人工检查」

Step 3  【测试账号验证 + 自动创建（完整 Persona 体系）】
        
        需要创建以下 4 类测试人格账号（Personas）：
        
        ```python
        # 测试 Persona 定义（从 test-config.md 读取，以下为默认值）
        PERSONAS = [
            # admin：管理员，生成邀请码
            {"phone": "10000000001", "password": "admin2026",
             "role": "admin",    "handle": "test_admin", "display_name": "测试管理员"},
            
            # rich：丰富认知库，Proxy 测试目标 + Owner 模式测试
            {"phone": "19900000001", "password": "sim2026",
             "role": "creator",  "handle": "user_rich",  "display_name": "认知丰富用户",
             "seed_workspace": True,  # 需要预装知识库
             "visibility": "public_free"},  # 公开供 Proxy 测试
            
            # empty：空 workspace，冷启动/新用户测试
            {"phone": "19900000088", "password": "newuser2026",
             "role": "creator",  "handle": "user_empty", "display_name": "新用户"},
            
            # visitor：访客，专门用于 Proxy 对话 / 消息系统测试
            {"phone": "19900000099", "password": "visitor2026",
             "role": "user",     "handle": "user_visitor", "display_name": "访客测试者"},
        ]
        ```
        
        验证并创建缺失账号：
        ```python
        import requests, json
        
        BASE_URL = "http://localhost:[PORT]"
        
        def check_account(phone, password):
            r = requests.post(f"{BASE_URL}/auth/login", json={"phone": phone, "password": password})
            return r.status_code == 200, r.json() if r.status_code == 200 else None
        
        # 先确保 admin 账号存在（用 SQL 直接创建，绕过邀请码）
        admin_exists, _ = check_account("10000000001", "admin2026")
        IF not admin_exists:
            # 用终端执行 Python 脚本直接写 DB
            python -c "
            import sys; sys.path.insert(0, 'backend')
            from core.db import get_db
            from core.auth import create_user, hash_password, create_profile
            with get_db() as db:
                uid = create_user(db, '10000000001', hash_password('admin2026'))
                create_profile(db, uid, role='admin', display_name='测试管理员')
                print(f'Admin 创建成功: {uid}')
            "
        
        # 用 admin 账号生成邀请码并创建其他账号
        _, admin_data = check_account("10000000001", "admin2026")
        admin_token = admin_data["access_token"]
        
        r = requests.post(f"{BASE_URL}/admin/invite-codes",
                          headers={"Authorization": f"Bearer {admin_token}"},
                          params={"count": 10, "expire_days": 365})
        codes = r.json()["codes"]
        code_idx = 0
        
        for persona in PERSONAS[1:]:  # 跳过 admin
            exists, _ = check_account(persona["phone"], persona["password"])
            if not exists:
                r = requests.post(f"{BASE_URL}/auth/register", json={
                    "phone": persona["phone"],
                    "password": persona["password"],
                    "invite_code": codes[code_idx],
                    "display_name": persona["display_name"],
                    "handle": persona["handle"],
                })
                print(f"注册 {persona['handle']}: {r.status_code}")
                code_idx += 1

Step 3.5  【预装知识库（seed_workspace 标记的账号）】
        
        对标记了 seed_workspace=True 的账号（默认：user_rich），
        将预制知识库内容复制到其 workspace：
        
        ```bash
        # 检查 fixture 目录是否存在
        FIXTURE_DIR="tests/fixtures/rich_workspace"
        
        if [ ! -d "$FIXTURE_DIR" ]; then
            echo "⚠️ Fixture 目录不存在，正在从模板创建..."
            mkdir -p "$FIXTURE_DIR/sources"
            mkdir -p "$FIXTURE_DIR/docs/L1/产品理论维度"
            mkdir -p "$FIXTURE_DIR/docs/L2"
            
            # 创建已知内容的 fixture 文档（用于精确断言）
            cat > "$FIXTURE_DIR/sources/认知杠杆论.md" << 'EOF'
        # 认知杠杆论
        
        AI时代的核心竞争力是认知结构而非执行效率。
        知识管理的本质是建立连接，而非存储。
        个人认知结构的稀缺性，决定了它在AI时代的价值。
        EOF
            
            cat > "$FIXTURE_DIR/sources/研究方法论.md" << 'EOF'
        # 自下而上的系统性研究方法
        
        从具体案例出发，逐步归纳规律，最后升维到原则。
        避免一开始就构造大框架的陷阱。
        EOF
            
            echo "✅ Fixture 内容已创建"
        fi
        ```
        
        将 fixture 触发 build-cognition（让 AI 真正处理并建立认知结构）：
        
        ```python
        import requests, time
        
        # 登录 rich 账号
        _, rich_data = check_account("19900000001", "sim2026")
        rich_token = rich_data["access_token"]
        rich_ws = rich_data["workspace_id"]
        rich_headers = {"Authorization": f"Bearer {rich_token}"}
        
        # 检查是否已有认知内容（避免重复 seed）
        r = requests.get(f"{BASE_URL}/doc-registry", headers=rich_headers)
        existing_docs = r.json().get("docs", [])
        
        if len(existing_docs) < 3:  # 少于3个文档，说明需要 seed
            # 上传 fixture 文件
            import os
            from pathlib import Path
            
            fixture_files = list(Path("tests/fixtures/rich_workspace/sources").glob("*.md"))
            files = [("files", (f.name, open(f, "rb"), "text/markdown")) for f in fixture_files]
            
            r = requests.post(f"{BASE_URL}/twin/upload", headers=rich_headers, files=files)
            print(f"上传 fixture 文件: {r.status_code}, 文件数: {r.json().get('file_count')}")
            
            # 触发认知构建（通过 SSE 等待完成）
            print("⏳ 触发认知构建，等待完成（最多3分钟）...")
            with requests.post(f"{BASE_URL}/twin/build-cognition",
                               headers=rich_headers, stream=True, timeout=180) as r:
                for line in r.iter_lines():
                    if b'"done"' in line or b'build_done' in line:
                        print("✅ 认知构建完成")
                        break
        else:
            print(f"✅ rich 账号已有 {len(existing_docs)} 个文档，跳过 seed")
        
        # 设置 rich 账号的分身为公开（供 Proxy 测试）
        requests.post(f"{BASE_URL}/brain-square/visibility",
                      headers=rich_headers,
                      json={"visibility": "public_free"})
        print("✅ rich 账号设为公开（供 Proxy 测试）")
        ```

Step 4  【Python 依赖验证】
        ```bash
        cd [项目根目录]
        # 验证关键依赖是否安装
        $PYTHON -c "import requests, pytest, sseclient" 2>/dev/null && \
          echo "✅ Python 依赖齐全" || \
          (echo "⚠️ 安装缺失依赖..." && pip install requests pytest sseclient-py 2>&1 | tail -5)
        ```

Step 5  【最终就绪确认 + 输出报告】
        ```bash
        # 最终健康检查
        HEALTH=$(curl -s http://localhost:[PORT]/health 2>/dev/null)
        echo "服务状态: $HEALTH"
        
        # 验证两个测试账号均可登录
        TOKEN_MAIN=$(curl -s -X POST http://localhost:[PORT]/auth/login \
          -H "Content-Type: application/json" \
          -d '{"phone":"[TEST_PHONE_MAIN]","password":"[TEST_PASSWORD_MAIN]"}' \
          | python3 -c "import sys,json; d=json.load(sys.stdin); print(d.get('access_token','FAIL')[:20])")
        
        echo "主账号 Token: $TOKEN_MAIN"
        ```
        
        输出就绪报告：
        「✅ 测试环境就绪
         
         服务状态：✅ localhost:[PORT]/health → ok
         数据库：✅ 已连接
         主测试账号（[TEST_PHONE_MAIN]）：✅ 可登录
         新用户账号（[TEST_PHONE_NEW]）：✅ 可登录
         Python 依赖：✅ 齐全
         
         准备就绪，可以开始测试。」
```

---

## 自动推断逻辑（无 test-config.md 时）

```
推断后端端口：
  grep -r "port.*810[0-9]\|:810[0-9]" [项目目录]/backend/ 2>/dev/null | head -3
  → 默认端口尝试顺序：8100 → 8000 → 3000

推断技术栈：
  IF exists requirements.txt AND exists backend/ → FastAPI/Python
  IF exists package.json AND exists pages/ → Next.js
  IF exists package.json AND exists src/ → React/Node

推断数据库：
  grep DATABASE_URL [项目目录]/.env 2>/dev/null
```

---

## 项目配置文件规范（.cursor/test-config.md）

```markdown
# 测试环境配置

## 服务
BACKEND_PORT: 8100
HEALTH_ENDPOINT: /health
STARTUP_CMD: python -m uvicorn backend.app.main:app --port 8100 --workers 1

## 测试账号
TEST_PHONE_MAIN: 19900000001
TEST_PASSWORD_MAIN: sim2026
TEST_PHONE_NEW: 19900000088
TEST_PASSWORD_NEW: newuser2026

## 技术栈
LANGUAGE: python
TEST_CMD: python -m pytest tests/ -x -v --timeout=120
VENV_PATH: .venv

## P2 处理
P2_HANDLING: skip   # skip / fix_all / ask
```

---

## 日志驱动断言工具（测试必用）

> 产品天然对 AI 透明：`/logs` 端点暴露所有工具调用、节点状态、SSE 事件。
> 用这些做断言，比「检查 AI 回答了什么文字」可靠 10 倍。

```python
import requests

BASE_URL = "http://localhost:[PORT]"

def get_session_logs(headers, session_id=None):
    """获取当前 session 的完整操作日志"""
    r = requests.get(f"{BASE_URL}/logs", headers=headers)
    logs = r.json() if r.status_code == 200 else []
    if session_id:
        logs = [l for l in logs if l.get("session_id") == session_id]
    return logs

def clear_logs(headers):
    """清空日志（每条 Layer 1 测试前调用，防止干扰）"""
    requests.delete(f"{BASE_URL}/logs", headers=headers)

def assert_tool_called(logs, tool_name, args_contains=None):
    """验证某个工具被调用（不管 AI 输出了什么）"""
    for log in logs:
        if log.get("tool") == tool_name or tool_name in str(log.get("action", "")):
            if args_contains is None:
                return True
            if args_contains in str(log.get("args", "")):
                return True
    return False

def assert_workflow_completed(events):
    """验证工作流完整执行完毕"""
    event_types = [e.get("type") for e in events if isinstance(e, dict)]
    return "workflow_done" in event_types

def assert_proposal_created(workspace_path):
    """验证工作流真的创建了 Proposal（文件系统状态断言）"""
    from pathlib import Path
    proposals = list(Path(workspace_path) / "proposals").glob("p-*.json"))
    return len(proposals) > 0

def assert_doc_modified(workspace_path, doc_pattern, after_timestamp):
    """验证某文档在测试操作后被修改"""
    from pathlib import Path
    import os
    for doc in Path(workspace_path).rglob(doc_pattern):
        if os.path.getmtime(doc) > after_timestamp:
            return True
    return False

def count_l2_fragments(workspace_path):
    """统计 L2 碎片数量（认知积累断言）"""
    from pathlib import Path
    return len(list((Path(workspace_path) / "docs/L2").glob("*.md")))
```

### 断言优先级

```
1. 文件系统状态（最可靠）：Proposal 出现了吗？L2 有新文件吗？
2. 日志工具调用：read_skill 被调用了吗？write_file 被调用了吗？
3. SSE 事件序列：workflow_done 在所有 node_update 之后吗？
4. 接口响应结构：返回了正确的字段吗？
5. 文字内容（最后手段）：仅在格式强制要求时（如第三人称检查）
```

---

## 失败处理（人工干预指引）

| 情况 | 自动尝试 | 无法自动处理时的指引 |
|------|---------|------------------|
| 后端服务无法启动 | kill+restart × 3 | 「查看 /tmp/test-backend.log，检查端口冲突或环境变量」 |
| 数据库无法连接 | 尝试 docker-compose up | 「检查 .env 的 DATABASE_URL，确认 PostgreSQL 在运行」 |
| 测试账号无法创建 | 尝试管理员 API 创建 | 输出手动创建 SQL |
| 依赖缺失 | pip install | 「运行 pip install -r requirements.txt」 |

---

## 变更记录

### v1.0 — 2026-03-24 — 初始创建

**根因**：Agent 拥有完整终端权限，但之前测试流程的「第一步」仍是人工确认环境就绪。
需要一个专门的 Skill 把环境准备工作完全自动化，让「说一句话就能开始测试」成为现实。

**设计决策**：
- 5步串行（健康检查→DB→账号→依赖→就绪确认）
- 每步有自动修复逻辑，修复3次失败才报告人工干预
- 支持 .cursor/test-config.md 项目配置，无配置时智能推断
- 输出就绪报告，让调用方知道环境状态

**验证状态**：🔵 待验证

