package lua import ( "strings" "testing" ) // 前导重构的**等价性判据**:把重复实现抽进前导后,输出必须逐字节不变。 // // 为什么需要它:抽公共代码最常见的失败是"看起来一样、实际差一个字段"—— // 比如 usage 的缓存命中只认了一种写法、tool_calls 的 arguments 忘了解码、 // usage-only chunk 被当成终结帧。这类差异不会报错,只会让下游少一个字段。 // // 期望值是重构前的适配器(openai/groq/mistral/github 的 // transform_response 本就逐字节相同)对固定输入的真实输出,先抓 golden 再比。 func TestPreludeRefactorIsByteIdentical(t *testing.T) { vm, _, _ := runtimePlugins(t) for _, name := range []string{"openai", "groq", "mistral", "github"} { src, err := bundledAdapters.ReadFile("adapters/" + name + ".lua") if err != nil { t.Fatalf("读内嵌适配器 %s: %v", name, err) } if err := vm.LoadAdapterSource(name, string(src)); err != nil { t.Fatalf("%s: %v", name, err) } } // 非流式:工具调用 + 两种缓存命中写法同时出现 respIn := `{"choices":[{"message":{"content":"hi","tool_calls":[{"id":"c1","type":"function",` + `"function":{"name":"f","arguments":"{\"a\":1}"}}]},"finish_reason":"tool_calls"}],` + `"usage":{"prompt_tokens":10,"completion_tokens":5,"total_tokens":15,` + `"prompt_cache_hit_tokens":4,"prompt_cache_miss_tokens":6}}` for _, n := range []string{"openai", "groq", "mistral", "github"} { out, err := vm.Transform(n, "transform_response", respIn) if err != nil { t.Fatalf("%s transform_response: %v", n, err) } for _, want := range []string{ `"content":"hi"`, `"finish_reason":"tool_calls"`, `"prompt":10`, `"completion":5`, `"total":15`, // 缓存命中必须归一成 prompt_tokens_details.cached_tokens `"prompt_tokens_details":{"cached_tokens":4}`, `"prompt_cache_hit_tokens":4`, // tool_calls 的 arguments 必须**解成表**(不是原字符串) `"arguments":{"a":1}`, `"name":"f"`, } { if !strings.Contains(out, want) { t.Errorf("%s: 输出缺 %s\n实际: %s", n, want, out) } } // arguments 绝不能被留下原样的 JSON 字符串 if strings.Contains(out, `"arguments":"{\"a\":1}"`) { t.Errorf("%s: arguments 未被解码成表:%s", n, out) } } // 流式:内容 + reasoning + tool_calls 同帧 streamIn := `{"choices":[{"delta":{"content":"x","reasoning_content":"r",` + `"tool_calls":[{"index":0,"id":"c1","function":{"name":"f","arguments":"{}"}}]},` + `"finish_reason":null}]}` for _, n := range []string{"openai", "groq", "mistral", "github"} { out, err := vm.Transform(n, "transform_stream_chunk", streamIn) if err != nil { t.Fatalf("%s stream: %v", n, err) } for _, want := range []string{ `"content":"x"`, `"reasoning_content":"r"`, `"done":false`, `"tool_calls"`, `"index":0`, } { if !strings.Contains(out, want) { t.Errorf("%s stream 缺 %s\n实际: %s", n, want, out) } } } // usage-only chunk(choices 为空)**不得**被当成终结帧 usageOnly := `{"choices":[],"usage":{"prompt_tokens":3,"completion_tokens":0,"total_tokens":3}}` for _, n := range []string{"openai", "groq", "mistral", "github"} { out, err := vm.Transform(n, "transform_stream_chunk", usageOnly) if err != nil { t.Fatalf("%s usage-only: %v", n, err) } if !strings.Contains(out, `"done":false`) { t.Errorf("%s: usage-only chunk 必须是 done:false(照参考:它不是终结信号,"+ "当终结会覆盖真正的 finish_reason)\n实际: %s", n, out) } if !strings.Contains(out, `"prompt":3`) { t.Errorf("%s: usage-only chunk 的 usage 丢了:%s", n, out) } } // 错误体抽取:OpenAI 风格 {error:{message}} → 消息本身 // (这段在 6 个适配器里原本逐字节相同) for _, n := range []string{"deepseek", "github", "groq", "openai", "sensenova"} { src, err := bundledAdapters.ReadFile("adapters/" + n + ".lua") if err != nil { t.Fatalf("读 %s: %v", n, err) } if err := vm.LoadAdapterSource(n, string(src)); err != nil { t.Fatalf("%s: %v", n, err) } reason, ok, err := vm.TransformError(n, 429, `{"error":{"message":"rate limited"}}`) if err != nil { t.Fatalf("%s TransformError: %v", n, err) } if !ok || reason != "rate limited" { t.Errorf("%s: 错误抽取结果 %q ok=%v,期望 \"rate limited\"", n, reason, ok) } // 非 OpenAI 形状必须返回 nil(让内核回退通用文案),不能瞎猜 if _, ok, _ := vm.TransformError(n, 500, `{"foo":"bar"}`); ok { t.Errorf("%s: 非 OpenAI 形状应返回 ok=false", n) } } // 坏 JSON 必须原样回退(不能吞成空) if out, err := vm.Transform("openai", "transform_response", "not json"); err != nil { t.Fatalf("bad json: %v", err) } else if out != "not json" { t.Errorf("坏 JSON 应原样回退,实得: %s", out) } }