# TiDB 测试运行时改造策略设计

> Status: draft for review
> Scope: engineering / test-infra only
> Related code: `internal/coredb/testkit/*`, `cmd/agentteam-tidb-admin/*_test.go`, `internal/api/agent/*_test.go`, `internal/coredb/testkit/tidb_fixture.go`

## 背景

当前仓库里的 TiDB 集成测试默认通过 `testkit.StartInitializedTiDB(t)` 走一条隐式路径：

- 如果设置了 `AGENTTEAM_TIDB_TEST_DSN`，则在该 root DSN 上创建隔离 database
- 如果没有设置，则复用 `internal/coredb/testkit/tidb_runtime.go` 自举的共享 `TiDB + PD + TiKV` playground runtime
- schema fixture 通过 migrated template clone 到每个测试 database

这条默认路径保证了“行为尽量接近完整 TiDB 集群”，但也让**并不依赖 full-cluster 能力的包**持续支付高额测试成本。

前一个已验证的例子是 `cmd/agentteam-tidb-admin`：

- 最初这组测试每个 case 都走一次重型 initialized fixture，曾在整包约 `189s` 时被外层发 `SIGTERM`
- 后续已经在 PR #337 中验证过：把 runtime 显式拆成 `full-cluster` / `unistore` 两类后，只有 `finalize` 继续走 full cluster，其他命令测试走 unistore，成本显著下降

现在的新目标是：**把这种“按能力选择 runtime”的测试改造方式整理成一个可复用 spec，而不是继续按包临时手改。**

## 当前证据

### 1. `internal/api/agent` 当前没有 full-cluster 显式依赖

基于最新 `origin/main` 代码静态检查，`internal/api/agent` 目录下没有出现以下 full-cluster 敏感信号：

- `TiFlash`
- fulltext / `tidb_enable_fulltext_index`
- `information_schema.cluster_info`
- 需要显式外部 `AGENTTEAM_TIDB_TEST_DSN` 才能跑的 live-runtime 校验

当前 integration test 入口集中在 `startAgentAPITestPool()`：

- `internal/api/agent/handlers_integration_test.go`
- `internal/api/agent/handlers_triggers_integration_test.go`
- `internal/api/agent/review_context_integration_test.go`

其中 `handlers_integration_test.go` 内已有 9 个调用点，另外两个 integration test 文件各 1 个调用点，默认都直接走：

```go
func startAgentAPITestPool(t *testing.T, ctx context.Context) (string, coredb.DB) {
    t.Helper()

    _ = ctx
    databaseURL := testkit.StartInitializedTiDB(t)
    return databaseURL, coredb.WrapSQLDB(testkit.OpenTiDB(t, databaseURL))
}
```

### 2. 最新 `origin/main` 上的实测耗时

在 `2026-04-30` 基于最新 `origin/main` 的 fresh worktree 中，针对 `internal/api/agent` 做了下面几组实测：

#### 默认 shared full-cluster 路径

```bash
/usr/bin/time -p go test -count=1 -run '^TestInternalAgentAPITasksCompatListCreateClaimUnclaim$' ./internal/api/agent
# ok   github.com/tidbcloud/AgentTeam/internal/api/agent  37.166s
# real 40.43

/usr/bin/time -p go test -count=1 ./internal/api/agent
# ok   github.com/tidbcloud/AgentTeam/internal/api/agent  128.451s
# real 129.06
```

#### 显式 unistore root DSN

```bash
/usr/bin/time -p env AGENTTEAM_TIDB_TEST_DSN='<unistore root dsn>' \
  go test -count=1 -run '^TestInternalAgentAPITasksCompatListCreateClaimUnclaim$' ./internal/api/agent
# ok   github.com/tidbcloud/AgentTeam/internal/api/agent  2.675s
# real 3.19

/usr/bin/time -p env AGENTTEAM_TIDB_TEST_DSN='<unistore root dsn>' \
  go test -count=1 ./internal/api/agent
# ok   github.com/tidbcloud/AgentTeam/internal/api/agent  7.029s
# real 7.59
```

这说明对于 `internal/api/agent` 这类不依赖 full-cluster 特性的包，仅把 root runtime 切到 unistore，就已经能把整包从 **129s 级** 收敛到 **8s 级**。

## 问题定义

我们真正需要解决的不是“单个包怎么偷偷跑快”，而是下面三个更一般化的问题：

1. **runtime 选择当前是隐式的**：默认 full cluster，例外只能靠 `AGENTTEAM_TIDB_TEST_DSN` 或包内私有 helper 绕过去。
2. **不同测试包的隔离模型并不相同**：有的包可以复用单库 + reset，有的包必须每测独立 DB，有的包适合事务回滚隔离。
3. **缺少统一的能力分类**：什么场景必须保留 full cluster，什么场景可以安全降到 unistore，现在主要靠人工记忆。

如果继续按包 ad-hoc 改，会重复出现：

- package 私有 env hack
- runtime 选择语义不统一
- 后续新增 full-cluster 需求时很难发现回归

## 目标

1. 让 TiDB test runtime 选择**显式化、可复用、可审查**。
2. 在不丢失 full-cluster 覆盖的前提下，把明显不需要该能力的包迁到 unistore。
3. 为不同类型的测试包定义清晰的 fixture 模式，避免“把一种成功经验生搬硬套到所有包”。
4. 先服务 `internal/api/agent` 这类收益明显的包，但设计本身要能复用到其他慢包。

## 非目标

1. 不在这一个 spec 里把所有 TiDB integration package 全部改完。
2. 不直接改变产品行为或运行时语义；本次只讨论测试基础设施与测试组织方式。
3. 不要求所有包都切成 unistore；full-cluster 测试仍然是必要能力。
4. 不在第一步里重写现有所有 `StartInitializedTiDB` 调用点。

## 备选方案

### 方案 A：全局默认从 full cluster 改成 unistore

**做法**
- 直接把 `testkit.StartInitializedTiDB` 的默认 root runtime 从 shared TiUP playground 改成 unistore

**优点**
- 一次改动，立刻让大量测试提速

**缺点**
- 会静默降低 full-cluster 覆盖面
- fulltext / TiFlash / PD/TiKV 依赖的 case 会变成“晚失败”或“假通过”
- 破坏现有默认语义，回归面过大

**结论**
- 不推荐。收益大，但风险不可控。

### 方案 B：每个包各自用 `TestMain` / env 私下切到 unistore

**做法**
- 类似现在 `cmd/agentteam-tidb-admin` 里的私有 helper，包内自己起 unistore runtime，再通过 `AGENTTEAM_TIDB_TEST_DSN` 或局部 helper 接入

**优点**
- 迁移单个包很快
- 不需要先设计公共 API

**缺点**
- runtime 选择逻辑分散，重复代码多
- 容易继续依赖 env mutation 这种隐式通道
- 随着包数增长，很快会出现多套几乎相同但细节不同的 test fixture

**结论**
- 可作为短期过渡，但不应成为长期策略。

### 方案 C：在 `internal/coredb/testkit` 中引入第一类“双 runtime + 多 fixture 模式”策略（推荐）

**做法**
- 把 runtime 选择抽成公共概念
- 保留 full-cluster 与 unistore 两类 root runtime
- 再按测试包类型选择合适的 fixture 复用方式

**优点**
- runtime 选择显式、可审查
- 能同时覆盖 mixed-runtime package 与 unistore-only package
- 便于后续按能力清单持续迁移慢包

**缺点**
- 第一轮需要额外整理公共 helper
- 需要明确 package archetype，不能简单“一刀切”

**结论**
- 推荐采用。

## 推荐设计

## 1. 把 runtime 选择提升为公共概念

建议在 `internal/coredb/testkit` 暴露显式 runtime 概念，例如：

```go
type TiDBTestRuntime string

const (
    TiDBTestRuntimeFullCluster TiDBTestRuntime = "full-cluster"
    TiDBTestRuntimeUnistore    TiDBTestRuntime = "unistore"
)
```

围绕它补齐两类 helper：

1. **root runtime helper**
   - 负责拿到某种 runtime 的 root DSN，并带 cleanup
   - 例如：`StartTiDBRuntimeWithCleanup(ctx, runtime)`

2. **initialized database helper**
   - 负责在指定 root runtime 上生成 migrated test database
   - 例如：`StartInitializedTiDBWithRootDSN(ctx, rootDSN)`
   - 或更直接：`StartInitializedTiDBWithRuntime(ctx, runtime)`

关键点是：**不要继续把“换 runtime”主要建立在 `os.Setenv("AGENTTEAM_TIDB_TEST_DSN", ...)` 这种隐式全局副作用上。**

`AGENTTEAM_TIDB_TEST_DSN` 仍然可以保留给外部显式覆盖，但包内 fixture 不应长期依赖它做核心控制流。

## 2. 明确三种 package archetype

同样是 TiDB 测试，不同包对“共享什么”这件事的约束差异非常大。建议把可复用模式明确成三类。

### Archetype A：串行命令包 —— 共享单 initialized DB + reset

**适用条件**
- 包内测试不是大规模 `t.Parallel()`
- 测试写入的数据集合可控，且 reset 成本低
- 典型代表：`cmd/agentteam-tidb-admin`

**策略**
- 按 runtime 建 package-level fixture
- 每轮测试前手工 reset 相关表/状态
- full-cluster 与 unistore 可以在同一包里并存，但由测试显式选择

**优点**
- 速度最快
- 对命令类测试非常有效

**注意点**
- 不能盲目复制到并发 integration package

### Archetype B：并行 request-path 包 —— 共享 root runtime，但每测独立 DB

**适用条件**
- 包内大量 `t.Parallel()`
- 测试体内使用固定 ID / 固定 seed 数据
- 共享一套业务数据库会互相污染
- 典型代表：`internal/api/agent`

**策略**
- 共享 package-level root runtime（例如 unistore root DSN）
- 每个测试仍然调用“initialized clone”路径拿到**自己的** database
- 只把 runtime 启动成本和 migrated template 准备成本共享出去，不共享可变业务状态

**为什么 `internal/api/agent` 适合这一类**
- 该包已有大量 `t.Parallel()`
- 多个测试复用了固定 workspace / member / agent ID
- 直接共享同一个 initialized DB，即使每次 reset，也很容易在并发下冲突

### Archetype C：事务友好型 DB/service 包 —— 共享 package fixture + rollback/savepoint

**适用条件**
- 可以把测试操作包裹在单个事务或 savepoint 模型里
- 典型代表：`internal/coredb/testkit/tidb_fixture.go`

**策略**
- package-level initialized DB
- 每个测试用 rollback transaction / savepoint 做隔离

**注意点**
- 只适用于依赖注入边界足够清晰、能够把请求路径绑定在同一个 DB handle 上的测试
- 对完整 HTTP handler/request-path 测试通常不现实

## 3. 为 runtime 选择建立能力规则

建议先把“何时必须 full cluster”明确成工程规则，而不是靠经验判断。

### 必须保留 full-cluster 的情形

- 依赖 fulltext / TiFlash 能力
- 依赖 PD/TiKV/TiFlash 相关状态或系统表
- 依赖 `information_schema.cluster_info` 等多组件 runtime 视角
- 需要验证只会在完整集群暴露的运行时行为

### 可以优先迁到 unistore 的情形

- 仅需要 migrated schema
- 仅覆盖普通 CRUD / store / handler / task / thread / membership 等业务逻辑
- 不依赖 full-cluster 专属系统能力
- 当前实测已证明显式 unistore 下可稳定通过

## 4. `internal/api/agent` 应作为下一批 pilot package

推荐将 `internal/api/agent` 作为这套 spec 的首个 request-path pilot，原因有三点：

1. **收益非常大**：当前 fresh 包级实测 `129.06s -> 7.59s`
2. **能力边界清晰**：目前未见 full-cluster 敏感依赖
3. **复用价值高**：它代表的是“大量并行 handler integration test”这一类常见包，而不是特例

建议的落地方式不是“像命令包一样共用一个数据库”，而是：

- package-level 启动一个 unistore root runtime
- `startAgentAPITestPool()` 改成通过 runtime-aware helper 为**每个测试**创建 initialized DB
- 保留 future escape hatch：如果后续 `internal/api/agent` 出现 full-cluster 敏感 case，可以只让那部分测试显式切回 full cluster

## 5. 渐进式迁移顺序

### Phase 1：抽公共 helper

目标：把 `cmd/agentteam-tidb-admin` 里已经验证过的 runtime 选择思路，沉淀为 `internal/coredb/testkit` 公共 API。

至少应覆盖：

- runtime enum
- runtime -> root DSN helper
- root DSN -> initialized DB helper
- package-level cleanup 约定

### Phase 2：迁移 `internal/api/agent`

目标：用 Archetype B 模式把 `internal/api/agent` 切到 unistore-first。

验收重点：

- 当前 integration test 全部继续通过
- 不移除 full-cluster escape hatch
- 不引入跨测试污染
- fresh 包级 wall-clock 明显下降

### Phase 3：审计其他慢包并分类

优先检查仍大量使用 `StartInitializedTiDB(t)` 的包，并按 archetype 分类，而不是一次性批量替换。

可优先观察：

- 是否有 full-cluster 能力依赖
- 是否大量 `t.Parallel()`
- 是否可事务隔离
- 是否适合单库 reset

## 风险与缓解

### 风险 1：把 full-cluster 敏感测试错误地下放到 unistore

**缓解**
- 迁移前先做 capability audit
- 要求 runtime 选择显式出现在 helper 名字或 test setup 中
- mixed-runtime package 必须保留显式 full-cluster 分支

### 风险 2：继续依赖 env mutation，导致并发包之间相互干扰

**缓解**
- 公共 helper 以参数传递 runtime / root DSN 为主
- `AGENTTEAM_TIDB_TEST_DSN` 只保留为外部 override 入口，不作为包内长期主控制流

### 风险 3：错误复用单 DB，导致并行测试串扰

**缓解**
- 在 spec 层明确：Archetype A 只适用于串行/可 reset 的命令包
- 对 `t.Parallel()` 的 request-path package 默认使用 Archetype B

### 风险 4：package-level unistore runtime 清理不彻底

**缓解**
- 统一通过 `TestMain` / package fixture cleanup 收口
- 复用 `internal/coredb/testkit/tidb_unistore.go` 的 cleanup 语义，不在每个包自己写进程清理逻辑

## 建议的 acceptance criteria

当这套策略开始落实现实代码时，至少应满足：

1. `internal/api/agent` 在 unistore 路径下默认全包通过
2. 任何需要 full cluster 的测试都能通过显式 API 选择 full-cluster runtime
3. 不再新增包级私有 `AGENTTEAM_TIDB_TEST_DSN` env hack 作为主要 runtime 分流手段
4. fixture 复用方式与包 archetype 一一对应，代码审查时容易看出“为什么这个包可以/不可以共享一个 DB”

## 结论

推荐把当前已经在 `cmd/agentteam-tidb-admin` 验证过的经验，上升为一套**公共的、runtime-aware 的 TiDB 测试策略**：

- runtime 维度：`full-cluster` 与 `unistore` 显式并存
- fixture 维度：按 package archetype 选择“单库 reset / 每测独立 DB / rollback transaction”
- rollout 维度：先从 `internal/api/agent` 这种收益高、能力边界清晰的包开始

这比“把所有测试都切到 unistore”更安全，也比“每个包自己想办法偷切 runtime”更可维护。
