fix: 完善工具名改写测试和格式
This commit is contained in:
@@ -15,8 +15,8 @@ import (
|
|||||||
utls "github.com/refraction-networking/utls"
|
utls "github.com/refraction-networking/utls"
|
||||||
)
|
)
|
||||||
|
|
||||||
// CapturedFingerprint mirrors the Fingerprint struct from tls-fingerprint-web.
|
// CapturedFingerprint 对应 tls-fingerprint-web 返回的 Fingerprint 结构。
|
||||||
// Used to deserialize the JSON response from the capture server.
|
// 用于反序列化 capture server 的 JSON 响应。
|
||||||
type CapturedFingerprint struct {
|
type CapturedFingerprint struct {
|
||||||
JA3Raw string `json:"ja3_raw"`
|
JA3Raw string `json:"ja3_raw"`
|
||||||
JA3Hash string `json:"ja3_hash"`
|
JA3Hash string `json:"ja3_hash"`
|
||||||
@@ -35,17 +35,17 @@ type CapturedFingerprint struct {
|
|||||||
EnableGREASE bool `json:"enable_grease"`
|
EnableGREASE bool `json:"enable_grease"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDialerAgainstCaptureServer connects to the tls-fingerprint-web capture server
|
// TestDialerAgainstCaptureServer 连接 tls-fingerprint-web capture server,
|
||||||
// and verifies that the dialer's TLS fingerprint matches the configured Profile.
|
// 验证 Dialer 的 TLS 指纹是否匹配配置的 Profile。
|
||||||
//
|
//
|
||||||
// Default capture server: https://tls.sub2api.org:8090
|
// 该测试依赖外部服务,默认跳过。需要手动验证时设置:
|
||||||
// Override with env: TLSFINGERPRINT_CAPTURE_URL=https://localhost:8443
|
// TLSFINGERPRINT_CAPTURE_URL=https://localhost:8443
|
||||||
//
|
//
|
||||||
// Run: go test -v -run TestDialerAgainstCaptureServer ./internal/pkg/tlsfingerprint/...
|
// 运行方式:go test -tags=integration -v -run TestDialerAgainstCaptureServer ./internal/pkg/tlsfingerprint/...
|
||||||
func TestDialerAgainstCaptureServer(t *testing.T) {
|
func TestDialerAgainstCaptureServer(t *testing.T) {
|
||||||
captureURL := os.Getenv("TLSFINGERPRINT_CAPTURE_URL")
|
captureURL := strings.TrimSpace(os.Getenv("TLSFINGERPRINT_CAPTURE_URL"))
|
||||||
if captureURL == "" {
|
if captureURL == "" {
|
||||||
captureURL = "https://tls.sub2api.org:8090"
|
t.Skip("跳过外部 TLS 指纹 capture 测试:未设置 TLSFINGERPRINT_CAPTURE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
@@ -57,7 +57,7 @@ func TestDialerAgainstCaptureServer(t *testing.T) {
|
|||||||
profile: &Profile{
|
profile: &Profile{
|
||||||
Name: "default",
|
Name: "default",
|
||||||
EnableGREASE: false,
|
EnableGREASE: false,
|
||||||
// All empty → uses built-in defaults
|
// 全部留空时使用内置默认值
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -104,7 +104,7 @@ func TestDialerAgainstCaptureServer(t *testing.T) {
|
|||||||
t.Logf("JA3 Hash: %s", captured.JA3Hash)
|
t.Logf("JA3 Hash: %s", captured.JA3Hash)
|
||||||
t.Logf("JA4: %s", captured.JA4)
|
t.Logf("JA4: %s", captured.JA4)
|
||||||
|
|
||||||
// Resolve effective profile values (what the dialer actually uses)
|
// 解析实际生效的 Profile 值,也就是 Dialer 最终使用的值。
|
||||||
effectiveCipherSuites := tc.profile.CipherSuites
|
effectiveCipherSuites := tc.profile.CipherSuites
|
||||||
if len(effectiveCipherSuites) == 0 {
|
if len(effectiveCipherSuites) == 0 {
|
||||||
effectiveCipherSuites = defaultCipherSuites
|
effectiveCipherSuites = defaultCipherSuites
|
||||||
@@ -144,7 +144,7 @@ func TestDialerAgainstCaptureServer(t *testing.T) {
|
|||||||
effectivePSKModes = []uint16{1} // psk_dhe_ke
|
effectivePSKModes = []uint16{1} // psk_dhe_ke
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify each field
|
// 校验每个指纹字段
|
||||||
assertIntSliceEqual(t, "cipher_suites", uint16sToInts(effectiveCipherSuites), captured.CipherSuites)
|
assertIntSliceEqual(t, "cipher_suites", uint16sToInts(effectiveCipherSuites), captured.CipherSuites)
|
||||||
assertIntSliceEqual(t, "curves", uint16sToInts(effectiveCurves), captured.Curves)
|
assertIntSliceEqual(t, "curves", uint16sToInts(effectiveCurves), captured.Curves)
|
||||||
assertIntSliceEqual(t, "point_formats", uint16sToInts(effectivePointFormats), captured.PointFormats)
|
assertIntSliceEqual(t, "point_formats", uint16sToInts(effectivePointFormats), captured.PointFormats)
|
||||||
@@ -160,13 +160,13 @@ func TestDialerAgainstCaptureServer(t *testing.T) {
|
|||||||
t.Logf(" enable_grease: %v OK", captured.EnableGREASE)
|
t.Logf(" enable_grease: %v OK", captured.EnableGREASE)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify extension order
|
// 校验扩展顺序;如果 Profile 显式配置了 Extensions 就使用配置值,
|
||||||
// Use profile.Extensions if set, otherwise the default order (Node.js 24.x)
|
// 否则使用默认顺序(Node.js 24.x)。
|
||||||
expectedExtOrder := uint16sToInts(defaultExtensionOrder)
|
expectedExtOrder := uint16sToInts(defaultExtensionOrder)
|
||||||
if len(tc.profile.Extensions) > 0 {
|
if len(tc.profile.Extensions) > 0 {
|
||||||
expectedExtOrder = uint16sToInts(tc.profile.Extensions)
|
expectedExtOrder = uint16sToInts(tc.profile.Extensions)
|
||||||
}
|
}
|
||||||
// Strip GREASE values from both expected and captured for comparison
|
// 比较前从期望值和采集值中剔除 GREASE。
|
||||||
var filteredExpected, filteredActual []int
|
var filteredExpected, filteredActual []int
|
||||||
for _, e := range expectedExtOrder {
|
for _, e := range expectedExtOrder {
|
||||||
if !isGREASEValue(uint16(e)) {
|
if !isGREASEValue(uint16(e)) {
|
||||||
@@ -180,7 +180,7 @@ func TestDialerAgainstCaptureServer(t *testing.T) {
|
|||||||
}
|
}
|
||||||
assertIntSliceEqual(t, "extensions (order, non-GREASE)", filteredExpected, filteredActual)
|
assertIntSliceEqual(t, "extensions (order, non-GREASE)", filteredExpected, filteredActual)
|
||||||
|
|
||||||
// Print full captured data as JSON for debugging
|
// 打印完整采集结果,便于排查指纹差异。
|
||||||
capturedJSON, _ := json.MarshalIndent(captured, " ", " ")
|
capturedJSON, _ := json.MarshalIndent(captured, " ", " ")
|
||||||
t.Logf("Full captured fingerprint:\n %s", string(capturedJSON))
|
t.Logf("Full captured fingerprint:\n %s", string(capturedJSON))
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -169,10 +169,10 @@ func buildToolNameRewriteFromBody(body []byte) *ToolNameRewrite {
|
|||||||
|
|
||||||
// applyToolNameRewriteToBody 把已构造的 ToolNameRewrite 应用到 body 上:
|
// applyToolNameRewriteToBody 把已构造的 ToolNameRewrite 应用到 body 上:
|
||||||
//
|
//
|
||||||
// - 改写 $.tools[*].name(仅对 shouldMimicToolName 通过的 tool)
|
// - 改写 $.tools[*].name(仅对 shouldMimicToolName 通过的 tool)
|
||||||
// - 改写 $.tool_choice.name(仅当 $.tool_choice.type == "tool")
|
// - 改写 $.tool_choice.name(仅当 $.tool_choice.type == "tool")
|
||||||
// - 改写 $.messages[*].content[*].name(仅当 type == "tool_use")
|
// - 改写 $.messages[*].content[*].name(仅当 type == "tool_use")
|
||||||
// - 在 $.tools[last].cache_control 上打 ephemeral 缓存断点
|
// - 在 $.tools[last].cache_control 上打 ephemeral 缓存断点
|
||||||
//
|
//
|
||||||
// 响应侧 bytes.Replace 会连带还原假名 → 真名。
|
// 响应侧 bytes.Replace 会连带还原假名 → 真名。
|
||||||
func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
|
func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
|
||||||
@@ -213,9 +213,8 @@ func applyToolNameRewriteToBody(body []byte, rw *ToolNameRewrite) []byte {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Rewrite tool_use names in messages to match the renamed tools.
|
// 同步改写历史消息中的 tool_use.name,确保它和 tools[] 中的假名一致。
|
||||||
// Without this, Anthropic rejects requests where messages reference tools
|
// 否则 Anthropic 会因为 tool_use 引用了未声明的原始工具名而拒绝请求。
|
||||||
// by their original name but tools[] declares the renamed (fake) name.
|
|
||||||
messages := gjson.GetBytes(body, "messages")
|
messages := gjson.GetBytes(body, "messages")
|
||||||
if messages.IsArray() {
|
if messages.IsArray() {
|
||||||
messages.ForEach(func(msgKey, msg gjson.Result) bool {
|
messages.ForEach(func(msgKey, msg gjson.Result) bool {
|
||||||
|
|||||||
@@ -69,26 +69,25 @@ func TestApplyToolNameRewriteToBody_RenamesToolsAndToolChoice(t *testing.T) {
|
|||||||
require.NotNil(t, rw)
|
require.NotNil(t, rw)
|
||||||
require.Contains(t, rw.Forward, "sessions_list")
|
require.Contains(t, rw.Forward, "sessions_list")
|
||||||
require.Contains(t, rw.Forward, "session_get")
|
require.Contains(t, rw.Forward, "session_get")
|
||||||
// web_search is a server tool, not rewritten
|
// web_search 是 server tool,不参与工具名改写
|
||||||
require.NotContains(t, rw.Forward, "web_search")
|
require.NotContains(t, rw.Forward, "web_search")
|
||||||
|
|
||||||
out := applyToolNameRewriteToBody(body, rw)
|
out := applyToolNameRewriteToBody(body, rw)
|
||||||
|
|
||||||
// tools[0].name and tools[1].name rewritten; tools[2].name untouched
|
// tools[0].name 和 tools[1].name 被改写,tools[2].name 保持不变
|
||||||
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tools.0.name").String())
|
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tools.0.name").String())
|
||||||
require.Equal(t, "cc_ses_get", gjson.GetBytes(out, "tools.1.name").String())
|
require.Equal(t, "cc_ses_get", gjson.GetBytes(out, "tools.1.name").String())
|
||||||
require.Equal(t, "web_search", gjson.GetBytes(out, "tools.2.name").String())
|
require.Equal(t, "web_search", gjson.GetBytes(out, "tools.2.name").String())
|
||||||
|
|
||||||
// tool_choice.name rewritten
|
// tool_choice.name 被同步改写
|
||||||
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tool_choice.name").String())
|
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tool_choice.name").String())
|
||||||
require.Equal(t, "tool", gjson.GetBytes(out, "tool_choice.type").String())
|
require.Equal(t, "tool", gjson.GetBytes(out, "tool_choice.type").String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
func TestApplyToolNameRewriteToBody_RenamesToolUseInMessages(t *testing.T) {
|
func TestApplyToolNameRewriteToBody_RenamesToolUseInMessages(t *testing.T) {
|
||||||
// sessions_list -> cc_sess_list (static prefix: sessions_ -> sessions_)
|
// sessions_list 通过静态前缀规则改写为 cc_sess_list
|
||||||
// web_search is a server tool (type != ""), not rewritten
|
// web_search 是 server tool(type != ""),不参与工具名改写
|
||||||
// messages tool_use names must be rewritten to match tools[]
|
// messages 中的 tool_use.name 必须同步改写,才能和 tools[] 保持一致
|
||||||
body := []byte(`{"tools":[{"name":"sessions_list","input_schema":{}},{"name":"web_search","type":"web_search_20250305"}],"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]},{"role":"assistant","content":[{"type":"tool_use","id":"tu_01","name":"sessions_list","input":{}},{"type":"text","text":"thinking"}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_01","content":"ok"}]}]}`)
|
body := []byte(`{"tools":[{"name":"sessions_list","input_schema":{}},{"name":"web_search","type":"web_search_20250305"}],"messages":[{"role":"user","content":[{"type":"text","text":"hi"}]},{"role":"assistant","content":[{"type":"tool_use","id":"tu_01","name":"sessions_list","input":{}},{"type":"text","text":"thinking"}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_01","content":"ok"}]}]}`)
|
||||||
rw := buildToolNameRewriteFromBody(body)
|
rw := buildToolNameRewriteFromBody(body)
|
||||||
require.NotNil(t, rw)
|
require.NotNil(t, rw)
|
||||||
@@ -96,18 +95,42 @@ func TestApplyToolNameRewriteToBody_RenamesToolUseInMessages(t *testing.T) {
|
|||||||
|
|
||||||
out := applyToolNameRewriteToBody(body, rw)
|
out := applyToolNameRewriteToBody(body, rw)
|
||||||
|
|
||||||
// tools[0].name rewritten
|
// tools[0].name 被改写
|
||||||
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tools.0.name").String())
|
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "tools.0.name").String())
|
||||||
// tools[1].name untouched (server tool)
|
// tools[1].name 是 server tool,保持不变
|
||||||
require.Equal(t, "web_search", gjson.GetBytes(out, "tools.1.name").String())
|
require.Equal(t, "web_search", gjson.GetBytes(out, "tools.1.name").String())
|
||||||
// messages[1].content[0].name (tool_use) also rewritten to match tools
|
// messages[1].content[0].name 是 tool_use,必须同步改写以匹配 tools[]
|
||||||
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "messages.1.content.0.name").String())
|
require.Equal(t, "cc_sess_list", gjson.GetBytes(out, "messages.1.content.0.name").String())
|
||||||
// messages[1].content[1] (text) untouched
|
// messages[1].content[1] 是 text,保持不变
|
||||||
require.Equal(t, "thinking", gjson.GetBytes(out, "messages.1.content.1.text").String())
|
require.Equal(t, "thinking", gjson.GetBytes(out, "messages.1.content.1.text").String())
|
||||||
// messages[2].content[0] (tool_result) untouched — no name field in tool_result
|
// messages[2].content[0] 是 tool_result,不包含 name 字段,保持不变
|
||||||
require.Equal(t, "ok", gjson.GetBytes(out, "messages.2.content.0.content").String())
|
require.Equal(t, "ok", gjson.GetBytes(out, "messages.2.content.0.content").String())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestApplyToolNameRewriteToBody_RenamesToolUseWithDynamicMapping(t *testing.T) {
|
||||||
|
body := []byte(`{"tools":[{"name":"alpha_search","input_schema":{}},{"name":"beta_lookup","input_schema":{}},{"name":"gamma_fetch","input_schema":{}},{"name":"delta_update","input_schema":{}},{"name":"epsilon_parse","input_schema":{}},{"name":"zeta_render","input_schema":{}},{"name":"web_search","type":"web_search_20250305"}],"tool_choice":{"type":"tool","name":"gamma_fetch"},"messages":[{"role":"assistant","content":[{"type":"tool_use","id":"tu_dyn","name":"gamma_fetch","input":{}},{"type":"tool_use","id":"tu_srv","name":"web_search","input":{}},{"type":"text","text":"done"}]},{"role":"user","content":[{"type":"tool_result","tool_use_id":"tu_dyn","content":"ok"}]}]}`)
|
||||||
|
rw := buildToolNameRewriteFromBody(body)
|
||||||
|
require.NotNil(t, rw)
|
||||||
|
require.Len(t, rw.Forward, 6)
|
||||||
|
|
||||||
|
fakeGamma := rw.Forward["gamma_fetch"]
|
||||||
|
require.NotEmpty(t, fakeGamma)
|
||||||
|
require.NotEqual(t, "gamma_fetch", fakeGamma)
|
||||||
|
require.NotContains(t, rw.Forward, "web_search")
|
||||||
|
|
||||||
|
out := applyToolNameRewriteToBody(body, rw)
|
||||||
|
|
||||||
|
// 动态映射会改写 tools[]、tool_choice 和历史 tool_use 中的同一个工具名
|
||||||
|
require.Equal(t, fakeGamma, gjson.GetBytes(out, "tools.2.name").String())
|
||||||
|
require.Equal(t, fakeGamma, gjson.GetBytes(out, "tool_choice.name").String())
|
||||||
|
require.Equal(t, fakeGamma, gjson.GetBytes(out, "messages.0.content.0.name").String())
|
||||||
|
// server tool 不参与动态映射,历史 tool_use 中同名引用也保持不变
|
||||||
|
require.Equal(t, "web_search", gjson.GetBytes(out, "tools.6.name").String())
|
||||||
|
require.Equal(t, "web_search", gjson.GetBytes(out, "messages.0.content.1.name").String())
|
||||||
|
// tool_result 依靠 tool_use_id 关联,不需要 name 字段
|
||||||
|
require.Equal(t, "ok", gjson.GetBytes(out, "messages.1.content.0.content").String())
|
||||||
|
}
|
||||||
|
|
||||||
func TestApplyToolsLastCacheBreakpoint_InjectsDefault(t *testing.T) {
|
func TestApplyToolsLastCacheBreakpoint_InjectsDefault(t *testing.T) {
|
||||||
body := []byte(`{"tools":[{"name":"a","input_schema":{}},{"name":"b","input_schema":{}}]}`)
|
body := []byte(`{"tools":[{"name":"a","input_schema":{}},{"name":"b","input_schema":{}}]}`)
|
||||||
out := applyToolsLastCacheBreakpoint(body)
|
out := applyToolsLastCacheBreakpoint(body)
|
||||||
|
|||||||
Reference in New Issue
Block a user