package lua import ( "encoding/json" "strings" "testing" ) // E2E:加载改后的适配器,验证 (1) 头含两个 session 头且值相同;(2) id 段服从公式; // (3) 无 tools 的请求被注入 read 桩且 properties 非空。 func TestOpencodeZenAdapterE2E(t *testing.T) { src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua") if err != nil { t.Fatal(err) } vm := NewVM(t.TempDir()) defer vm.Stop() if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil { t.Fatalf("加载失败: %v", err) } t.Run("头:两个 session 头同值", func(t *testing.T) { h, err := vm.BuildHeaders("opencodezen", map[string]interface{}{"timestamp": int64(1792065049)}) if err != nil { t.Fatal(err) } s1, s2 := h["x-opencode-session"], h["x-opencode-session-id"] if s1 == "" || s2 == "" { t.Fatalf("两个 session 头都必须存在: %#v", h) } if s1 != s2 { t.Fatalf("必须同值: %q vs %q", s1, s2) } if !strings.HasPrefix(s1, "ses_") || len(s1) != 4+12+14 { t.Fatalf("session 形状应为 ses_+12hex+14base62: %q (len=%d)", s1, len(s1)) } if len(h["x-opencode-request"]) != 4+12+14 || !strings.HasPrefix(h["x-opencode-request"], "msg_") { t.Fatalf("request 形状: %q", h["x-opencode-request"]) } if !strings.Contains(h["User-Agent"], "opencode/1.19.0") { t.Fatalf("UA 应为 1.19.0: %q", h["User-Agent"]) } t.Logf("session=%s request=%s", s1, h["x-opencode-request"]) // 公式校验:段 = (K - bucket_ms*4096) & MASK,其中 bucket 已对齐到整分钟 // (适配器按分钟桶取会话号,以保留上游前缀缓存命中)。 // ⚠️ 判据必须算**同一个桶**,不能拿原始 ms 去比 —— 那会把正确的实现判成错。 seg := s1[4:16] ms := int64(1792065049) * 1000 bucket := ms - (ms % 60000) want := (int64(0x1afffffffffffe) - bucket*4096) & 0xFFFFFFFFFFFF if got := seg; got != fmtHex12(want) { t.Fatalf("session 段公式不符: got=%s want=%s (bucket=%d)", got, fmtHex12(want), bucket) } }) t.Run("tools 兜底:无 tools 时注入 read", func(t *testing.T) { out, err := vm.Transform("opencodezen", "transform_request", `{"model":"m","stream":true,"messages":[{"role":"user","content":"hi"}]}`) if err != nil { t.Fatal(err) } var req map[string]interface{} if err := json.Unmarshal([]byte(out), &req); err != nil { t.Fatal(err) } ts, _ := req["tools"].([]interface{}) if len(ts) == 0 { t.Fatalf("必须注入 tools: %s", out) } found := false for _, x := range ts { fn := x.(map[string]interface{})["function"].(map[string]interface{}) if fn["name"] == "read" { p, _ := fn["parameters"].(map[string]interface{}) props, _ := p["properties"].(map[string]interface{}) if len(props) == 0 { t.Fatalf("read 的 properties 必须非空(门禁要求): %s", out) } found = true } } if !found { t.Fatalf("必须含名为 read 的工具: %s", out) } }) t.Run("tools 保留:用户自带 read 不被覆盖", func(t *testing.T) { in := `{"model":"m","stream":true,"messages":[{"role":"user","content":"hi"}], "tools":[{"type":"function","function":{"name":"read","description":"MY","parameters":{"type":"object","properties":{"a":{"type":"string"}}}}}]}` out, err := vm.Transform("opencodezen", "transform_request", in) if err != nil { t.Fatal(err) } var req map[string]interface{} json.Unmarshal([]byte(out), &req) ts, _ := req["tools"].([]interface{}) if len(ts) != 1 { t.Fatalf("不应追加(已有 read): %s", out) } fn := ts[0].(map[string]interface{})["function"].(map[string]interface{}) if fn["description"] != "MY" { t.Fatalf("用户定义被覆盖: %s", out) } }) } func fmtHex12(v int64) string { const hex = "0123456789abcdef" out := make([]byte, 12) for i := 11; i >= 0; i-- { out[i] = hex[v&0xF] v >>= 4 } return string(out) } // ★ 免费层只能流式(适配器强制 stream=true),而内核在客户端要非流式时 // 会调 transform_response(期望 JSON)⇒ 适配器必须能把 SSE 聚合回非流式对象。 // 这条判据钉住那个聚合:真实 SSE 片段 → 完整 chat.completion。 func TestOpencodeZenAggregatesSSEForNonStreamClient(t *testing.T) { src, err := bundledAdapters.ReadFile("adapters/opencodezen.lua") if err != nil { t.Fatal(err) } vm := NewVM(t.TempDir()) defer vm.Stop() if err := vm.LoadAdapterSource("opencodezen", string(src)); err != nil { t.Fatal(err) } // 模仿上游真实 SSE(含 reasoning_content 与 usage 末帧) sse := `data: {"id":"x1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"role":"assistant","content":"","reasoning_content":"think "}}]} data: {"id":"x1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":"HELLO"}}]} data: {"id":"x1","object":"chat.completion.chunk","created":1,"model":"m","choices":[{"index":0,"delta":{"content":" WORLD"},"finish_reason":"stop"}]} data: {"id":"x1","object":"chat.completion.chunk","created":1,"model":"m","choices":[],"usage":{"prompt_tokens":7,"completion_tokens":2,"total_tokens":9}} data: [DONE] ` out, err := vm.Transform("opencodezen", "transform_response", sse) if err != nil { t.Fatal(err) } var u map[string]interface{} if err := json.Unmarshal([]byte(out), &u); err != nil { t.Fatalf("聚合产物不是 JSON: %v (%s)", err, out) } if u["content"] != "HELLO WORLD" { t.Fatalf("content 应为 'HELLO WORLD',实得 %v", u["content"]) } tu, _ := u["token_usage"].(map[string]interface{}) if tu == nil || tu["prompt"] != float64(7) { t.Fatalf("usage 应透传: %v", u["token_usage"]) } if u["finish_reason"] != "stop" { t.Fatalf("finish_reason 应为 stop: %v", u["finish_reason"]) } t.Logf("聚合 OK: content=%v usage=%v finish=%v", u["content"], tu, u["finish_reason"]) }