Addressed clippy lint issues

This commit is contained in:
Simian Astronaut 7 2026-03-10 01:48:19 -04:00
parent 08d6959e0d
commit 7ef9d8a7b5
27 changed files with 140 additions and 134 deletions

View File

@ -387,7 +387,7 @@ fn parse_tool_call_value(value: &serde_json::Value) -> Option<ParsedToolCall> {
return Some(ParsedToolCall {
name,
arguments,
tool_call_id: tool_call_id,
tool_call_id,
});
}
}
@ -409,7 +409,7 @@ fn parse_tool_call_value(value: &serde_json::Value) -> Option<ParsedToolCall> {
Some(ParsedToolCall {
name,
arguments,
tool_call_id: tool_call_id,
tool_call_id,
})
}
@ -2625,15 +2625,13 @@ pub(crate) async fn run_tool_call_loop(
ordered_results[*idx] = Some((call.name.clone(), call.tool_call_id.clone(), outcome));
}
for entry in ordered_results {
if let Some((tool_name, tool_call_id, outcome)) = entry {
individual_results.push((tool_call_id, outcome.output.clone()));
let _ = writeln!(
tool_results,
"<tool_result name=\"{}\">\n{}\n</tool_result>",
tool_name, outcome.output
);
}
for (tool_name, tool_call_id, outcome) in ordered_results.into_iter().flatten() {
individual_results.push((tool_call_id, outcome.output.clone()));
let _ = writeln!(
tool_results,
"<tool_result name=\"{}\">\n{}\n</tool_result>",
tool_name, outcome.output
);
}
// Add assistant message with tool calls + tool results to history.

View File

@ -5,6 +5,7 @@ use parking_lot::Mutex;
use reqwest::multipart::{Form, Part};
use serde_json::json;
use std::collections::HashMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use tokio_tungstenite::tungstenite::Message;
use uuid::Uuid;
@ -370,6 +371,7 @@ fn pick_uniform_index(len: usize) -> usize {
loop {
let value = rand::random::<u64>();
if value < reject_threshold {
#[allow(clippy::cast_possible_truncation)]
return (value % upper) as usize;
}
}
@ -391,7 +393,7 @@ fn encode_emoji_for_discord(emoji: &str) -> String {
let mut encoded = String::new();
for byte in emoji.as_bytes() {
encoded.push_str(&format!("%{byte:02X}"));
let _ = write!(encoded, "%{byte:02X}");
}
encoded
}
@ -1368,7 +1370,10 @@ mod tests {
#[test]
fn split_message_many_short_lines() {
// Many short lines should be batched into chunks under the limit
let msg: String = (0..500).map(|i| format!("line {i}\n")).collect();
let msg: String = (0..500).fold(String::new(), |mut acc, i| {
let _ = writeln!(acc, "line {i}");
acc
});
let parts = split_message_for_discord(&msg);
for part in &parts {
assert!(

View File

@ -1295,9 +1295,7 @@ fn sanitize_tool_json_value(
return None;
}
let Some(object) = value.as_object() else {
return None;
};
let object = value.as_object()?;
if let Some(tool_calls) = object.get("tool_calls").and_then(|value| value.as_array()) {
if !tool_calls.is_empty()
@ -1337,7 +1335,7 @@ fn strip_isolated_tool_json_artifacts(message: &str, known_tool_names: &HashSet<
let mut saw_tool_call_payload = false;
while cursor < message.len() {
let Some(rel_start) = message[cursor..].find(|ch: char| ch == '{' || ch == '[') else {
let Some(rel_start) = message[cursor..].find(['{', '[']) else {
cleaned.push_str(&message[cursor..]);
break;
};
@ -2682,7 +2680,7 @@ struct ConfiguredChannel {
fn collect_configured_channels(
config: &Config,
_matrix_skip_context: &str,
matrix_skip_context: &str,
) -> Vec<ConfiguredChannel> {
let mut channels = Vec::new();
@ -2767,7 +2765,7 @@ fn collect_configured_channels(
if config.channels_config.matrix.is_some() {
tracing::warn!(
"Matrix channel is configured but this build was compiled without `channel-matrix`; skipping Matrix {}.",
_matrix_skip_context
matrix_skip_context
);
}

View File

@ -429,7 +429,7 @@ mod tests {
"message": {
"actorType": "users",
"actorId": "user_a",
"timestamp": 1735701200123u64,
"timestamp": 1_735_701_200_123_u64,
"message": "hello"
}
});

View File

@ -6,6 +6,7 @@ use async_trait::async_trait;
use directories::UserDirs;
use parking_lot::Mutex;
use reqwest::multipart::{Form, Part};
use std::fmt::Write as _;
use std::path::Path;
use std::sync::{Arc, RwLock};
use std::time::Duration;
@ -100,6 +101,7 @@ fn pick_uniform_index(len: usize) -> usize {
loop {
let value = rand::random::<u64>();
if value < reject_threshold {
#[allow(clippy::cast_possible_truncation)]
return (value % upper) as usize;
}
}
@ -1248,7 +1250,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if self.mention_only && is_group {
let bot_username = self.bot_username.lock();
if let Some(ref bot_username) = *bot_username {
if !Self::contains_bot_mention(&text, bot_username) {
if !Self::contains_bot_mention(text, bot_username) {
return None;
}
} else {
@ -1283,7 +1285,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
let content = if self.mention_only && is_group {
let bot_username = self.bot_username.lock();
let bot_username = bot_username.as_ref()?;
Self::normalize_incoming_content(&text, bot_username)?
Self::normalize_incoming_content(text, bot_username)?
} else {
text.to_string()
};
@ -1391,7 +1393,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if i + 1 < len && bytes[i] == b'*' && bytes[i + 1] == b'*' {
if let Some(end) = line[i + 2..].find("**") {
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
line_out.push_str(&format!("<b>{inner}</b>"));
let _ = write!(line_out, "<b>{inner}</b>");
i += 4 + end;
continue;
}
@ -1399,7 +1401,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if i + 1 < len && bytes[i] == b'_' && bytes[i + 1] == b'_' {
if let Some(end) = line[i + 2..].find("__") {
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
line_out.push_str(&format!("<b>{inner}</b>"));
let _ = write!(line_out, "<b>{inner}</b>");
i += 4 + end;
continue;
}
@ -1409,7 +1411,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if let Some(end) = line[i + 1..].find('*') {
if end > 0 {
let inner = Self::escape_html(&line[i + 1..i + 1 + end]);
line_out.push_str(&format!("<i>{inner}</i>"));
let _ = write!(line_out, "<i>{inner}</i>");
i += 2 + end;
continue;
}
@ -1419,7 +1421,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if bytes[i] == b'`' && (i == 0 || bytes[i - 1] != b'`') {
if let Some(end) = line[i + 1..].find('`') {
let inner = Self::escape_html(&line[i + 1..i + 1 + end]);
line_out.push_str(&format!("<code>{inner}</code>"));
let _ = write!(line_out, "<code>{inner}</code>");
i += 2 + end;
continue;
}
@ -1435,9 +1437,8 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if url.starts_with("http://") || url.starts_with("https://") {
let text_html = Self::escape_html(text_part);
let url_html = Self::escape_html(url);
line_out.push_str(&format!(
"<a href=\"{url_html}\">{text_html}</a>"
));
let _ =
write!(line_out, "<a href=\"{url_html}\">{text_html}</a>");
i = after_bracket + 1 + paren_end + 1;
continue;
}
@ -1449,7 +1450,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
if i + 1 < len && bytes[i] == b'~' && bytes[i + 1] == b'~' {
if let Some(end) = line[i + 2..].find("~~") {
let inner = Self::escape_html(&line[i + 2..i + 2 + end]);
line_out.push_str(&format!("<s>{inner}</s>"));
let _ = write!(line_out, "<s>{inner}</s>");
i += 4 + end;
continue;
}
@ -1478,14 +1479,14 @@ Allowlist Telegram username (without '@') or numeric user ID.",
for line in joined.split('\n') {
let trimmed = line.trim();
if trimmed.starts_with("```") {
if !in_code_block {
in_code_block = true;
code_buf.clear();
} else {
if in_code_block {
in_code_block = false;
let escaped = code_buf.trim_end_matches('\n');
// Telegram HTML parse mode supports <pre> and <code>, but not class attributes.
final_out.push_str(&format!("<pre><code>{escaped}</code></pre>\n"));
let _ = writeln!(final_out, "<pre><code>{escaped}</code></pre>");
code_buf.clear();
} else {
in_code_block = true;
code_buf.clear();
}
} else if in_code_block {
@ -1497,10 +1498,7 @@ Allowlist Telegram username (without '@') or numeric user ID.",
}
}
if in_code_block && !code_buf.is_empty() {
final_out.push_str(&format!(
"<pre><code>{}</code></pre>\n",
code_buf.trim_end()
));
let _ = writeln!(final_out, "<pre><code>{}</code></pre>", code_buf.trim_end());
}
final_out.trim_end_matches('\n').to_string()
@ -2731,7 +2729,7 @@ mod tests {
"update_id": 1,
"message": {
"message_id": 99,
"chat": { "id": -100123456 }
"chat": { "id": -100_123_456 }
}
});
@ -3824,7 +3822,10 @@ mod tests {
#[test]
fn telegram_split_many_short_lines() {
let msg: String = (0..1000).map(|i| format!("line {i}\n")).collect();
let msg: String = (0..1000).fold(String::new(), |mut acc, i| {
let _ = writeln!(acc, "line {i}");
acc
});
let parts = split_message_for_telegram(&msg);
for part in &parts {
assert!(
@ -4130,7 +4131,7 @@ mod tests {
/// Skipped automatically when `GROQ_API_KEY` is not set.
/// Run: `GROQ_API_KEY=<key> cargo test --lib -- telegram::tests::e2e_live_voice_transcription_and_reply_cache --ignored`
#[tokio::test]
#[ignore]
#[ignore = "requires GROQ_API_KEY environment variable"]
async fn e2e_live_voice_transcription_and_reply_cache() {
if std::env::var("GROQ_API_KEY").is_err() {
eprintln!("GROQ_API_KEY not set — skipping live voice transcription test");

View File

@ -335,7 +335,7 @@ mod tests {
"text": "Hello from WATI!",
"waId": "1234567890",
"fromMe": false,
"timestamp": 1705320000u64
"timestamp": 1_705_320_000_u64
});
let msgs = ch.parse_webhook_payload(&payload);
@ -344,7 +344,7 @@ mod tests {
assert_eq!(msgs[0].content, "Hello from WATI!");
assert_eq!(msgs[0].channel, "wati");
assert_eq!(msgs[0].reply_target, "+1234567890");
assert_eq!(msgs[0].timestamp, 1705320000);
assert_eq!(msgs[0].timestamp, 1_705_320_000);
}
#[test]
@ -381,7 +381,7 @@ mod tests {
"message": { "body": "Alt field test" },
"wa_id": "1234567890",
"from_me": false,
"timestamp": 1705320000u64
"timestamp": 1_705_320_000_u64
});
let msgs = ch.parse_webhook_payload(&payload);
@ -396,11 +396,11 @@ mod tests {
let payload = serde_json::json!({
"text": "Test",
"waId": "1234567890",
"timestamp": 1705320000u64
"timestamp": 1_705_320_000_u64
});
let msgs = ch.parse_webhook_payload(&payload);
assert_eq!(msgs[0].timestamp, 1705320000);
assert_eq!(msgs[0].timestamp, 1_705_320_000);
}
#[test]
@ -409,11 +409,11 @@ mod tests {
let payload = serde_json::json!({
"text": "Test",
"waId": "1234567890",
"timestamp": 1705320000000u64
"timestamp": 1_705_320_000_000_u64
});
let msgs = ch.parse_webhook_payload(&payload);
assert_eq!(msgs[0].timestamp, 1705320000);
assert_eq!(msgs[0].timestamp, 1_705_320_000);
}
#[test]
@ -426,7 +426,7 @@ mod tests {
});
let msgs = ch.parse_webhook_payload(&payload);
assert_eq!(msgs[0].timestamp, 1736942400);
assert_eq!(msgs[0].timestamp, 1_736_942_400);
}
#[test]

View File

@ -19,7 +19,7 @@ pub use schema::{
WebFetchConfig, WebSearchConfig, WebhookConfig,
};
pub fn name_and_presence<T: traits::ChannelConfig>(channel: &Option<T>) -> (&'static str, bool) {
pub fn name_and_presence<T: traits::ChannelConfig>(channel: Option<&T>) -> (&'static str, bool) {
(T::name(), channel.is_some())
}

View File

@ -457,7 +457,7 @@ fn parse_skills_prompt_injection_mode(raw: &str) -> Option<SkillsPromptInjection
}
/// Skills loading configuration (`[skills]` section).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct SkillsConfig {
/// Enable loading and syncing the community open-skills repository.
/// Default: `false` (opt-in).
@ -473,16 +473,6 @@ pub struct SkillsConfig {
pub prompt_injection_mode: SkillsPromptInjectionMode,
}
impl Default for SkillsConfig {
fn default() -> Self {
Self {
open_skills_enabled: false,
open_skills_dir: None,
prompt_injection_mode: SkillsPromptInjectionMode::default(),
}
}
}
/// Multimodal (image) handling configuration (`[multimodal]` section).
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct MultimodalConfig {
@ -1940,20 +1930,12 @@ impl Default for HooksConfig {
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema, Default)]
pub struct BuiltinHooksConfig {
/// Enable the command-logger hook (logs tool calls for auditing).
pub command_logger: bool,
}
impl Default for BuiltinHooksConfig {
fn default() -> Self {
Self {
command_logger: false,
}
}
}
// ── Autonomy / Security ──────────────────────────────────────────
/// Autonomy and security policy configuration (`[autonomy]` section).
@ -2528,7 +2510,7 @@ pub struct CustomTunnelConfig {
struct ConfigWrapper<T: ChannelConfig>(std::marker::PhantomData<T>);
impl<T: ChannelConfig> ConfigWrapper<T> {
fn new(_: &Option<T>) -> Self {
fn new(_: Option<&T>) -> Self {
Self(std::marker::PhantomData)
}
}
@ -2604,79 +2586,79 @@ impl ChannelsConfig {
pub fn channels_except_webhook(&self) -> Vec<(Box<dyn super::traits::ConfigHandle>, bool)> {
vec![
(
Box::new(ConfigWrapper::new(&self.telegram)),
Box::new(ConfigWrapper::new(self.telegram.as_ref())),
self.telegram.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.discord)),
Box::new(ConfigWrapper::new(self.discord.as_ref())),
self.discord.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.slack)),
Box::new(ConfigWrapper::new(self.slack.as_ref())),
self.slack.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.mattermost)),
Box::new(ConfigWrapper::new(self.mattermost.as_ref())),
self.mattermost.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.imessage)),
Box::new(ConfigWrapper::new(self.imessage.as_ref())),
self.imessage.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.matrix)),
Box::new(ConfigWrapper::new(self.matrix.as_ref())),
self.matrix.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.signal)),
Box::new(ConfigWrapper::new(self.signal.as_ref())),
self.signal.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.whatsapp)),
Box::new(ConfigWrapper::new(self.whatsapp.as_ref())),
self.whatsapp.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.linq)),
Box::new(ConfigWrapper::new(self.linq.as_ref())),
self.linq.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.wati)),
Box::new(ConfigWrapper::new(self.wati.as_ref())),
self.wati.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.nextcloud_talk)),
Box::new(ConfigWrapper::new(self.nextcloud_talk.as_ref())),
self.nextcloud_talk.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.email)),
Box::new(ConfigWrapper::new(self.email.as_ref())),
self.email.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.irc)),
Box::new(ConfigWrapper::new(self.irc.as_ref())),
self.irc.is_some()
),
(
Box::new(ConfigWrapper::new(&self.lark)),
Box::new(ConfigWrapper::new(self.lark.as_ref())),
self.lark.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.feishu)),
Box::new(ConfigWrapper::new(self.feishu.as_ref())),
self.feishu.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.dingtalk)),
Box::new(ConfigWrapper::new(self.dingtalk.as_ref())),
self.dingtalk.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.qq)),
Box::new(ConfigWrapper::new(self.qq.as_ref())),
self.qq.is_some()
),
(
Box::new(ConfigWrapper::new(&self.nostr)),
Box::new(ConfigWrapper::new(self.nostr.as_ref())),
self.nostr.is_some(),
),
(
Box::new(ConfigWrapper::new(&self.clawdtalk)),
Box::new(ConfigWrapper::new(self.clawdtalk.as_ref())),
self.clawdtalk.is_some(),
),
]
@ -2685,7 +2667,7 @@ impl ChannelsConfig {
pub fn channels(&self) -> Vec<(Box<dyn super::traits::ConfigHandle>, bool)> {
let mut ret = self.channels_except_webhook();
ret.push((
Box::new(ConfigWrapper::new(&self.webhook)),
Box::new(ConfigWrapper::new(self.webhook.as_ref())),
self.webhook.is_some(),
));
ret
@ -4823,7 +4805,7 @@ async fn sync_directory(path: &Path) -> Result<()> {
dir.sync_all()
.await
.with_context(|| format!("Failed to fsync directory metadata: {}", path.display()))?;
return Ok(());
Ok(())
}
#[cfg(not(unix))]

View File

@ -53,8 +53,7 @@ async fn handle_socket(socket: WebSocket, state: AppState) {
while let Some(msg) = receiver.next().await {
let msg = match msg {
Ok(Message::Text(text)) => text,
Ok(Message::Close(_)) => break,
Err(_) => break,
Ok(Message::Close(_)) | Err(_) => break,
_ => continue,
};

View File

@ -109,7 +109,7 @@ pub fn handle_command(cmd: crate::HardwareCommands, _config: &Config) -> Result<
let _ = &cmd;
println!("Hardware discovery requires the 'hardware' feature.");
println!("Build with: cargo build --features hardware");
return Ok(());
Ok(())
}
#[cfg(all(

View File

@ -766,8 +766,7 @@ async fn main() -> Result<()> {
}
match cli.command {
Commands::Onboard { .. } => unreachable!(),
Commands::Completions { .. } => unreachable!(),
Commands::Onboard { .. } | Commands::Completions { .. } => unreachable!(),
Commands::Agent {
message,

View File

@ -328,7 +328,14 @@ fn date_prefix(filename: &str) -> Option<NaiveDate> {
if filename.len() < 10 {
return None;
}
NaiveDate::parse_from_str(&filename[..filename.floor_char_boundary(10)], "%Y-%m-%d").ok()
let boundary = {
let mut i = 10.min(filename.len());
while i > 0 && !filename.is_char_boundary(i) {
i -= 1;
}
i
};
NaiveDate::parse_from_str(&filename[..boundary], "%Y-%m-%d").ok()
}
fn is_older_than(path: &Path, cutoff: SystemTime) -> bool {

View File

@ -22,7 +22,6 @@ pub enum RuntimeTraceStorageMode {
impl RuntimeTraceStorageMode {
fn from_raw(raw: &str) -> Self {
match raw.trim().to_ascii_lowercase().as_str() {
"none" => Self::None,
"rolling" => Self::Rolling,
"full" => Self::Full,
_ => Self::None,

View File

@ -2004,7 +2004,7 @@ fn resolve_interactive_onboarding_mode(
" Existing config found at {}. Select setup mode",
config_path.display()
))
.items(&options)
.items(options)
.default(1)
.interact()?;
@ -5814,7 +5814,7 @@ mod tests {
apply_provider_update(
&mut config,
"anthropic".to_string(),
"".to_string(),
String::new(),
"claude-sonnet-4-5-20250929".to_string(),
None,
);

View File

@ -229,6 +229,7 @@ pub async fn create_peripheral_tools(config: &PeripheralsConfig) -> Result<Vec<B
}
#[cfg(not(feature = "hardware"))]
#[allow(clippy::unused_async)]
pub async fn create_peripheral_tools(_config: &PeripheralsConfig) -> Result<Vec<Box<dyn Tool>>> {
Ok(Vec::new())
}

View File

@ -697,7 +697,6 @@ impl BedrockProvider {
let after_semi = &rest[semi + 1..];
if let Some(b64) = after_semi.strip_prefix("base64,") {
let format = match mime {
"image/jpeg" | "image/jpg" => "jpeg",
"image/png" => "png",
"image/gif" => "gif",
"image/webp" => "webp",

View File

@ -281,7 +281,6 @@ fn clamp_reasoning_effort(model: &str, effort: &str) -> String {
return match effort {
"low" | "medium" | "high" => effort.to_string(),
"minimal" => "low".to_string(),
"xhigh" => "high".to_string(),
_ => "high".to_string(),
};
}

View File

@ -311,7 +311,7 @@ mod tests {
assert!(patterns.iter().any(|p| p.contains("Stripe")));
assert!(redacted.contains("[REDACTED"));
}
_ => panic!("Should detect Stripe key"),
LeakResult::Clean => panic!("Should detect Stripe key"),
}
}
@ -324,7 +324,7 @@ mod tests {
LeakResult::Detected { patterns, .. } => {
assert!(patterns.iter().any(|p| p.contains("AWS")));
}
_ => panic!("Should detect AWS key"),
LeakResult::Clean => panic!("Should detect AWS key"),
}
}
@ -342,7 +342,7 @@ MIIEowIBAAKCAQEA0ZPr5JeyVDonXsKhfq...
assert!(patterns.iter().any(|p| p.contains("private key")));
assert!(redacted.contains("[REDACTED_PRIVATE_KEY]"));
}
_ => panic!("Should detect private key"),
LeakResult::Clean => panic!("Should detect private key"),
}
}
@ -356,7 +356,7 @@ MIIEowIBAAKCAQEA0ZPr5JeyVDonXsKhfq...
assert!(patterns.iter().any(|p| p.contains("JWT")));
assert!(redacted.contains("[REDACTED_JWT]"));
}
_ => panic!("Should detect JWT"),
LeakResult::Clean => panic!("Should detect JWT"),
}
}
@ -369,7 +369,7 @@ MIIEowIBAAKCAQEA0ZPr5JeyVDonXsKhfq...
LeakResult::Detected { patterns, .. } => {
assert!(patterns.iter().any(|p| p.contains("PostgreSQL")));
}
_ => panic!("Should detect database URL"),
LeakResult::Clean => panic!("Should detect database URL"),
}
}

View File

@ -200,12 +200,12 @@ fn audit_manifest_file(root: &Path, path: &Path, report: &mut SkillAuditReport)
.push(format!("{rel}: tools[{idx}] is missing a command field."));
}
if kind.eq_ignore_ascii_case("script") || kind.eq_ignore_ascii_case("shell") {
if command.is_some_and(|value| value.trim().is_empty()) {
report
.findings
.push(format!("{rel}: tools[{idx}] has an empty {kind} command."));
}
if (kind.eq_ignore_ascii_case("script") || kind.eq_ignore_ascii_case("shell"))
&& command.is_some_and(|value| value.trim().is_empty())
{
report
.findings
.push(format!("{rel}: tools[{idx}] has an empty {kind} command."));
}
}
}

View File

@ -238,7 +238,7 @@ fn open_skills_enabled_from_sources(
env_override: Option<&str>,
) -> bool {
if let Some(raw) = env_override {
if let Some(enabled) = parse_open_skills_enabled(&raw) {
if let Some(enabled) = parse_open_skills_enabled(raw) {
return enabled;
}
if !raw.trim().is_empty() {

View File

@ -173,7 +173,11 @@ impl ScreenshotTool {
let size = bytes.len();
let mut encoded = base64::engine::general_purpose::STANDARD.encode(&bytes);
let truncated = if encoded.len() > MAX_BASE64_BYTES {
encoded.truncate(encoded.floor_char_boundary(MAX_BASE64_BYTES));
let mut boundary = MAX_BASE64_BYTES.min(encoded.len());
while boundary > 0 && !encoded.is_char_boundary(boundary) {
boundary -= 1;
}
encoded.truncate(boundary);
true
} else {
false

View File

@ -164,11 +164,19 @@ impl Tool for ShellTool {
// Truncate output to prevent OOM
if stdout.len() > MAX_OUTPUT_BYTES {
stdout.truncate(stdout.floor_char_boundary(MAX_OUTPUT_BYTES));
let mut b = MAX_OUTPUT_BYTES.min(stdout.len());
while b > 0 && !stdout.is_char_boundary(b) {
b -= 1;
}
stdout.truncate(b);
stdout.push_str("\n... [output truncated at 1MB]");
}
if stderr.len() > MAX_OUTPUT_BYTES {
stderr.truncate(stderr.floor_char_boundary(MAX_OUTPUT_BYTES));
let mut b = MAX_OUTPUT_BYTES.min(stderr.len());
while b > 0 && !stderr.is_char_boundary(b) {
b -= 1;
}
stderr.truncate(b);
stderr.push_str("\n... [stderr truncated at 1MB]");
}

View File

@ -119,10 +119,12 @@ fn memory_config_default_vector_keyword_weights_sum_to_one() {
#[test]
fn config_toml_roundtrip_preserves_provider() {
let mut config = Config::default();
config.default_provider = Some("deepseek".into());
config.default_model = Some("deepseek-chat".into());
config.default_temperature = 0.5;
let config = Config {
default_provider: Some("deepseek".into()),
default_model: Some("deepseek-chat".into()),
default_temperature: 0.5,
..Default::default()
};
let toml_str = toml::to_string(&config).expect("config should serialize to TOML");
let parsed: Config = toml::from_str(&toml_str).expect("TOML should deserialize back");
@ -173,9 +175,11 @@ fn config_file_write_read_roundtrip() {
let tmp = tempfile::TempDir::new().expect("tempdir creation should succeed");
let config_path = tmp.path().join("config.toml");
let mut config = Config::default();
config.default_provider = Some("mistral".into());
config.default_model = Some("mistral-large".into());
let mut config = Config {
default_provider: Some("mistral".into()),
default_model: Some("mistral-large".into()),
..Default::default()
};
config.agent.max_tool_iterations = 15;
let toml_str = toml::to_string(&config).expect("config should serialize");

View File

@ -99,11 +99,13 @@ fn gateway_config_idempotency_defaults() {
#[test]
fn gateway_config_toml_roundtrip() {
let mut gw = GatewayConfig::default();
gw.port = 8080;
gw.host = "0.0.0.0".into();
gw.require_pairing = false;
gw.pair_rate_limit_per_minute = 5;
let gw = GatewayConfig {
port: 8080,
host: "0.0.0.0".into(),
require_pairing: false,
pair_rate_limit_per_minute: 5,
..Default::default()
};
let toml_str = toml::to_string(&gw).expect("gateway config should serialize");
let parsed: GatewayConfig = toml::from_str(&toml_str).expect("should deserialize back");

View File

@ -4,6 +4,7 @@
//! 1. Primary provider (OpenAI Codex) fails
//! 2. Fallback to Gemini is triggered
//! 3. Gemini OAuth tokens are expired (we manually expire them)
//!
//! Then:
//! - Gemini provider's warmup() automatically refreshes the tokens
//! - The fallback request succeeds

View File

@ -12,7 +12,7 @@
//! Run manually: `cargo test provider_vision -- --ignored --nocapture`
use anyhow::Result;
use zeroclaw::providers::{ChatMessage, ChatRequest, Provider, ProviderRuntimeOptions};
use zeroclaw::providers::{ChatMessage, ChatRequest, ProviderRuntimeOptions};
/// Tests that provider supports vision input.
///

View File

@ -292,7 +292,7 @@ fn provider_construction_with_different_auth_styles() {
#[test]
fn chat_messages_maintain_role_sequence() {
let history = vec![
let history = [
ChatMessage::system("You are helpful"),
ChatMessage::user("What is Rust?"),
ChatMessage::assistant("Rust is a systems programming language"),
@ -309,7 +309,7 @@ fn chat_messages_maintain_role_sequence() {
#[test]
fn chat_messages_with_tool_calls_maintain_sequence() {
let history = vec![
let history = [
ChatMessage::system("You are helpful"),
ChatMessage::user("Search for Rust"),
ChatMessage::assistant("I'll search for that"),