agentleFS
Ex-imanityEx-imanity/SkillCollections/skills/case-reorganize/SKILL.md

case-reorganize

Skill1 starsChanged 17 days agoMITView on GitHub

将搬山中已有的测试用例整理为链路 case。支持合并冗余用例、去除边界 case、将串联操作合并为一个场景。输出 full.md 后写回搬山(替换原 case 或追加到目标 case)。当用户说"整理搬山用例"、"用例重构"、"链路case"、"合并冗余用例"时触发。

The file

Files
SKILL.md

case-reorganize:搬山测试用例整理为链路 case

定位

针对搬山中已有的测试用例,将分散的、边界偏多的、不成链路的用例重新整理为以用户操作链路为主线的用例结构。

核心原则:

  • 以用户操作链路为主:每个测试点描述一段完整的操作流程,前提→操作→结果
  • 合并串联步骤:将多个顺序执行的原子步骤合并为一条链路用例
  • 去除冗余:合并重复覆盖同一路径的用例;可选去除边界 case(由用户决定)
  • 不改内容:整理结构,不新增未覆盖的场景,不删除用户要求保留的内容

与 case-lite 的区别

case-litecase-reorganize
输入来源飞书文档(需求/技术方案)搬山已有用例
内容来源从文档生成新用例整理/重构已有用例
典型场景新需求上线前生成用例旧用例维护、回归精简
飞书 MCP必需不需要

执行流程

Step 1:收集输入

从用户消息中提取:

  1. 源 case URL(s)(必须)→ 要整理的搬山用例链接,可多个
  2. 目标 case(必须)→ 整理后写回的位置:
    • 替换:写回到源 case,覆盖原内容(先 deleteNode 再写入)
    • 追加:追加到另一个 case(如汇总用的 case 23375),保留原内容
  3. 整理参考 case(可选)→ 格式参考,如 case 23375
  4. 内容策略(可选,默认询问)→ 是否去除边界 case

引导话术:

请提供以下信息:
1. 要整理的搬山用例链接(可多个)
2. 整理后写回的位置:替换原 case,还是追加到其他 case?
3. 是否去除边界 case?(默认询问,或"去除"/"全部保留")
4. 有没有参考格式的 case 链接?(可选)

Step 2:获取源用例内容 [HITL]

对每个源 case,从 URL 中解析 caseId(URL 路径中的数字,如 #/caseManager/91/17210/ 中的 17210)。

调用搬山 MCP 获取用例树:

testCaseDetail(caseId)

搬山 MCP 端点:https://tech.baijia.com/mcp-server/banshan/mcp(JSON-RPC 2.0)

返回的节点树结构(四级串联):

  • 深度 0:场景(resource: ["场景"])
  • 深度 1:测试点(resource: ["测试点", "UI"/"逻辑"])
  • 深度 2:前置条件(resource: ["前置条件"],text = 前置条件内容)
  • 深度 3:执行步骤(resource: ["执行步骤"],text = 所有步骤合并多行,是前置条件的子节点)
  • 深度 4:预期结果(resource: ["预期结果"],text = 所有结果合并多行,是执行步骤的子节点)

注意:执行步骤和预期结果的 text 字段是将所有编号步骤/结果合并为一个多行字符串,不是每条步骤单独一个子节点。

将内容整理为可读摘要展示给用户:

已获取 case XXXXX 内容:

场景1:XXX(N 个测试点)
  - 测试点1.1:XXX
  - 测试点1.2:XXX(可能与 1.1 重复:都在测试 XX)
场景2:XXX(N 个测试点)
  ...

共 N 个场景,N 个测试点。

初步整理意见:
- 测试点 1.2 和 1.3 可合并为一条链路
- 测试点 2.4 为边界 case(验证码格式校验),可考虑去除
- 测试点 3.x 和 3.y 步骤串联,建议合并

请确认整理方向,或说明需要特别保留 / 去除的内容。

Step 3:生成场景结构 [HITL]

基于用户确认的整理方向,生成 structure.md。

整理原则(按优先级):

  1. 合并串联步骤:如「点击按钮 A」→「查看页面 B」→「点击按钮 C」属于同一链路,合并为一个测试点
  2. 合并重复路径:多个测试点覆盖同一主路径时,保留最完整的一个
  3. 拆分无关场景:一个测试点同时覆盖多个不相关功能时,按场景拆分
  4. 去除边界 case(若用户同意):纯参数校验(空值/格式/长度)、平台兼容性边界、低优先级异常分支
  5. 保留核心场景:用户登录/注册、主功能操作链路、已明确要求保留的测试点

命名规范:

  • 场景:XX 链路(如"登录链路"、"启动流程链路")
  • 测试点:主功能 + 分支特征(如"验证码登录链路"、"微信登录未绑定手机号冲突链路")
  • 不使用"验证"、"测试"、"检查"等前缀

生成后展示 structure.md 给用户审核:

场景结构已整理,请审核:
[展示 structure.md 内容]

与原用例对比:
- 原 N 个测试点 → 整理后 N 个测试点
- 去除/合并:XX, XX
- 保留:XX, XX

确认后将生成完整用例。

Step 4:生成完整用例 [HITL]

基于审核通过的 structure.md + 原用例内容生成 full.md。

生成要点:

  • 每个测试点描述完整的操作链路:前置条件 → 执行步骤 → 预期结果
  • 步骤中的"查看..."应拆解为具体的观察对象("查看按钮状态"→"确认按钮变为灰色不可点击状态")
  • 预期结果写具体可观测的状态,不写"正常展示"等模糊描述
  • 不在 full.md 中加优先级标记(P0/P1/P2)

生成后展示 full.md 摘要并请用户审核:

完整用例已生成:
[展示各场景和测试点标题]

共 N 个场景,N 个测试点,N 个节点。
请确认后进行写回。

Step 5:dry-run 验证

必须先执行 dry-run,不可跳过。用以下 Python 片段解析 full.md 并统计节点数:

import re
from pathlib import Path

def dry_run(md_path):
    lines = Path(md_path).read_text(encoding="utf-8").splitlines()
    scenes, points = 0, 0
    cur_has_steps = False
    warnings = []
    point_title = ""
    for line in lines:
        if re.match(r"^## 场景\d+[::]", line):
            scenes += 1
        elif re.match(r"^### 测试点\d+\.\d+[::]", line):
            if points > 0 and not cur_has_steps:
                warnings.append(f"测试点「{point_title}」缺少执行步骤")
            points += 1
            point_title = line.strip()
            cur_has_steps = False
        elif re.match(r"^#### 执行步骤", line):
            cur_has_steps = True
    if points > 0 and not cur_has_steps:
        warnings.append(f"测试点「{point_title}」缺少执行步骤")
    print(f"{scenes} 个场景,{points} 个测试点")
    for w in warnings:
        print(f"⚠ {w}")
    if not warnings:
        print("✓ 无警告")

dry_run("case-lite-output/{slug}/full.md")

检查:场景/测试点数与 full.md 一致,无测试点缺失执行步骤的警告。如有警告,先修复 full.md 再继续。

dry-run 通过后,展示摘要并等待用户明确确认后才能执行写回:

dry-run 通过:N 个场景,N 个测试点,无警告。

写回目标:case XXXXX([替换原内容 / 追加到现有 N 个场景之后])

确认写回请回复「确认」或「写回」,如需修改请继续反馈。

不得在用户确认前自动执行写回。

Step 6:写回搬山 [HITL]

写回使用内联 Python 脚本直接调用搬山 MCP,不依赖 writeback.py。核心解析函数:

import json, re, urllib.request
from pathlib import Path

MCP_ENDPOINT = "https://tech.baijia.com/mcp-server/banshan/mcp"

def mcp_call(method, args):
    payload = {"jsonrpc": "2.0", "id": 1, "method": "tools/call",
               "params": {"name": method, "arguments": args}}
    data = json.dumps(payload, ensure_ascii=False).encode()
    req = urllib.request.Request(MCP_ENDPOINT, data=data,
                                 headers={"Content-Type": "application/json"})
    with urllib.request.urlopen(req, timeout=120) as resp:
        return json.loads(resp.read())

def parse_full_md(md_text, ui_points=None):
    """解析 full.md,输出节点树(前置条件串联在测试点与执行步骤之间)"""
    if ui_points is None:
        ui_points = set()
    lines = md_text.splitlines()
    node_tree, cur_scene, cur_point, cur_section = [], None, None, None
    precond_lines, steps_lines, results_lines = [], [], []
    scene_idx, point_idx = 0, 0

    def flush_point():
        if cur_point is None:
            return
        precond_text = "\n".join(precond_lines).strip()
        steps_text   = "\n".join(steps_lines).strip()
        results_text = "\n".join(results_lines).strip()
        if steps_text:
            steps_node = {"data": {"text": steps_text, "resource": ["执行步骤"], "sourceDesc": "ai"}, "children": []}
            if results_text:
                steps_node["children"].append(
                    {"data": {"text": results_text, "resource": ["预期结果"], "sourceDesc": "ai"}, "children": []})
            if precond_text:
                cur_point["children"].append(
                    {"data": {"text": precond_text, "resource": ["前置条件"], "sourceDesc": "ai"}, "children": [steps_node]})
            else:
                cur_point["children"].append(steps_node)

    for line in lines:
        m = re.match(r"^## 场景\d+[::](.+)", line)
        if m:
            flush_point()
            cur_scene = {"data": {"text": m.group(1).strip(), "resource": ["场景"], "sourceDesc": "ai"}, "children": []}
            node_tree.append(cur_scene)
            scene_idx += 1; point_idx = 0
            cur_point = cur_section = None
            precond_lines = steps_lines = results_lines = []
            continue
        m = re.match(r"^### 测试点\d+\.\d+[::](.+)", line)
        if m and cur_scene is not None:
            flush_point()
            point_idx += 1
            label = "UI" if (scene_idx, point_idx) in ui_points else "逻辑"
            cur_point = {"data": {"text": m.group(1).strip(), "resource": ["测试点", label], "sourceDesc": "ai"}, "children": []}
            cur_scene["children"].append(cur_point)
            cur_section = None
            precond_lines = steps_lines = results_lines = []
            continue
        if re.match(r"^#### 前置条件", line): cur_section = "precond"; continue
        if re.match(r"^#### 执行步骤", line): cur_section = "steps"; continue
        if re.match(r"^#### 预期结果", line): cur_section = "results"; continue
        stripped = line.strip()
        if not stripped or stripped == "---": continue
        if cur_section == "precond": precond_lines.append(stripped)
        elif cur_section == "steps": steps_lines.append(stripped)
        elif cur_section == "results": results_lines.append(stripped)

    flush_point()
    return node_tree

根据写回模式执行不同操作:

模式 A:替换原 case

# 1. 获取原 case 所有场景节点 ID(注意是 s["data"]["id"],不是 s["id"])
r = mcp_call("testCaseDetail", {"caseId": str(CASE_ID)})
root = json.loads(r["result"]["content"][0]["text"])["data"]["caseContent"]
if isinstance(root, str): root = json.loads(root)
scene_ids = [(s["data"]["id"], s["data"]["text"]) for s in root["root"]["children"]]

# 2. 依次删除(deleteNode 会级联删除子节点)
for node_id, title in scene_ids:
    mcp_call("deleteNode", {"caseId": CASE_ID, "nodeId": node_id, "modifier": "case-lite"})

# 3. 解析并写回
node_tree = parse_full_md(Path("full.md").read_text(encoding="utf-8"), ui_points=UI_POINTS)
mcp_call("batchAddNode", {"caseId": CASE_ID, "modifier": "case-lite", "nodeTreeList": node_tree})

注意:deleteNode 操作不可逆,执行前务必确认。

模式 B:追加到目标 case

不需要删除节点,也不需要调整场景编号,直接追加:

# 1. 查询目标 case 当前场景数(仅用于日志展示)
r = mcp_call("testCaseDetail", {"caseId": str(TARGET_CASE_ID)})
root = json.loads(r["result"]["content"][0]["text"])["data"]["caseContent"]
if isinstance(root, str): root = json.loads(root)
current_count = len(root["root"]["children"])
print(f"当前已有 {current_count} 个场景")

# 2. 解析并追加(full.md 场景编号从1开始即可,不需要偏移)
node_tree = parse_full_md(Path("full.md").read_text(encoding="utf-8"), ui_points=UI_POINTS)
mcp_call("batchAddNode", {"caseId": TARGET_CASE_ID, "modifier": "case-lite", "nodeTreeList": node_tree})

Step 7:打 UI / 逻辑 标签(可选)[HITL]

写回时已在 parse_full_md 的 ui_points 参数中注入标签,写回完成即已打标,无需额外步骤。

写回前需整理出 UI_POINTS 集合:

  • 格式:{(场景序号, 场景内测试点序号), ...},1-based,相对于 full.md 文件内位置
  • 依据:展示类(按钮状态、页面渲染、动效)→ "UI";交互逻辑、接口调用、状态流转 → "逻辑"

如需重新打标(已写回后才发现标签有误),使用模式 A 全量重建。


产物目录

case-lite-output/{slug}/
├── structure.md        ← 场景结构(用户审核)
├── full.md             ← 完整用例(用户审核)
└── writeback.py        ← 写回脚本(含 parse_full_md + mcp_call)

slug 由 case 名称或功能名生成,如 app-common-flow。


full.md 格式规范

与 case-lite 完全一致,核心约束:

规则正确错误
场景标题## 场景1:标题## 场景一:标题
测试点标题### 测试点1.1:标题### 1.1 标题
前置条件标题#### 前置条件写入步骤中 / 省略
步骤标题#### 执行步骤### 执行步骤
结果标题#### 预期结果**预期结果**
步骤格式1. 操作内容- 操作内容

前置条件会被解析为独立节点,串联在测试点与执行步骤之间,不可省略。


工具依赖

工具用途必需
Banshan MCP获取原用例内容、写回节点是
飞书 MCP不需要否

写回逻辑通过 Step 6 中的内联 Python 脚本直接调用搬山 MCP,不依赖 writeback.py。


常见决策参考

哪些测试点适合合并?

  • 步骤有明确前后依赖关系(步骤 A 的结果是步骤 B 的前提)
  • 同一功能的正向路径拆成了多个测试点
  • 同一弹窗/页面的多个按钮分开测试(可合并为"弹窗交互链路")

哪些测试点可以去除(边界 case)?

  • 输入框格式校验(手机号格式、密码长度)
  • 网络异常/超时等低频场景(除非该功能有专门的降级逻辑)
  • UI 细节检查(字体大小、颜色、间距)
  • 已被其他测试点覆盖的重复路径

哪些测试点必须保留?

  • 用户核心操作链路(登录、支付、主功能入口)
  • 有明确分叉的流程(如"已绑定"vs"未绑定"、"首次"vs"非首次")
  • 用户明确说要保留的内容

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.