use crate::brain::agent::context::AgentContext;
use crate::brain::provider::custom_openai_compatible::OpenAIProvider;
use crate::brain::provider::{ContentBlock, LLMRequest, Message, Role};
use crate::db::models::Message as DbMessage;
use chrono::Utc;
use uuid::Uuid;
fn assistant_db_row(content: &str, thinking: Option<&str>) -> DbMessage {
DbMessage {
id: Uuid::new_v4(),
session_id: Uuid::new_v4(),
role: "assistant".to_string(),
content: content.to_string(),
sequence: 0,
created_at: Utc::now(),
token_count: None,
cost: None,
input_tokens: None,
thinking: thinking.map(String::from),
}
}
#[test]
fn from_db_messages_rehydrates_thinking_for_assistant_rows() {
let session_id = Uuid::new_v4();
let rows = vec![assistant_db_row(
"Sure, here is the answer.",
Some("The user asked for X, so I reasoned through Y then Z."),
)];
let ctx = AgentContext::from_db_messages(session_id, rows, 200_000);
assert_eq!(ctx.messages.len(), 1);
let msg = &ctx.messages[0];
assert_eq!(msg.role, Role::Assistant);
assert_eq!(
msg.content.len(),
2,
"expected [Thinking, Text]: got {:?}",
msg.content
);
assert!(
matches!(
&msg.content[0],
ContentBlock::Thinking { thinking, .. } if thinking.contains("reasoned through")
),
"first block must be the rehydrated Thinking: got {:?}",
msg.content[0]
);
assert!(
matches!(&msg.content[1], ContentBlock::Text { text } if text == "Sure, here is the answer."),
);
}
#[test]
fn from_db_messages_does_not_duplicate_thinking_when_absent() {
let session_id = Uuid::new_v4();
let rows = vec![assistant_db_row("plain answer, no reasoning saved", None)];
let ctx = AgentContext::from_db_messages(session_id, rows, 200_000);
assert_eq!(ctx.messages.len(), 1);
assert_eq!(ctx.messages[0].content.len(), 1);
assert!(matches!(
&ctx.messages[0].content[0],
ContentBlock::Text { .. }
));
}
#[test]
fn from_db_messages_skips_empty_content_with_no_thinking() {
let session_id = Uuid::new_v4();
let rows = vec![assistant_db_row("", None)];
let ctx = AgentContext::from_db_messages(session_id, rows, 200_000);
assert!(
ctx.messages.is_empty(),
"empty row with no thinking should be dropped"
);
}
#[test]
fn from_db_messages_keeps_row_when_only_thinking_present() {
let session_id = Uuid::new_v4();
let rows = vec![assistant_db_row("", Some("midway thinking"))];
let ctx = AgentContext::from_db_messages(session_id, rows, 200_000);
assert_eq!(ctx.messages.len(), 1);
assert!(matches!(
&ctx.messages[0].content[0],
ContentBlock::Thinking { thinking, .. } if thinking == "midway thinking"
));
}
#[test]
fn from_db_messages_skips_thinking_on_user_rows() {
let session_id = Uuid::new_v4();
let row = DbMessage {
id: Uuid::new_v4(),
session_id,
role: "user".to_string(),
content: "hello".to_string(),
sequence: 0,
created_at: Utc::now(),
token_count: None,
cost: None,
input_tokens: None,
thinking: Some("leaked reasoning".to_string()),
};
let ctx = AgentContext::from_db_messages(session_id, vec![row], 200_000);
assert_eq!(ctx.messages.len(), 1);
assert_eq!(ctx.messages[0].content.len(), 1);
assert!(matches!(
&ctx.messages[0].content[0],
ContentBlock::Text { .. }
));
}
fn opencode_kimi_provider() -> OpenAIProvider {
OpenAIProvider::with_base_url(
"test-key".to_string(),
"https://opencode.ai/zen/go/v1/chat/completions".to_string(),
)
.with_name("opencode-kimi")
}
fn non_kimi_provider() -> OpenAIProvider {
OpenAIProvider::with_base_url(
"test-key".to_string(),
"https://api.z.ai/api/coding/paas/v4/chat/completions".to_string(),
)
.with_name("zhipu")
}
fn assistant_with_tool_call_and_thinking(thinking: Option<&str>) -> Message {
let mut content = Vec::new();
if let Some(t) = thinking {
content.push(ContentBlock::Thinking {
thinking: t.to_string(),
signature: None,
});
}
content.push(ContentBlock::ToolUse {
id: "call_abc123".to_string(),
name: "bash".to_string(),
input: serde_json::json!({"command": "echo hi"}),
});
Message {
role: Role::Assistant,
content,
}
}
#[test]
fn encoder_emits_reasoning_content_from_thinking_block_for_kimi() {
let provider = opencode_kimi_provider();
let req = LLMRequest::new(
"kimi-k2.6".to_string(),
vec![
Message::user("run bash".to_string()),
assistant_with_tool_call_and_thinking(Some(
"User wants a shell command. I'll run echo.",
)),
],
);
let encoded = provider.to_openai_request(req);
let body = serde_json::to_value(&encoded).expect("serialize");
let msgs = body["messages"].as_array().expect("messages array");
let asst = &msgs[1];
assert_eq!(asst["role"], "assistant");
assert!(asst["tool_calls"].is_array());
assert_eq!(
asst["reasoning_content"].as_str(),
Some("User wants a shell command. I'll run echo."),
"reasoning_content must equal the Thinking block text verbatim; got {}",
asst
);
}
#[test]
fn encoder_uses_safety_placeholder_for_kimi_when_no_thinking_block() {
let provider = opencode_kimi_provider();
let req = LLMRequest::new(
"kimi-k2.6".to_string(),
vec![
Message::user("run bash".to_string()),
assistant_with_tool_call_and_thinking(None),
],
);
let encoded = provider.to_openai_request(req);
let body = serde_json::to_value(&encoded).expect("serialize");
let asst = &body["messages"].as_array().unwrap()[1];
let rc = asst["reasoning_content"]
.as_str()
.expect("reasoning_content must be serialized for kimi");
assert!(
!rc.is_empty(),
"reasoning_content must be non-empty for kimi fallback path; got {:?}",
rc
);
}
#[test]
fn encoder_omits_reasoning_content_for_non_kimi_without_thinking() {
let provider = non_kimi_provider();
let req = LLMRequest::new(
"glm-5.1".to_string(),
vec![
Message::user("hello".to_string()),
assistant_with_tool_call_and_thinking(None),
],
);
let encoded = provider.to_openai_request(req);
let body = serde_json::to_value(&encoded).expect("serialize");
let asst = &body["messages"].as_array().unwrap()[1];
assert!(
asst.get("reasoning_content").is_none_or(|v| v.is_null()),
"non-kimi providers must not receive reasoning_content when we have no thinking: got {}",
asst
);
}
#[test]
fn encoder_passes_thinking_through_for_non_kimi_providers_too() {
let provider = non_kimi_provider();
let req = LLMRequest::new(
"glm-5.1".to_string(),
vec![
Message::user("hi".to_string()),
assistant_with_tool_call_and_thinking(Some("actual reasoning")),
],
);
let encoded = provider.to_openai_request(req);
let body = serde_json::to_value(&encoded).expect("serialize");
let asst = &body["messages"].as_array().unwrap()[1];
assert_eq!(asst["reasoning_content"].as_str(), Some("actual reasoning"));
}