---
date: 2026-05-15
tags: [testing, git, bash, agent-pool, patch-id]
status: documented
---

# 测试 squash merge 等价检测：用 `commit-tree` + `update-ref` 在本地伪造 squash commit

## 场景

agent-pool 的 `ap_branch_absorbed` 用 `git diff <merge-base> HEAD | patch-id --stable` 检测 squash zombie。要写回归测试，但**不能真的 push 到 origin/develop**（CI 拒绝；也会污染真实数据）。

PR [#376](http://43.130.59.228/FFAIWorkspace/workspace/pulls/376) 的 AI review 要求补这条覆盖，否则 L2 累积 patch-id 路径只能靠人工 dry-run 验证，后续重构无法防回归。

## 不直观的点

`refs/remotes/origin/develop` 是**本地的远端跟踪 ref**，跟真实 origin 解耦——本地 `update-ref refs/remotes/origin/develop <sha>` 可以原地改它的指向，不需要任何网络交互。

利用这点可以**在测试里伪造一个 squash merge 后的 develop 状态**，验证检测逻辑：

```bash
# 1. 备份当前 origin/develop SHA（cleanup 时恢复）
T16_ORIG_DEVELOP_SHA=$(git rev-parse refs/remotes/origin/develop)

# 2. 在 slot 里造 N 个 commit（模拟 feature 分支累积变更）
for i in 1 2 3; do
  echo "line ${i}" >> file.txt
  git commit -am "feat: commit ${i}"
done

# 3. 关键：用 commit-tree 直接造一个 squash 等价 commit
#    parent = 原 develop，tree = slot HEAD 的累积 tree
SLOT_HEAD_TREE=$(git rev-parse HEAD^{tree})
SQUASH_SHA=$(git commit-tree "${SLOT_HEAD_TREE}" \
  -p "${T16_ORIG_DEVELOP_SHA}" \
  -m "feat: squashed feature")

# 4. 把 origin/develop 指向这个伪造的 squash commit
git update-ref refs/remotes/origin/develop "${SQUASH_SHA}"

# 5. 跑被测代码，验证它能识别 squash zombie
bash agent-sweep.sh

# 6. 恢复 origin/develop（panic 路径走 cleanup trap）
git update-ref refs/remotes/origin/develop "${T16_ORIG_DEVELOP_SHA}"
```

## 为什么 patch-id 会匹配

- slot HEAD 的累积 diff = `git diff T16_ORIG_DEVELOP_SHA HEAD` = "从原 develop tree 到 slot HEAD tree 的变化"
- 伪造的 squash commit 的 diff = `git diff-tree -p SQUASH_SHA` = "从其 parent (T16_ORIG_DEVELOP_SHA) 到其 tree (= slot HEAD tree) 的变化"
- 两者是**完全相同的 patch**（同一对 tree 之间的差异）
- `patch-id --stable` 对相同 patch 输出相同哈希 ✓

`patch-id --stable` 忽略 commit message / author / date / 行号偏移，只算 patch 的"语义指纹"。所以伪造 commit 用什么 message 都不影响。

## 兜底防御

`commit-tree` 或 `rev-parse` 失败时会让变量是空串，后续 `update-ref refs/remotes/origin/develop ""` 会**静默删除** ref：

```bash
T16_ORIG_DEVELOP_SHA=$(git rev-parse refs/remotes/origin/develop 2>/dev/null || echo '')
if [[ -z "${T16_ORIG_DEVELOP_SHA}" ]]; then
  fail "T16: refs/remotes/origin/develop 不存在"
fi
```

cleanup trap 里也要兜底（panic 路径不一定走到显式恢复）：

```bash
if [[ -n "${T16_ORIG_DEVELOP_SHA:-}" ]]; then
  git update-ref refs/remotes/origin/develop "${T16_ORIG_DEVELOP_SHA}" 2>/dev/null || true
fi
```

## 适用范围

任何需要测试"squash / rebase 后 ref 状态"的回归测试都适用：
- patch-id 等价检测（本次）
- `sweep-remote-stale.py` 的类似逻辑（Python 侧也可用 GitPython 走同样路径）
- 任何依赖 "feature 内容已合进 base，但 SHA 不同" 状态的代码

**关键约束**：被测代码不能区分"真实 push 来的 commit"和"`commit-tree` 造的 commit"——只要 SHA 链 + tree 正确，git 看着完全一样。

## 关联

- PR [#376](http://43.130.59.228/FFAIWorkspace/workspace/pulls/376)
- 实现：`scripts/dev/lib/agent-pool-lib.sh` `ap_branch_absorbed`
- 测试：`scripts/dev/agent-pool/tests/run-tests.sh` T16
- 同口径 Python 实现：`scripts/ops/sweep-remote-stale.py`
