|
| 1 | +# AI 模块单元测试 TODO LIST |
| 2 | + |
| 3 | +> 版本:v7(三层校验重构统一版:Parser / Loader+Schema / Runtime / Component) |
| 4 | +
|
| 5 | +## 快速开始 |
| 6 | + |
| 7 | +```bash |
| 8 | +go test -race -v ./config/... ./runtime/... ./component/... ./test/... ./cmd/... |
| 9 | +``` |
| 10 | + |
| 11 | +--- |
| 12 | + |
| 13 | +## Tier 1:Parser(语法层,4个) |
| 14 | + |
| 15 | +### Config Parser |
| 16 | + |
| 17 | +**文件**: `config/loader_test.go` |
| 18 | + |
| 19 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 20 | +|---|---------|------|------|------|---------| |
| 21 | +| 1 | `TestLoader_MainConfig_ParseError` | 无 | 主配置 YAML 语法错误 | 非法 `config.yaml`(如缺失冒号) | 返回 `parse error`,不进入结构校验 | |
| 22 | +| 2 | `TestLoader_Component_ParseError` | 无 | 组件配置 YAML 语法错误 | 非法组件 YAML | 返回 `parse error`,不进入结构校验 | |
| 23 | +| 3 | `TestLoader_MainConfig_ParseError_Priority` | 无 | 语法错误优先级 | 同时包含结构问题与语法错误的主配置 | 优先返回 `parse error` | |
| 24 | +| 4 | `TestLoader_Component_ParseError_Priority` | 无 | 语法错误优先级 | 同时包含结构问题与语法错误的组件配置 | 优先返回 `parse error` | |
| 25 | + |
| 26 | +--- |
| 27 | + |
| 28 | +## Tier 2:Loader + Schema(结构层,12个) |
| 29 | + |
| 30 | +### Main Config Structural |
| 31 | + |
| 32 | +**文件**: `config/loader_test.go` |
| 33 | + |
| 34 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 35 | +|---|---------|------|------|------|---------| |
| 36 | +| 5 | `TestLoader_MainConfig_UnknownField` | 无 | 主配置 unknown field 拒绝 | `config.yaml` 含未定义字段 | 返回 `structural error` | |
| 37 | +| 6 | `TestLoader_MainConfig_ComponentsTypeInvalid` | 无 | components 项类型约束 | `components.x` 为对象/数字 | 返回 `structural error`(仅允许 string/[]string) | |
| 38 | +| 7 | `TestLoader_MainConfig_ComponentsArrayItemInvalid` | 无 | components 数组项类型约束 | `components.x` 为 `["a.yaml", 1]` | 返回 `structural error` | |
| 39 | +| 8 | `TestLoader_MainConfig_DefaultSchemaDir` | 无 | SCHEMA_DIR 默认路径 | 环境变量未设置 | 默认使用 `schema/json` 成功加载 | |
| 40 | + |
| 41 | +### Component Structural |
| 42 | + |
| 43 | +**文件**: `config/loader_test.go` |
| 44 | + |
| 45 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 46 | +|---|---------|------|------|------|---------| |
| 47 | +| 9 | `TestLoader_Component_MissingType` | 无 | 组件 type 必填 | 组件 YAML 缺失 `type` | 返回 `structural error` | |
| 48 | +| 10 | `TestLoader_Component_MissingSpec` | 无 | 组件 spec 必填 | 组件 YAML 缺失 `spec` | 返回 `structural error` | |
| 49 | +| 11 | `TestLoader_Component_UnknownTopField` | 无 | top-level unknown field 拒绝 | 组件 YAML 含未定义字段 | 返回 `structural error` | |
| 50 | +| 12 | `TestLoader_Component_DefaultInjection_Server` | 无 | server 默认值注入 | 仅给最小 server 配置 | decode 后包含 port/host/timeout 默认值 | |
| 51 | +| 13 | `TestLoader_Component_DefaultInjection_Agent` | 无 | agent 阶段默认值注入 | stage 省略 temperature/top_p 等 | decode 后包含默认值 | |
| 52 | + |
| 53 | +### Conditional Required |
| 54 | + |
| 55 | +**文件**: `config/loader_test.go` |
| 56 | + |
| 57 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 58 | +|---|---------|------|------|------|---------| |
| 59 | +| 14 | `TestLoader_Tools_MCPEnabled_RequireHost` | 无 | 条件必填(tools) | `enable_mcp_tools=true` 且缺 `mcp_host_name` | 返回 `structural error` | |
| 60 | +| 15 | `TestLoader_RAG_RerankerEnabled_RequireAPIKey` | 无 | 条件必填(rag) | `reranker.enabled=true` 且缺 `api_key` | 返回 `structural error` | |
| 61 | +| 16 | `TestLoader_RAG_Splitter_OneOfBranchValidation` | 无 | oneOf 分支结构约束 | splitter spec 与 type 不匹配 | 返回 `structural error` | |
| 62 | + |
| 63 | +--- |
| 64 | + |
| 65 | +## Tier 3:Runtime 调度层(6个) |
| 66 | + |
| 67 | +### Runtime Orchestration |
| 68 | + |
| 69 | +**文件**: `runtime/runtime_test.go` |
| 70 | + |
| 71 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 72 | +|---|---------|------|------|------|---------| |
| 73 | +| 17 | `TestRuntime_RegisterFactory_Duplicate` | 无 | 重复注册覆盖 | 同类型注册两次 | 第二次覆盖生效;包含重复注册提示 | |
| 74 | +| 18 | `TestRuntime_RegisterFactory_Concurrent` | 无 | 并发注册安全 | 100 goroutine 并发注册 | 无 data race;数量正确 | |
| 75 | +| 19 | `TestRuntime_GetFactoryFn_NotFound` | 无 | 未注册工厂 | 类型名 `test` | 返回 error,包含 `not registered` | |
| 76 | +| 20 | `TestRuntime_GetComponent_NotFound` | 无 | 未注册组件 | 名称 `agent` | 返回 error,包含 `component not found` | |
| 77 | +| 21 | `TestRuntime_ComponentInitOrder` | Stub Component | 初始化顺序 | 注册多个工厂 | 按 `factoryOrder` 顺序 `Validate -> Init` | |
| 78 | +| 22 | `TestBootstrap_ValidateFailStopsInit` | Stub Component | 语义失败中断 | 某组件 `Validate()` 返回 error | `Bootstrap` 返回 `failed to validate <name>`,不执行 Init | |
| 79 | + |
| 80 | +--- |
| 81 | + |
| 82 | +## Tier 4:Component 语义层(8个) |
| 83 | + |
| 84 | +### Component Validate |
| 85 | + |
| 86 | +**文件**: `component/*/test/*_test.go` |
| 87 | + |
| 88 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 89 | +|---|---------|------|------|------|---------| |
| 90 | +| 23 | `TestServerComponent_Validate_PortRange` | 无 | server 端口语义 | `port=70000` | `Validate()` 返回 error | |
| 91 | +| 24 | `TestServerComponent_Validate_TimeoutPositive` | 无 | server 超时语义 | `read_timeout<=0` 或 `write_timeout<=0` | `Validate()` 返回 error | |
| 92 | +| 25 | `TestMemoryComponent_Validate_MaxTurns` | 无 | memory 轮次语义 | `max_turns<=0` | `Validate()` 返回 error | |
| 93 | +| 26 | `TestToolsComponent_Validate_MCPConfig` | 无 | tools MCP 语义 | MCP enabled 且 host 为空 | `Validate()` 返回 error | |
| 94 | +| 27 | `TestModelsComponent_Validate_Providers` | 无 | models 语义一致性 | providers 为空或 base_url 为空 | `Validate()` 返回 error | |
| 95 | +| 28 | `TestRAGComponent_Validate_SplitterSemantic` | 无 | rag 分块语义 | `overlap_size >= chunk_size` | `Validate()` 返回 error | |
| 96 | +| 29 | `TestAgentComponent_Validate_StageFlowType` | 无 | agent 阶段语义 | 非法 `flow_type` | `Validate()` 返回 error | |
| 97 | +| 30 | `TestAgentComponent_Validate_StagePromptRequired` | 无 | agent 阶段语义 | 缺 `prompt_file` | `Validate()` 返回 error | |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +## Tier 5:Business Workflows(业务流程保留项,8个) |
| 102 | + |
| 103 | +### RAG Workflow |
| 104 | + |
| 105 | +**文件**: `component/rag/test/workflow_test.go` |
| 106 | + |
| 107 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 108 | +|---|---------|------|------|------|---------| |
| 109 | +| 31 | `TestRAGWorkflow_Index_Retrieve` | Mock Retriever/Indexer | 索引后可检索 | 文档 `Dubbo is RPC`,查询 `RPC` | 检索结果包含 `Dubbo` | |
| 110 | +| 32 | `TestRAGWorkflow_Split_Index` | Mock Splitter/Indexer | 分块后索引 | 长文档 | `len(chunks)>1` 且全部进入索引 | |
| 111 | +| 33 | `TestRAGWorkflow_Namespace` | Mock Retriever | 命名空间隔离 | ns1/ns2 各索引文档 | ns1 查询不返回 ns2 内容 | |
| 112 | +| 34 | `TestRAG_Retrieve_EmptyQuery` | Mock Retriever | 空查询处理 | `queries=nil` | 返回空 map(非nil),无 error | |
| 113 | + |
| 114 | +### Agent Workflow |
| 115 | + |
| 116 | +**文件**: `component/agent/react/test/flow_test.go` |
| 117 | + |
| 118 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 119 | +|---|---------|------|------|------|---------| |
| 120 | +| 35 | `TestActFlow_GeneralInquiry_NoTools` | Mock Prompt | 一般询问不调工具 | `Intent=GeneralInquiry` | 返回 `ToolOutputs`,`len(Outputs)=0` | |
| 121 | +| 36 | `TestActFlow_WithToolCall_ReturnsToolOutputs` | Mock Prompt+Tool | 工具调用主流程 | `Intent=PerformanceInvestigation` | 返回 `ToolOutputs`,至少1条结果 | |
| 122 | +| 37 | `TestActFlow_ToolErrorHandling` | Mock Prompt/工具 | 工具错误处理 | 工具返回 error | 返回 error,包含工具名 | |
| 123 | +| 38 | `TestThinkFlow_ExecuteError_NoNilDeref` | Mock Prompt | think 异常路径健壮性 | `Execute` 返回 error | 不应因 `resp.Text()` 引发 nil deref | |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +## Tier 6:并发与边界(保留项,8个) |
| 128 | + |
| 129 | +### Memory Concurrency & State |
| 130 | + |
| 131 | +**文件**: `component/memory/test/history_test.go` |
| 132 | + |
| 133 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 134 | +|---|---------|------|------|------|---------| |
| 135 | +| 39 | `TestHistoryMemory_AddHistory_UserMessage` | 无 | 添加用户消息 | session-1 + user message | 进入当前 turn 的 `UserMessages` | |
| 136 | +| 40 | `TestHistoryMemory_NextTurn_ArchivesCurrentTurn` | 无 | 推进会话归档当前 turn | 已有1个turn | 旧 turn 进入 history,window 前移 | |
| 137 | +| 41 | `TestHistoryMemory_NextTurn_WhenSessionFull` | 无 | 会话窗口满时行为 | 将窗口填满后 `NextTurn` | 返回 error,包含 `context is full` | |
| 138 | +| 42 | `TestHistoryMemory_ConcurrentAddHistory` | 无 | 并发写历史安全 | 100 goroutine 写入 | 无 panic,无 race | |
| 139 | +| 43 | `TestHistoryMemory_ConcurrentReadWrite` | 无 | 并发读写安全 | 10写+10读 goroutine | 不 panic,无 data race | |
| 140 | +| 44 | `TestHistoryMemory_NextTurn_EmptyWindowSafety` | 无 | 空窗口推进安全 | session 被 pop 空后重复 `NextTurn` | 固定当前 panic 风险或修复后断言 error | |
| 141 | + |
| 142 | +### Runtime/Bootstrap Boundary |
| 143 | + |
| 144 | +**文件**: `runtime/runtime_test.go`, `component/*/test/*_test.go` |
| 145 | + |
| 146 | +| # | 测试名称 | Mock | 说明 | 输入 | 预期输出 | |
| 147 | +|---|---------|------|------|------|---------| |
| 148 | +| 45 | `TestBootstrap_MissingFactoryForConfiguredType` | 无 | 配置类型无工厂 | 配置 type 未注册 | error 包含 `no factory for` | |
| 149 | +| 46 | `TestRuntime_GetRuntime_NotInitialized` | 无 | 全局Runtime未初始化 | 直接调用 `GetRuntime()` | 触发 panic `Runtime not initialized` | |
| 150 | + |
| 151 | +--- |
| 152 | + |
| 153 | +## 按层统计 |
| 154 | + |
| 155 | +| 层/域 | 数量 | 说明 | |
| 156 | +|------|------|------| |
| 157 | +| Parser | 4 | 只验证语法失败归属 | |
| 158 | +| Loader+Schema | 12 | 只验证结构校验、条件必填、默认注入 | |
| 159 | +| Runtime | 6 | 只验证调度顺序与生命周期 | |
| 160 | +| Component Semantic | 8 | 只验证语义规则 | |
| 161 | +| Business Workflows | 8 | 保留原有高价值业务流程测试 | |
| 162 | +| Robustness & Concurrency | 8 | 保留原有并发与边界保护测试 | |
| 163 | +| **总计** | **46** | 三层重构 + 原有单测统一清单 | |
| 164 | + |
| 165 | +--- |
| 166 | + |
| 167 | +## 编写规范 |
| 168 | + |
| 169 | +- 错误断言按层级关键字匹配: |
| 170 | + - 语法层:`parse error` |
| 171 | + - 结构层:`structural error` |
| 172 | + - 语义层:`failed to validate` 或组件语义错误信息 |
| 173 | +- 业务流程测试不承担结构层职责断言。 |
| 174 | +- 并发测试建议配合 `-race` 在 CI 中执行。 |
| 175 | + |
| 176 | +--- |
| 177 | + |
| 178 | +## Mock 说明 |
| 179 | + |
| 180 | +| Mock对象 | 用途 | 实现方式 | |
| 181 | +|---------|------|---------| |
| 182 | +| Mock Genkit Registry | 模拟模型注册与组件初始化 | 使用 `testutils.CreateMockGenkitRegistry()` 或等价 stub | |
| 183 | +| Mock Prompt | 模拟 LLM 返回(含 ToolRequests) | 自定义 `MockPrompt` / stub prompt,覆盖成功与失败分支 | |
| 184 | +| Mock Tool | 模拟工具调用成功/失败/超时 | 本地 mock struct + 可配置返回值 | |
| 185 | +| Mock Retriever/Indexer/Splitter | 控制 RAG 行为并断言参数 | 本地 mock struct + 调用计数/入参记录 | |
| 186 | +| Stub Component | 验证 Runtime 初始化顺序/错误传播 | 自定义实现 `runtime.Component`,可注入 `Validate/Init/Start/Stop` 行为 | |
| 187 | +| 临时配置文件夹(Fixture Dir) | 隔离配置输入与路径解析 | `t.TempDir()` 下写入最小 YAML/Schema 夹具 | |
| 188 | + |
| 189 | +--- |
| 190 | + |
| 191 | +## Go 表格驱动单测编写规范 |
| 192 | + |
| 193 | +### 1) 基本模板 |
| 194 | + |
| 195 | +```go |
| 196 | +func TestXXX(t *testing.T) { |
| 197 | + tests := []struct { |
| 198 | + name string |
| 199 | + input any |
| 200 | + wantErr bool |
| 201 | + errLike string |
| 202 | + }{ |
| 203 | + { |
| 204 | + name: "valid case", |
| 205 | + input: ..., |
| 206 | + wantErr: false, |
| 207 | + }, |
| 208 | + { |
| 209 | + name: "invalid case", |
| 210 | + input: ..., |
| 211 | + wantErr: true, |
| 212 | + errLike: "structural error", |
| 213 | + }, |
| 214 | + } |
| 215 | + |
| 216 | + for _, tt := range tests { |
| 217 | + t.Run(tt.name, func(t *testing.T) { |
| 218 | + // arrange |
| 219 | + // act |
| 220 | + // assert |
| 221 | + }) |
| 222 | + } |
| 223 | +} |
| 224 | +``` |
| 225 | + |
| 226 | +### 2) 断言规范 |
| 227 | + |
| 228 | +- 错误断言使用关键子串匹配,避免脆弱的全量字符串匹配。 |
| 229 | +- 每条测试只验证一个主行为(单一断言目标)。 |
| 230 | +- 分层测试中禁止跨层断言: |
| 231 | + - Parser 测试不验证语义 |
| 232 | + - Loader 测试不验证组件语义 |
| 233 | + - Component 测试假设结构已通过 |
| 234 | + |
| 235 | +### 3) 并发与稳定性 |
| 236 | + |
| 237 | +- 并发测试统一使用 `go test -race` 验证。 |
| 238 | +- 避免真实网络/端口依赖,全部使用本地 mock。 |
| 239 | +- 使用 `t.Helper()` 封装重复构建逻辑,提高可读性。 |
| 240 | + |
| 241 | +### 4) 夹具与命名 |
| 242 | + |
| 243 | +- 测试名采用 `Test<模块>_<行为>_<预期>` 风格。 |
| 244 | +- 配置夹具优先最小化:只保留触发当前断言所需字段。 |
| 245 | +- 使用 `t.TempDir()` 管理临时文件,避免污染仓库。 |
| 246 | + |
| 247 | +### 5) 回归要求 |
| 248 | + |
| 249 | +- 每个缺陷修复至少补 1 条失败重现用例。 |
| 250 | +- 先写失败断言,再修复实现(红-绿流程)。 |
0 commit comments