package engine import ( "testing" ) // L1187 契约测试: DefaultNormalizePipeline 的执行顺序必须满足文档化依赖图. // // 依赖偏序 (见 DefaultNormalizePipeline 注释): // // 1. tool_result_pairing 必须早于 orphan_tool_result // 2. empty_message 必须早于 whitespace_assistant // 3. 所有结构化 normalizer 必须早于 consecutive_role // 4. 所有 transformation 必须早于 image_validator // // 本测试硬编码 10 个内置 normalizer 的预期执行顺序, 任何未来改 Priority // 值破坏上述 4 条偏序会立刻 fail CI, 提供零静默破坏保证. func TestDefaultNormalizePipeline_OrderingContract(t *testing.T) { pipeline := DefaultNormalizePipeline() // 触发排序 (通过 Run 路径, 模拟真实使用) // pipeline.Run 会在第一次调用时按 Priority 排序 // 这里用反射直接访问 sorted steps 以获得顺序 // // 等价做法: pipeline.Run(空 messages), 然后遍历 steps. 但我们需要能看到 // 排序后的名称, 所以用 Run 触发 + 一个 trackingNormalizer 记录执行顺序 // 不可行 (它会改变 pipeline 行为). 直接访问内部 steps 字段是包内测试的 // 合法特权 (本测试在同包 package engine). // 强制排序 (模拟 Run 的排序分支) pipeline.Run(nil) // 空消息直接返回, 但仍会走排序分支 // 获取排序后的顺序 actual := make([]string, 0, len(pipeline.steps)) for _, s := range pipeline.steps { actual = append(actual, s.Name()) } // 预期顺序 (见 normalize.go DefaultNormalizePipeline 注释) // 注: attachment_reorder 目前被注释掉, 不在 default pipeline 里 expected := []string{ "tool_result_pairing", // 8 "orphan_tool_result", // 10 "error_content_strip", // 15 "orphan_thinking", // 18 "empty_message", // 20 "whitespace_assistant", // 22 "tool_input", // 25 "consecutive_role", // 30 "image_validator", // 50 } if len(actual) != len(expected) { t.Fatalf("DefaultNormalizePipeline size = %d, want %d\n actual: %v\n expected: %v", len(actual), len(expected), actual, expected) } for i, name := range expected { if actual[i] != name { t.Errorf("DefaultNormalizePipeline[%d] = %q, want %q\n full actual: %v", i, actual[i], name, actual) } } } // TestDefaultNormalizePipeline_DependencyInvariants 用偏序断言代替完整序列, // 这样未来加入新 normalizer (consumer / 场景插入) 时只要保持偏序, 测试仍通过, // 不会因为"多了一个新节点"整体 fail. // // 与 OrderingContract 互补: OrderingContract 锁定精确顺序 (catch 误移默认步骤), // DependencyInvariants 锁定核心偏序 (catch 破坏真实依赖). func TestDefaultNormalizePipeline_DependencyInvariants(t *testing.T) { pipeline := DefaultNormalizePipeline() pipeline.Run(nil) // 触发排序 // 建立 name → index 映射 idx := make(map[string]int, len(pipeline.steps)) for i, s := range pipeline.steps { idx[s.Name()] = i } mustBefore := func(earlier, later string) { t.Helper() e, eOk := idx[earlier] l, lOk := idx[later] if !eOk { t.Errorf("preorder: %q not found in default pipeline", earlier) return } if !lOk { t.Errorf("preorder: %q not found in default pipeline", later) return } if e >= l { t.Errorf("dependency violated: %q (idx %d) must run before %q (idx %d)", earlier, e, later, l) } } // 偏序 1: tool_result_pairing 必须早于 orphan_tool_result mustBefore("tool_result_pairing", "orphan_tool_result") // 偏序 2: empty_message 必须早于 whitespace_assistant mustBefore("empty_message", "whitespace_assistant") // 偏序 3: 所有结构化 normalizer 必须早于 consecutive_role // (consecutive_role 是合并 pass, 过早合并会掩盖需要被过滤的空消息) for _, structural := range []string{ "tool_result_pairing", "orphan_tool_result", "orphan_thinking", "empty_message", "whitespace_assistant", } { mustBefore(structural, "consecutive_role") } // 偏序 4: 所有 transformation 必须早于 image_validator // (image_validator 是验证 pass, 不应看到被清理前的脏数据) for _, transformation := range []string{ "tool_result_pairing", "orphan_tool_result", "error_content_strip", "orphan_thinking", "empty_message", "whitespace_assistant", "tool_input", "consecutive_role", } { mustBefore(transformation, "image_validator") } }