File size: 6,106 Bytes
a21c316
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
#[cfg(test)]
mod tests {
    use crate::proxy::mappers::claude::models::{
        ClaudeRequest, Message, MessageContent, ContentBlock, ThinkingConfig
    };
    use crate::proxy::mappers::claude::request::transform_claude_request_in;
    use crate::proxy::mappers::claude::thinking_utils::{analyze_conversation_state, close_tool_loop_for_thinking};
    use serde_json::json;

    
    // ==================================================================================
    // 场景一:首次 Thinking 请求 (P0-2 Fix)
    // 验证在没有历史签名的情况下,首次发起 Thinking 请求是否被放行 (Perimssive Mode)
    // ==================================================================================
    #[test]
    fn test_first_thinking_request_permissive_mode() {
        // 1. 构造一个全新的请求 (无历史消息)
        let req = ClaudeRequest {
            model: "claude-3-7-sonnet-20250219".to_string(),
            messages: vec![
                Message {
                    role: "user".to_string(),
                    content: MessageContent::String("Hello, please think.".to_string()),
                }
            ],
            system: None,
            tools: None, // 无工具调用
            stream: false,
            max_tokens: None,
            temperature: None,
            top_p: None,
            top_k: None,
            thinking: Some(ThinkingConfig {
                type_: "enabled".to_string(),
                budget_tokens: Some(1024),
                effort: None,
            }),
            metadata: None,
            output_config: None,
            size: None,
            quality: None,
        };

        // 2. 执行转换
        // 如果修复生效,这里应该成功返回,且 thinkingConfig 被保留
        let result = transform_claude_request_in(&req, "test-project", false, None, "test_session", None);
        assert!(result.is_ok(), "First thinking request should be allowed");

        let body = result.unwrap();
        let request = &body["request"];
        
        // 验证 thinkingConfig 是否存在 (即 thinking 模式未被禁用)
        let has_thinking_config = request.get("generationConfig")
            .and_then(|g| g.get("thinkingConfig"))
            .is_some();
            
        assert!(has_thinking_config, "Thinking config should be preserved for first request without tool calls");
    }

    // ==================================================================================
    // 场景二:工具循环恢复 (P1-4 Fix)
    // 验证当历史消息中丢失 Thinking 块导致死循环时,是否会自动注入合成消息来闭环
    // ==================================================================================
    #[test]
    fn test_tool_loop_recovery() {
        // 1. 构造一个 "Broken Tool Loop" 场景
        // Assistant (ToolUse) -> User (ToolResult)
        // 但 Assistant 消息中缺少 Thinking 块 (模拟被 stripping)
        let mut messages = vec![
            Message {
                role: "user".to_string(),
                content: MessageContent::String("Check weather".to_string()),
            },
            Message {
                role: "assistant".to_string(),
                content: MessageContent::Array(vec![
                    // 只有 ToolUse,没有 Thinking (Broken State)
                    ContentBlock::ToolUse {
                        id: "call_1".to_string(),
                        name: "get_weather".to_string(),
                        input: json!({"location": "Beijing"}),
                        signature: None,
                        cache_control: None,
                    }
                ]),
            },
            Message {
                role: "user".to_string(),
                content: MessageContent::Array(vec![
                    ContentBlock::ToolResult {
                        tool_use_id: "call_1".to_string(),
                        content: json!("Sunny"),
                        is_error: None,
                    }
                ]),
            }
        ];

        // 2. 分析当前状态
        let state = analyze_conversation_state(&messages);
        assert!(state.in_tool_loop, "Should detect tool loop");

        // 3. 执行恢复逻辑
        close_tool_loop_for_thinking(&mut messages);

        // 4. 验证是否注入了合成消息
        assert_eq!(messages.len(), 5, "Should have injected 2 synthetic messages");
        
        // 验证倒数第二条是 Assistant 的 "Completed" 消息
        let injected_assistant = &messages[3];
        assert_eq!(injected_assistant.role, "assistant");
        
        // 验证最后一条是 User 的 "Proceed" 消息
        let injected_user = &messages[4];
        assert_eq!(injected_user.role, "user");
        
        // 这样当前状态就不再是 "in_tool_loop" (最后一条是 User Text),模型可以开始新的 Thinking
        let new_state = analyze_conversation_state(&messages);
        assert!(!new_state.in_tool_loop, "Tool loop should be broken/closed");
    }

    // ==================================================================================
    // 场景三:跨模型兼容性 (P1-5 Fix) - 模拟
    // 由于 request.rs 中的 is_model_compatible 是私有的,我们通过集成测试验证效果
    // ==================================================================================
    /* 
       注意:由于 is_model_compatible 和缓存逻辑深度集成在 transform_claude_request_in 中,
       且依赖全局单例 SignatureCache,单元测试较难模拟 "缓存了旧签名但切换了模型" 的状态。
       这里主要通过验证 "不兼容签名被丢弃" 的副作用(即 thoughtSignature 字段消息)来测试。
       但由于 SignatureCache 是全局的,我们无法在测试中轻易预置状态。
       因此,此场景主要依赖 Verification Guide 中的手动测试。
       或者,我们可以测试 request.rs 中公开的某些 helper (如果有的话),但目前没有。
    */

}