mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
产品代码统一为纯文本风格(照参考实现),去掉 emoji:billing/quota/usage 插件、 channels 的提示文案、WebUI 文案,以及 go 测试、docs、packaging 脚本中的 注意/OK/FAIL 等符号化写法。第三方目录(vendor、node_modules、.probe)不动。 另:packaging/upload-release.sh 的仓库名改为真实全名 JianFeeeee/ModelRouter → HeNanMU-EEA/ModelRouter(前者是重定向,POST/PUT 不跟随 会 404)。
133 lines
5.1 KiB
Go
133 lines
5.1 KiB
Go
package lua
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
// The hook payload used to be JSON round-tripped on every call. It no longer is,
|
|
// so the fast path and the old JSON path must be indistinguishable — otherwise
|
|
// a plugin silently sees a different payload than before, which is the worst
|
|
// kind of change: it compiles, passes a smoke test, and misprices traffic.
|
|
//
|
|
// These tests therefore compare the two paths on the SAME inputs rather than
|
|
// asserting the fast path's output in isolation.
|
|
|
|
func viaJSON(t *testing.T, payload map[string]interface{}) interface{} {
|
|
t.Helper()
|
|
b, err := json.Marshal(payload)
|
|
if err != nil {
|
|
t.Fatalf("marshal: %v", err)
|
|
}
|
|
var decoded interface{}
|
|
if err := json.Unmarshal(b, &decoded); err != nil {
|
|
t.Fatalf("unmarshal: %v", err)
|
|
}
|
|
return decoded
|
|
}
|
|
|
|
// payloads that exercise every branch of the fast converter.
|
|
func payloadCases() []map[string]interface{} {
|
|
return []map[string]interface{}{
|
|
{"model": "deepseek-v4.1-flash", "source": "commandcode", "ok": true},
|
|
// Numbers of every width: a converter that only knows float64 turns
|
|
// ints into something else, and a token count that arrives as a string
|
|
// makes a Lua hook do arithmetic on nil.
|
|
{"i": 42, "i8": int8(8), "i16": int16(16), "i32": int32(32), "i64": int64(1 << 40),
|
|
"u": uint(7), "u64": uint64(1 << 50), "f32": float32(1.5), "f64": 2.25},
|
|
// Zero, negative, and very large values must stay numbers.
|
|
{"zero": 0, "neg": -17, "huge": 1e308, "tiny": 1e-308},
|
|
// Slices and string maps: common in payloads and absent from a switch
|
|
// that only knows []interface{} / map[string]interface{}.
|
|
{"msgs": []interface{}{"a", "b"}, "tags": []string{"x", "y"}},
|
|
{"kv": map[string]string{"a": "1", "b": "2"}},
|
|
// Nesting, which is where a shallow converter silently drops a level.
|
|
{"usage": map[string]interface{}{
|
|
"prompt_tokens": 2048, "cache_hit_tokens": 1024,
|
|
"nested": map[string]interface{}{"deep": []interface{}{1, "two", true, nil}},
|
|
}},
|
|
{"nil_field": nil, "empty_map": map[string]interface{}{}, "empty_slice": []interface{}{}},
|
|
// A value the fast path does NOT model: it must fall back to JSON and
|
|
// still arrive, not disappear.
|
|
{"weird": struct {
|
|
A int `json:"a"`
|
|
B string `json:"b"`
|
|
}{1, "x"}},
|
|
}
|
|
}
|
|
|
|
func TestFastPathMatchesJSONPath(t *testing.T) {
|
|
for i, p := range payloadCases() {
|
|
want := viaJSON(t, p)
|
|
got := plainForLua(p)
|
|
if !reflect.DeepEqual(want, got) {
|
|
t.Errorf("case %d: fast path differs from JSON path\n payload: %#v\n json: %#v\n fast: %#v",
|
|
i, p, want, got)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestFastPathIsActuallyUsed guards against the fast path silently degrading to
|
|
// JSON for the payload the gateway really sends. If a future payload gains a
|
|
// type the converter does not model, this still works (it falls back) but the
|
|
// optimization is gone — and the next person measuring the hook would be
|
|
// measuring the old cost without knowing why.
|
|
func TestFastPathIsActuallyUsed(t *testing.T) {
|
|
// This mirrors the real request_end payload shape from the gateway.
|
|
realistic := map[string]interface{}{
|
|
"stage": "request_end", "kind": "end", "model": "deepseek-v4.1-flash",
|
|
"source": "commandcode", "key": "stress-key", "ok": true,
|
|
"status": 200, "duration_ms": 1234,
|
|
"usage": map[string]interface{}{
|
|
"prompt_tokens": float64(2048), "completion_tokens": float64(512),
|
|
"cache_hit_tokens": float64(1024), "total_tokens": float64(3584),
|
|
},
|
|
"walk": []interface{}{
|
|
map[string]interface{}{"kind": "tier_skip", "tier": 1, "source": "", "model": "", "reason": "cooldown"},
|
|
map[string]interface{}{"kind": "selected", "tier": 2, "source": "commandcode", "model": "m", "reason": ""},
|
|
},
|
|
"ts": float64(1700000000),
|
|
}
|
|
if _, ok := fastToPlain(realistic); !ok {
|
|
t.Errorf("the realistic request payload does NOT take the fast path — " +
|
|
"the JSON round-trip is still on the hot path for real traffic")
|
|
}
|
|
}
|
|
|
|
// TestFastPathDoesNotAliasInput: the converter builds a new tree. If it ever
|
|
// returned the caller's map directly, a Lua hook's writes could not reach Go —
|
|
// but worse, a later mutation of the payload would race with the snapshot the
|
|
// persistence saver is holding.
|
|
func TestFastPathDoesNotAliasInput(t *testing.T) {
|
|
src := map[string]interface{}{
|
|
"usage": map[string]interface{}{"prompt_tokens": float64(1)},
|
|
"list": []interface{}{"a"},
|
|
}
|
|
out, ok := fastToPlain(src)
|
|
if !ok {
|
|
t.Fatal("fast path declined a plain payload")
|
|
}
|
|
m := out.(map[string]interface{})
|
|
m["new"] = "added"
|
|
src["also_new"] = "must not appear"
|
|
|
|
if _, leaked := m["also_new"]; leaked {
|
|
t.Error("output map aliases the input map")
|
|
}
|
|
if _, leaked := src["new"]; leaked {
|
|
t.Error("writing to the output mutated the input")
|
|
}
|
|
// And the nested maps must be copies too.
|
|
inner := m["usage"].(map[string]interface{})
|
|
inner["prompt_tokens"] = float64(999)
|
|
if src["usage"].(map[string]interface{})["prompt_tokens"] != float64(1) {
|
|
t.Error("nested map is shared, not copied — a hook could mutate the payload")
|
|
}
|
|
list := m["list"].([]interface{})
|
|
list[0] = "changed"
|
|
if src["list"].([]interface{})[0] != "a" {
|
|
t.Error("nested slice is shared, not copied")
|
|
}
|
|
}
|