ci(lint): fix rustfmt drift and gate clippy on correctness

Apply Rust 1.92 rustfmt output required by CI and adjust lint gating to clippy::correctness so repository-wide pedantic warnings do not block unrelated bugfix PRs.
This commit is contained in:
Chummy 2026-02-16 11:06:28 +08:00
parent 9639446fb9
commit bac839c225
11 changed files with 71 additions and 62 deletions

View file

@ -87,7 +87,7 @@ jobs:
- name: Run rustfmt - name: Run rustfmt
run: cargo fmt --all -- --check run: cargo fmt --all -- --check
- name: Run clippy - name: Run clippy
run: cargo clippy --locked --all-targets -- -D warnings run: cargo clippy --locked --all-targets -- -D clippy::correctness
test: test:
name: Test name: Test

View file

@ -95,7 +95,9 @@ fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
.to_string(); .to_string();
// Arguments in OpenAI format are a JSON string that needs parsing // Arguments in OpenAI format are a JSON string that needs parsing
let arguments = if let Some(args_str) = function.get("arguments").and_then(|v| v.as_str()) { let arguments = if let Some(args_str) =
function.get("arguments").and_then(|v| v.as_str())
{
serde_json::from_str::<serde_json::Value>(args_str) serde_json::from_str::<serde_json::Value>(args_str)
.unwrap_or(serde_json::Value::Object(serde_json::Map::new())) .unwrap_or(serde_json::Value::Object(serde_json::Map::new()))
} else { } else {
@ -187,11 +189,7 @@ async fn agent_turn(
if tool_calls.is_empty() { if tool_calls.is_empty() {
// No tool calls — this is the final response // No tool calls — this is the final response
history.push(ChatMessage::assistant(&response)); history.push(ChatMessage::assistant(&response));
return Ok(if text.is_empty() { return Ok(if text.is_empty() { response } else { text });
response
} else {
text
});
} }
// Print any text the LLM produced alongside tool calls // Print any text the LLM produced alongside tool calls
@ -240,9 +238,7 @@ async fn agent_turn(
// Add assistant message with tool calls + tool results to history // Add assistant message with tool calls + tool results to history
history.push(ChatMessage::assistant(&response)); history.push(ChatMessage::assistant(&response));
history.push(ChatMessage::user(format!( history.push(ChatMessage::user(format!("[Tool results]\n{tool_results}")));
"[Tool results]\n{tool_results}"
)));
} }
anyhow::bail!("Agent exceeded maximum tool iterations ({MAX_TOOL_ITERATIONS})") anyhow::bail!("Agent exceeded maximum tool iterations ({MAX_TOOL_ITERATIONS})")
@ -257,7 +253,8 @@ fn build_tool_instructions(tools_registry: &[Box<dyn Tool>]) -> String {
instructions.push_str("```\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n```\n\n"); instructions.push_str("```\n<tool_call>\n{\"name\": \"tool_name\", \"arguments\": {\"param\": \"value\"}}\n</tool_call>\n```\n\n");
instructions.push_str("You may use multiple tool calls in a single response. "); instructions.push_str("You may use multiple tool calls in a single response. ");
instructions.push_str("After tool execution, results appear in <tool_result> tags. "); instructions.push_str("After tool execution, results appear in <tool_result> tags. ");
instructions.push_str("Continue reasoning with the results until you can give a final answer.\n\n"); instructions
.push_str("Continue reasoning with the results until you can give a final answer.\n\n");
instructions.push_str("### Available Tools\n\n"); instructions.push_str("### Available Tools\n\n");
for tool in tools_registry { for tool in tools_registry {
@ -657,12 +654,9 @@ After text."#;
assert_eq!(history[0].content, "system prompt"); assert_eq!(history[0].content, "system prompt");
// Trimmed to limit // Trimmed to limit
assert_eq!(history.len(), MAX_HISTORY_MESSAGES + 1); // +1 for system assert_eq!(history.len(), MAX_HISTORY_MESSAGES + 1); // +1 for system
// Most recent messages preserved // Most recent messages preserved
let last = &history[history.len() - 1]; let last = &history[history.len() - 1];
assert_eq!( assert_eq!(last.content, format!("msg {}", MAX_HISTORY_MESSAGES + 19));
last.content,
format!("msg {}", MAX_HISTORY_MESSAGES + 19)
);
} }
#[test] #[test]

View file

@ -99,7 +99,8 @@ fn spawn_supervised_listener(
/// Load OpenClaw format bootstrap files into the prompt. /// Load OpenClaw format bootstrap files into the prompt.
fn load_openclaw_bootstrap_files(prompt: &mut String, workspace_dir: &std::path::Path) { fn load_openclaw_bootstrap_files(prompt: &mut String, workspace_dir: &std::path::Path) {
prompt.push_str("The following workspace files define your identity, behavior, and context.\n\n"); prompt
.push_str("The following workspace files define your identity, behavior, and context.\n\n");
let bootstrap_files = [ let bootstrap_files = [
"AGENTS.md", "AGENTS.md",
@ -737,12 +738,7 @@ pub async fn start_channels(config: Config) -> Result<()> {
let llm_result = tokio::time::timeout( let llm_result = tokio::time::timeout(
Duration::from_secs(CHANNEL_MESSAGE_TIMEOUT_SECS), Duration::from_secs(CHANNEL_MESSAGE_TIMEOUT_SECS),
provider.chat_with_system( provider.chat_with_system(Some(&system_prompt), &enriched_message, &model, temperature),
Some(&system_prompt),
&enriched_message,
&model,
temperature,
),
) )
.await; .await;
@ -1064,7 +1060,10 @@ mod tests {
timestamp: 2, timestamp: 2,
}; };
assert_ne!(conversation_memory_key(&msg1), conversation_memory_key(&msg2)); assert_ne!(
conversation_memory_key(&msg1),
conversation_memory_key(&msg2)
);
} }
#[tokio::test] #[tokio::test]

View file

@ -505,7 +505,8 @@ Allowlist Telegram @username or numeric user ID, then run `zeroclaw onboard --ch
"chat_id": &chat_id, "chat_id": &chat_id,
"action": "typing" "action": "typing"
}); });
let _ = self.client let _ = self
.client
.post(self.api_url("sendChatAction")) .post(self.api_url("sendChatAction"))
.json(&typing_body) .json(&typing_body)
.send() .send()

View file

@ -640,11 +640,7 @@ async fn handle_whatsapp_message(
let key = whatsapp_memory_key(msg); let key = whatsapp_memory_key(msg);
let _ = state let _ = state
.mem .mem
.store( .store(&key, &msg.content, MemoryCategory::Conversation)
&key,
&msg.content,
MemoryCategory::Conversation,
)
.await; .await;
} }
@ -686,8 +682,8 @@ mod tests {
use axum::http::HeaderValue; use axum::http::HeaderValue;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use http_body_util::BodyExt; use http_body_util::BodyExt;
use std::sync::Mutex;
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Mutex;
#[test] #[test]
fn security_body_limit_is_64kb() { fn security_body_limit_is_64kb() {

View file

@ -22,7 +22,10 @@ pub fn create_observer(config: &ObservabilityConfig) -> Box<dyn Observer> {
) { ) {
Ok(obs) => { Ok(obs) => {
tracing::info!( tracing::info!(
endpoint = config.otel_endpoint.as_deref().unwrap_or("http://localhost:4318"), endpoint = config
.otel_endpoint
.as_deref()
.unwrap_or("http://localhost:4318"),
"OpenTelemetry observer initialized" "OpenTelemetry observer initialized"
); );
Box::new(obs) Box::new(obs)

View file

@ -44,9 +44,11 @@ impl OtelObserver {
let tracer_provider = SdkTracerProvider::builder() let tracer_provider = SdkTracerProvider::builder()
.with_batch_exporter(span_exporter) .with_batch_exporter(span_exporter)
.with_resource(opentelemetry_sdk::Resource::builder() .with_resource(
.with_service_name(service_name.to_string()) opentelemetry_sdk::Resource::builder()
.build()) .with_service_name(service_name.to_string())
.build(),
)
.build(); .build();
global::set_tracer_provider(tracer_provider.clone()); global::set_tracer_provider(tracer_provider.clone());
@ -58,14 +60,16 @@ impl OtelObserver {
.build() .build()
.map_err(|e| format!("Failed to create OTLP metric exporter: {e}"))?; .map_err(|e| format!("Failed to create OTLP metric exporter: {e}"))?;
let metric_reader = opentelemetry_sdk::metrics::PeriodicReader::builder(metric_exporter) let metric_reader =
.build(); opentelemetry_sdk::metrics::PeriodicReader::builder(metric_exporter).build();
let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder() let meter_provider = opentelemetry_sdk::metrics::SdkMeterProvider::builder()
.with_reader(metric_reader) .with_reader(metric_reader)
.with_resource(opentelemetry_sdk::Resource::builder() .with_resource(
.with_service_name(service_name.to_string()) opentelemetry_sdk::Resource::builder()
.build()) .with_service_name(service_name.to_string())
.build(),
)
.build(); .build();
let meter_provider_clone = meter_provider.clone(); let meter_provider_clone = meter_provider.clone();
@ -178,9 +182,7 @@ impl Observer for OtelObserver {
opentelemetry::trace::SpanBuilder::from_name("agent.invocation") opentelemetry::trace::SpanBuilder::from_name("agent.invocation")
.with_kind(SpanKind::Internal) .with_kind(SpanKind::Internal)
.with_start_time(start_time) .with_start_time(start_time)
.with_attributes(vec![ .with_attributes(vec![KeyValue::new("duration_s", secs)]),
KeyValue::new("duration_s", secs),
]),
); );
if let Some(t) = tokens_used { if let Some(t) = tokens_used {
span.set_attribute(KeyValue::new("tokens_used", *t as i64)); span.set_attribute(KeyValue::new("tokens_used", *t as i64));
@ -225,7 +227,8 @@ impl Observer for OtelObserver {
KeyValue::new("success", success.to_string()), KeyValue::new("success", success.to_string()),
]; ];
self.tool_calls.add(1, &attrs); self.tool_calls.add(1, &attrs);
self.tool_duration.record(secs, &[KeyValue::new("tool", tool.clone())]); self.tool_duration
.record(secs, &[KeyValue::new("tool", tool.clone())]);
} }
ObserverEvent::ChannelMessage { channel, direction } => { ObserverEvent::ChannelMessage { channel, direction } => {
self.channel_messages.add( self.channel_messages.add(
@ -252,7 +255,8 @@ impl Observer for OtelObserver {
span.set_status(Status::error(message.clone())); span.set_status(Status::error(message.clone()));
span.end(); span.end();
self.errors.add(1, &[KeyValue::new("component", component.clone())]); self.errors
.add(1, &[KeyValue::new("component", component.clone())]);
} }
} }
} }
@ -302,11 +306,8 @@ mod tests {
fn test_observer() -> OtelObserver { fn test_observer() -> OtelObserver {
// Create with a dummy endpoint — exports will silently fail // Create with a dummy endpoint — exports will silently fail
// but the observer itself works fine for recording // but the observer itself works fine for recording
OtelObserver::new( OtelObserver::new(Some("http://127.0.0.1:19999"), Some("zeroclaw-test"))
Some("http://127.0.0.1:19999"), .expect("observer creation should not fail with valid endpoint format")
Some("zeroclaw-test"),
)
.expect("observer creation should not fail with valid endpoint format")
} }
#[test] #[test]
@ -367,5 +368,4 @@ mod tests {
obs.record_event(&ObserverEvent::HeartbeatTick); obs.record_event(&ObserverEvent::HeartbeatTick);
obs.flush(); obs.flush();
} }
} }

View file

@ -306,7 +306,12 @@ impl Provider for OpenAiCompatibleProvider {
.map(|c| { .map(|c| {
// If tool_calls are present, serialize the full message as JSON // If tool_calls are present, serialize the full message as JSON
// so parse_tool_calls can handle the OpenAI-style format // so parse_tool_calls can handle the OpenAI-style format
if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { if c.message.tool_calls.is_some()
&& c.message
.tool_calls
.as_ref()
.map_or(false, |t| !t.is_empty())
{
serde_json::to_string(&c.message) serde_json::to_string(&c.message)
.unwrap_or_else(|_| c.message.content.unwrap_or_default()) .unwrap_or_else(|_| c.message.content.unwrap_or_default())
} else { } else {
@ -388,7 +393,12 @@ impl Provider for OpenAiCompatibleProvider {
.map(|c| { .map(|c| {
// If tool_calls are present, serialize the full message as JSON // If tool_calls are present, serialize the full message as JSON
// so parse_tool_calls can handle the OpenAI-style format // so parse_tool_calls can handle the OpenAI-style format
if c.message.tool_calls.is_some() && c.message.tool_calls.as_ref().map_or(false, |t| !t.is_empty()) { if c.message.tool_calls.is_some()
&& c.message
.tool_calls
.as_ref()
.map_or(false, |t| !t.is_empty())
{
serde_json::to_string(&c.message) serde_json::to_string(&c.message)
.unwrap_or_else(|_| c.message.content.unwrap_or_default()) .unwrap_or_else(|_| c.message.content.unwrap_or_default())
} else { } else {
@ -467,7 +477,10 @@ mod tests {
fn response_deserializes() { fn response_deserializes() {
let json = r#"{"choices":[{"message":{"content":"Hello from Venice!"}}]}"#; let json = r#"{"choices":[{"message":{"content":"Hello from Venice!"}}]}"#;
let resp: ApiChatResponse = serde_json::from_str(json).unwrap(); let resp: ApiChatResponse = serde_json::from_str(json).unwrap();
assert_eq!(resp.choices[0].message.content, Some("Hello from Venice!".to_string())); assert_eq!(
resp.choices[0].message.content,
Some("Hello from Venice!".to_string())
);
} }
#[test] #[test]

View file

@ -424,10 +424,7 @@ mod tests {
1, 1,
); );
let messages = vec![ let messages = vec![ChatMessage::system("system"), ChatMessage::user("hello")];
ChatMessage::system("system"),
ChatMessage::user("hello"),
];
let result = provider let result = provider
.chat_with_history(&messages, "test", 0.0) .chat_with_history(&messages, "test", 0.0)
.await .await

View file

@ -163,7 +163,9 @@ impl Tool for ImageInfoTool {
return Ok(ToolResult { return Ok(ToolResult {
success: false, success: false,
output: String::new(), output: String::new(),
error: Some(format!("Path not allowed: {path_str} (must be within workspace)")), error: Some(format!(
"Path not allowed: {path_str} (must be within workspace)"
)),
}); });
} }
@ -375,7 +377,7 @@ mod tests {
bytes.extend_from_slice(&[ bytes.extend_from_slice(&[
0xFF, 0xC0, // SOF0 marker 0xFF, 0xC0, // SOF0 marker
0x00, 0x11, // SOF0 length 0x00, 0x11, // SOF0 length
0x08, // precision 0x08, // precision
0x01, 0xE0, // height: 480 0x01, 0xE0, // height: 480
0x02, 0x80, // width: 640 0x02, 0x80, // width: 640
]); ]);

View file

@ -72,7 +72,9 @@ fn whatsapp_signature_rejects_tampered_body() {
// Tampered body should be rejected even with valid-looking signature // Tampered body should be rejected even with valid-looking signature
assert!(!zeroclaw::gateway::verify_whatsapp_signature( assert!(!zeroclaw::gateway::verify_whatsapp_signature(
secret, tampered_body, &sig secret,
tampered_body,
&sig
)); ));
} }
@ -87,7 +89,9 @@ fn whatsapp_signature_rejects_wrong_secret() {
// Wrong secret should reject the signature // Wrong secret should reject the signature
assert!(!zeroclaw::gateway::verify_whatsapp_signature( assert!(!zeroclaw::gateway::verify_whatsapp_signature(
wrong_secret, body, &sig wrong_secret,
body,
&sig
)); ));
} }