huskies: merge 927
This commit is contained in:
@@ -1,5 +1,7 @@
|
|||||||
//! Human-readable formatting of raw agent log entries.
|
//! Human-readable formatting of raw agent log entries.
|
||||||
|
|
||||||
|
use crate::chat::util::truncate_at_char_boundary;
|
||||||
|
|
||||||
/// Format a single log entry as a human-readable text line.
|
/// Format a single log entry as a human-readable text line.
|
||||||
///
|
///
|
||||||
/// `timestamp` is an ISO 8601 string; `event` is the flattened `AgentEvent`
|
/// `timestamp` is an ISO 8601 string; `event` is the flattened `AgentEvent`
|
||||||
@@ -7,6 +9,7 @@
|
|||||||
///
|
///
|
||||||
/// Returns `None` for entries that should be skipped (raw streaming noise,
|
/// Returns `None` for entries that should be skipped (raw streaming noise,
|
||||||
/// trivial status changes, empty output, etc.).
|
/// trivial status changes, empty output, etc.).
|
||||||
|
#[allow(clippy::string_slice)] // timestamp[11..19]: ISO 8601 is ASCII-only, these byte offsets are always valid
|
||||||
pub fn format_log_entry_as_text(timestamp: &str, event: &serde_json::Value) -> Option<String> {
|
pub fn format_log_entry_as_text(timestamp: &str, event: &serde_json::Value) -> Option<String> {
|
||||||
let agent_name = event
|
let agent_name = event
|
||||||
.get("agent_name")
|
.get("agent_name")
|
||||||
@@ -75,7 +78,7 @@ pub fn format_log_entry_as_text(timestamp: &str, event: &serde_json::Value) -> O
|
|||||||
.map(|v| serde_json::to_string(v).unwrap_or_default())
|
.map(|v| serde_json::to_string(v).unwrap_or_default())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
let display = if input.len() > 200 {
|
let display = if input.len() > 200 {
|
||||||
format!("{}...", &input[..200])
|
format!("{}...", truncate_at_char_boundary(&input, 200))
|
||||||
} else {
|
} else {
|
||||||
input
|
input
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ mod tests {
|
|||||||
/// AC: seed a stale claim older than the TTL, attempt a new claim from a
|
/// AC: seed a stale claim older than the TTL, attempt a new claim from a
|
||||||
/// different agent, assert the new claim succeeds and displacement is logged.
|
/// different agent, assert the new claim succeeds and displacement is logged.
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::string_slice)] // stale_holder is a hex/ASCII string literal; [..12] always valid
|
||||||
fn stale_claim_displaced_and_logged() {
|
fn stale_claim_displaced_and_logged() {
|
||||||
use crate::crdt_state::{init_for_test, our_node_id, read_item, write_claim, write_item};
|
use crate::crdt_state::{init_for_test, our_node_id, read_item, write_claim, write_item};
|
||||||
|
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ use loop_ops::{
|
|||||||
///
|
///
|
||||||
/// If `join_token` and `gateway_url` are both provided the agent will register
|
/// If `join_token` and `gateway_url` are both provided the agent will register
|
||||||
/// itself with the gateway on startup using the one-time token.
|
/// itself with the gateway on startup using the one-time token.
|
||||||
|
#[allow(clippy::string_slice)] // node_id is a UUID (ASCII); min(8) clamps within its length
|
||||||
pub async fn run(
|
pub async fn run(
|
||||||
project_root: Option<PathBuf>,
|
project_root: Option<PathBuf>,
|
||||||
rendezvous_url: String,
|
rendezvous_url: String,
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ const MAX_GATE_OUTPUT_BYTES: usize = 8_000;
|
|||||||
|
|
||||||
/// Truncate gate output to [`MAX_GATE_OUTPUT_BYTES`], keeping the **tail**
|
/// Truncate gate output to [`MAX_GATE_OUTPUT_BYTES`], keeping the **tail**
|
||||||
/// (where compiler errors and test failures are reported).
|
/// (where compiler errors and test failures are reported).
|
||||||
|
#[allow(clippy::string_slice)] // adjusted is walked forward to a char boundary before slicing
|
||||||
fn truncate_gate_output(output: &str) -> &str {
|
fn truncate_gate_output(output: &str) -> &str {
|
||||||
if output.len() <= MAX_GATE_OUTPUT_BYTES {
|
if output.len() <= MAX_GATE_OUTPUT_BYTES {
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ use super::super::types::StoryAgent;
|
|||||||
const GATE_OUTPUT_PROMPT_BYTES: usize = 3_000;
|
const GATE_OUTPUT_PROMPT_BYTES: usize = 3_000;
|
||||||
|
|
||||||
/// Truncate `output` to at most [`GATE_OUTPUT_PROMPT_BYTES`], keeping the tail.
|
/// Truncate `output` to at most [`GATE_OUTPUT_PROMPT_BYTES`], keeping the tail.
|
||||||
|
#[allow(clippy::string_slice)] // adjusted is walked forward to a char boundary before slicing
|
||||||
fn truncate_for_system_prompt(output: &str) -> &str {
|
fn truncate_for_system_prompt(output: &str) -> &str {
|
||||||
if output.len() <= GATE_OUTPUT_PROMPT_BYTES {
|
if output.len() <= GATE_OUTPUT_PROMPT_BYTES {
|
||||||
return output;
|
return output;
|
||||||
|
|||||||
@@ -86,6 +86,7 @@ pub(super) fn handle_cost(ctx: &CommandContext) -> Option<String> {
|
|||||||
///
|
///
|
||||||
/// Agent names like "coder-1", "qa-2", "mergemaster" map to types "coder",
|
/// Agent names like "coder-1", "qa-2", "mergemaster" map to types "coder",
|
||||||
/// "qa", "mergemaster". If the name ends with `-<digits>`, strip the suffix.
|
/// "qa", "mergemaster". If the name ends with `-<digits>`, strip the suffix.
|
||||||
|
#[allow(clippy::string_slice)] // pos comes from rfind('-'), so pos+1 is after an ASCII '-' → valid boundary
|
||||||
pub(super) fn extract_agent_type(agent_name: &str) -> String {
|
pub(super) fn extract_agent_type(agent_name: &str) -> String {
|
||||||
if let Some(pos) = agent_name.rfind('-') {
|
if let Some(pos) = agent_name.rfind('-') {
|
||||||
let suffix = &agent_name[pos + 1..];
|
let suffix = &agent_name[pos + 1..];
|
||||||
|
|||||||
@@ -240,6 +240,7 @@ fn parse_coverage_output(output: &str, passed: bool) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a value from lines like `"Rust line coverage: 62.5%"`.
|
/// Extract a value from lines like `"Rust line coverage: 62.5%"`.
|
||||||
|
#[allow(clippy::string_slice)] // starts_with(prefix) guarantees prefix.len() is a char boundary
|
||||||
fn extract_line_value(output: &str, prefix: &str) -> Option<String> {
|
fn extract_line_value(output: &str, prefix: &str) -> Option<String> {
|
||||||
output
|
output
|
||||||
.lines()
|
.lines()
|
||||||
@@ -248,6 +249,7 @@ fn extract_line_value(output: &str, prefix: &str) -> Option<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract a value from the summary block: `" Overall: 62.5%"`.
|
/// Extract a value from the summary block: `" Overall: 62.5%"`.
|
||||||
|
#[allow(clippy::string_slice)] // starts_with(label) guarantees label.len() is a char boundary
|
||||||
fn extract_summary_field(output: &str, label: &str) -> Option<String> {
|
fn extract_summary_field(output: &str, label: &str) -> Option<String> {
|
||||||
output
|
output
|
||||||
.lines()
|
.lines()
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
//! HEAD, formatted for readability in chat.
|
//! HEAD, formatted for readability in chat.
|
||||||
|
|
||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
|
use crate::chat::util::truncate_at_char_boundary;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
@@ -125,18 +126,6 @@ fn run_git(dir: &Path, args: &[&str]) -> String {
|
|||||||
.unwrap_or_default()
|
.unwrap_or_default()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Truncate `s` to at most `max_bytes` bytes without splitting a UTF-8 character.
|
|
||||||
fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
|
|
||||||
if s.len() <= max_bytes {
|
|
||||||
return s;
|
|
||||||
}
|
|
||||||
let mut boundary = max_bytes;
|
|
||||||
while !s.is_char_boundary(boundary) {
|
|
||||||
boundary -= 1;
|
|
||||||
}
|
|
||||||
&s[..boundary]
|
|
||||||
}
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// Tests
|
// Tests
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
|
|
||||||
/// Show compact git status: branch, uncommitted files, ahead/behind remote.
|
/// Show compact git status: branch, uncommitted files, ahead/behind remote.
|
||||||
|
#[allow(clippy::string_slice)] // line[..2] and line[3..]: git porcelain XY status codes are always ASCII
|
||||||
pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_git(ctx: &CommandContext) -> Option<String> {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use super::CommandContext;
|
|||||||
/// git diff --stat (files changed with line counts), and extracts key
|
/// git diff --stat (files changed with line counts), and extracts key
|
||||||
/// function/struct/type names added or modified in the implementation.
|
/// function/struct/type names added or modified in the implementation.
|
||||||
/// Returns a friendly message when no merge commit is found.
|
/// Returns a friendly message when no merge commit is found.
|
||||||
|
#[allow(clippy::string_slice)] // commit_hash is hex (ASCII), min(8) always within bounds
|
||||||
pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
pub(super) fn handle_overview(ctx: &CommandContext) -> Option<String> {
|
||||||
let num_str = ctx.args.trim();
|
let num_str = ctx.args.trim();
|
||||||
if num_str.is_empty() {
|
if num_str.is_empty() {
|
||||||
@@ -129,6 +130,7 @@ fn get_commit_stat(root: &std::path::Path, hash: &str) -> String {
|
|||||||
///
|
///
|
||||||
/// Scans added lines (`+`) for Rust `fn`, `struct`, `enum`, `type`, `trait`,
|
/// Scans added lines (`+`) for Rust `fn`, `struct`, `enum`, `type`, `trait`,
|
||||||
/// and `impl` declarations and returns them formatted as `` `Name` (kind) ``.
|
/// and `impl` declarations and returns them formatted as `` `Name` (kind) ``.
|
||||||
|
#[allow(clippy::string_slice)] // line starts with '+' (ASCII), so &line[1..] is always a valid boundary
|
||||||
fn extract_diff_symbols(root: &std::path::Path, hash: &str) -> Vec<String> {
|
fn extract_diff_symbols(root: &std::path::Path, hash: &str) -> Vec<String> {
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
let output = Command::new("git")
|
let output = Command::new("git")
|
||||||
|
|||||||
@@ -123,6 +123,7 @@ fn parse_test_counts(output: &str) -> (u64, u64) {
|
|||||||
(total_passed, total_failed)
|
(total_passed, total_failed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::string_slice)] // pos from line.find(label) → always a char boundary
|
||||||
fn extract_count(line: &str, label: &str) -> Option<u64> {
|
fn extract_count(line: &str, label: &str) -> Option<u64> {
|
||||||
let pos = line.find(label)?;
|
let pos = line.find(label)?;
|
||||||
let before = line[..pos].trim_end();
|
let before = line[..pos].trim_end();
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
use super::CommandContext;
|
use super::CommandContext;
|
||||||
|
|
||||||
/// Strip YAML front matter and return a summary of useful fields + the remaining body.
|
/// Strip YAML front matter and return a summary of useful fields + the remaining body.
|
||||||
|
#[allow(clippy::string_slice)] // indices from find("\n---") on ASCII delimiter; "---" and "\n---" are ASCII-only
|
||||||
fn strip_front_matter(text: &str) -> (String, String) {
|
fn strip_front_matter(text: &str) -> (String, String) {
|
||||||
let trimmed = text.trim_start();
|
let trimmed = text.trim_start();
|
||||||
if !trimmed.starts_with("---") {
|
if !trimmed.starts_with("---") {
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ fn status_shows_crdt_done_story_in_done_not_backlog() {
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Verify it's not in Backlog section specifically.
|
// Verify it's not in Backlog section specifically.
|
||||||
let backlog_section = &output[backlog_pos..done_pos];
|
let backlog_section = output.get(backlog_pos..done_pos).unwrap_or("");
|
||||||
assert!(
|
assert!(
|
||||||
!backlog_section.contains("503"),
|
!backlog_section.contains("503"),
|
||||||
"503 must not appear in Backlog section: {backlog_section}"
|
"503 must not appear in Backlog section: {backlog_section}"
|
||||||
@@ -573,10 +573,16 @@ fn merge_item_failure_snippet_truncated_at_120_chars() {
|
|||||||
);
|
);
|
||||||
// The snippet should not exceed 120 chars plus the ellipsis character.
|
// The snippet should not exceed 120 chars plus the ellipsis character.
|
||||||
let snippet_start = output.find("\u{26D4}").expect("stop sign must be present");
|
let snippet_start = output.find("\u{26D4}").expect("stop sign must be present");
|
||||||
let line = output[snippet_start..].lines().next().unwrap_or("");
|
let line = output
|
||||||
|
.get(snippet_start..)
|
||||||
|
.unwrap_or("")
|
||||||
|
.lines()
|
||||||
|
.next()
|
||||||
|
.unwrap_or("");
|
||||||
// Find the last " — " separator (before the snippet) and take what follows.
|
// Find the last " — " separator (before the snippet) and take what follows.
|
||||||
if let Some(sep_pos) = line.rfind(" \u{2014} ") {
|
if let Some(sep_pos) = line.rfind(" \u{2014} ") {
|
||||||
let snippet = &line[sep_pos + 5..]; // " — " is 5 bytes (space + 3-byte em dash + space)
|
// " — " is 5 bytes (space + 3-byte em dash + space)
|
||||||
|
let snippet = line.get(sep_pos + 5..).unwrap_or("");
|
||||||
assert!(
|
assert!(
|
||||||
snippet.chars().count() <= 122, // 120 chars + "…" (1 char) + possible trailing
|
snippet.chars().count() <= 122, // 120 chars + "…" (1 char) + possible trailing
|
||||||
"snippet should be at most ~121 chars: {snippet}"
|
"snippet should be at most ~121 chars: {snippet}"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::chat::{ChatTransport, MessageId};
|
use crate::chat::{ChatTransport, MessageId, util::truncate_at_char_boundary};
|
||||||
use crate::slog;
|
use crate::slog;
|
||||||
|
|
||||||
// ── Discord API base URL (overridable for tests) ──────────────────────
|
// ── Discord API base URL (overridable for tests) ──────────────────────
|
||||||
@@ -71,7 +71,7 @@ impl ChatTransport for DiscordTransport {
|
|||||||
|
|
||||||
// Discord messages have a 2000-char limit. Truncate if needed.
|
// Discord messages have a 2000-char limit. Truncate if needed.
|
||||||
let content = if plain.len() > 2000 {
|
let content = if plain.len() > 2000 {
|
||||||
format!("{}…", &plain[..1999])
|
format!("{}…", truncate_at_char_boundary(plain, 1999))
|
||||||
} else {
|
} else {
|
||||||
plain.to_string()
|
plain.to_string()
|
||||||
};
|
};
|
||||||
@@ -118,7 +118,7 @@ impl ChatTransport for DiscordTransport {
|
|||||||
);
|
);
|
||||||
|
|
||||||
let content = if plain.len() > 2000 {
|
let content = if plain.len() > 2000 {
|
||||||
format!("{}…", &plain[..1999])
|
format!("{}…", truncate_at_char_boundary(plain, 1999))
|
||||||
} else {
|
} else {
|
||||||
plain.to_string()
|
plain.to_string()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -37,6 +37,7 @@ pub fn mentions_bot(body: &str, formatted_body: Option<&str>, bot_user_id: &Owne
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if `haystack` contains `needle` at a word boundary.
|
/// Returns `true` if `haystack` contains `needle` at a word boundary.
|
||||||
|
#[allow(clippy::string_slice)] // all indices from find() or abs+needle.len() → always char boundaries
|
||||||
pub(super) fn contains_word(haystack: &str, needle: &str) -> bool {
|
pub(super) fn contains_word(haystack: &str, needle: &str) -> bool {
|
||||||
let mut start = 0;
|
let mut start = 0;
|
||||||
while let Some(rel) = haystack[start..].find(needle) {
|
while let Some(rel) = haystack[start..].find(needle) {
|
||||||
@@ -87,6 +88,7 @@ pub(super) async fn is_reply_to_bot(
|
|||||||
///
|
///
|
||||||
/// Used in ambient mode to suppress responses when a message is clearly
|
/// Used in ambient mode to suppress responses when a message is clearly
|
||||||
/// directed at a different participant (e.g. another bot in the same room).
|
/// directed at a different participant (e.g. another bot in the same room).
|
||||||
|
#[allow(clippy::string_slice)] // word_end and colon_pos from find() → always char boundaries
|
||||||
pub fn is_addressed_to_other(body: &str, bot_user_id: &OwnedUserId, bot_name: &str) -> bool {
|
pub fn is_addressed_to_other(body: &str, bot_user_id: &OwnedUserId, bot_name: &str) -> bool {
|
||||||
let trimmed = body.trim_start();
|
let trimmed = body.trim_start();
|
||||||
let lower = trimmed.to_lowercase();
|
let lower = trimmed.to_lowercase();
|
||||||
|
|||||||
@@ -101,6 +101,7 @@ fn parse_duration(s: &str) -> Option<u64> {
|
|||||||
///
|
///
|
||||||
/// Returns a short string like `"load average: 1.23, 0.98, 0.75"` on success,
|
/// Returns a short string like `"load average: 1.23, 0.98, 0.75"` on success,
|
||||||
/// or `"load: unknown"` on failure.
|
/// or `"load: unknown"` on failure.
|
||||||
|
#[allow(clippy::string_slice)] // idx comes from output.find("load average") → always a char boundary
|
||||||
fn get_load_average() -> String {
|
fn get_load_average() -> String {
|
||||||
let output = std::process::Command::new("uptime")
|
let output = std::process::Command::new("uptime")
|
||||||
.output()
|
.output()
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
//! WhatsApp message formatting — Markdown-to-WhatsApp conversion and message chunking.
|
//! WhatsApp message formatting — Markdown-to-WhatsApp conversion and message chunking.
|
||||||
|
use crate::chat::util::truncate_at_char_boundary;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::sync::LazyLock;
|
use std::sync::LazyLock;
|
||||||
|
|
||||||
@@ -24,13 +25,13 @@ pub fn chunk_for_whatsapp(text: &str) -> Vec<String> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Find the best split point within the limit.
|
// Find the best split point within the limit.
|
||||||
let window = &remaining[..WHATSAPP_MAX_MESSAGE_LEN];
|
let window = truncate_at_char_boundary(remaining, WHATSAPP_MAX_MESSAGE_LEN);
|
||||||
|
|
||||||
// Prefer paragraph boundary.
|
// Prefer paragraph boundary.
|
||||||
let split_pos = window
|
let split_pos = window
|
||||||
.rfind("\n\n")
|
.rfind("\n\n")
|
||||||
.or_else(|| window.rfind('\n'))
|
.or_else(|| window.rfind('\n'))
|
||||||
.unwrap_or(WHATSAPP_MAX_MESSAGE_LEN);
|
.unwrap_or(window.len());
|
||||||
|
|
||||||
let (chunk, rest) = remaining.split_at(split_pos);
|
let (chunk, rest) = remaining.split_at(split_pos);
|
||||||
let chunk = chunk.trim();
|
let chunk = chunk.trim();
|
||||||
|
|||||||
@@ -3,6 +3,23 @@
|
|||||||
//! These functions are transport-agnostic helpers for processing chat messages:
|
//! These functions are transport-agnostic helpers for processing chat messages:
|
||||||
//! prefix stripping, bot-mention handling, and paragraph buffering.
|
//! prefix stripping, bot-mention handling, and paragraph buffering.
|
||||||
|
|
||||||
|
/// Truncate `s` to at most `max_bytes` bytes without splitting a UTF-8 codepoint.
|
||||||
|
///
|
||||||
|
/// If `s.len() <= max_bytes` the original slice is returned unchanged.
|
||||||
|
/// Otherwise the returned slice ends at the largest char boundary ≤ `max_bytes`,
|
||||||
|
/// preventing any panic that would result from slicing mid-codepoint.
|
||||||
|
pub fn truncate_at_char_boundary(s: &str, max_bytes: usize) -> &str {
|
||||||
|
if s.len() <= max_bytes {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
let mut boundary = max_bytes;
|
||||||
|
while !s.is_char_boundary(boundary) {
|
||||||
|
boundary -= 1;
|
||||||
|
}
|
||||||
|
#[allow(clippy::string_slice)] // boundary is guaranteed to be a char boundary by the loop above
|
||||||
|
&s[..boundary]
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns `true` if the message body is an affirmative permission response.
|
/// Returns `true` if the message body is an affirmative permission response.
|
||||||
///
|
///
|
||||||
/// Recognised affirmative tokens (case-insensitive): `yes`, `y`, `approve`,
|
/// Recognised affirmative tokens (case-insensitive): `yes`, `y`, `approve`,
|
||||||
@@ -26,6 +43,7 @@ pub fn is_permission_approval(body: &str) -> bool {
|
|||||||
|
|
||||||
/// Case-insensitive prefix strip that also requires the match to end at a
|
/// Case-insensitive prefix strip that also requires the match to end at a
|
||||||
/// word boundary (whitespace, punctuation, or end-of-string).
|
/// word boundary (whitespace, punctuation, or end-of-string).
|
||||||
|
#[allow(clippy::string_slice)] // prefix.len() is safe: `get(..prefix.len())` already validated the boundary
|
||||||
pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
||||||
let candidate = text.get(..prefix.len())?;
|
let candidate = text.get(..prefix.len())?;
|
||||||
if !candidate.eq_ignore_ascii_case(prefix) {
|
if !candidate.eq_ignore_ascii_case(prefix) {
|
||||||
@@ -50,6 +68,7 @@ pub fn strip_prefix_ci<'a>(text: &'a str, prefix: &str) -> Option<&'a str> {
|
|||||||
/// - `DisplayName, rest` → `rest` (Element tab-completion may insert a comma)
|
/// - `DisplayName, rest` → `rest` (Element tab-completion may insert a comma)
|
||||||
/// - `DisplayName ⚡️: rest` → `rest` (display name with emoji)
|
/// - `DisplayName ⚡️: rest` → `rest` (display name with emoji)
|
||||||
/// - `[DisplayName](https://matrix.to/#/@user:server) rest` → `rest` (Element mention pill)
|
/// - `[DisplayName](https://matrix.to/#/@user:server) rest` → `rest` (Element mention pill)
|
||||||
|
#[allow(clippy::string_slice)] // all indices come from str::find / str::find_map → always char boundaries
|
||||||
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
|
pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str) -> &'a str {
|
||||||
let trimmed = message.trim();
|
let trimmed = message.trim();
|
||||||
|
|
||||||
@@ -98,6 +117,7 @@ pub fn strip_bot_mention<'a>(message: &'a str, bot_name: &str, bot_user_id: &str
|
|||||||
/// emoji from display names (e.g. `Timmy ⚡️`), and Element tab-completion
|
/// emoji from display names (e.g. `Timmy ⚡️`), and Element tab-completion
|
||||||
/// separators (`:` or `,`). This function skips all of that and returns a
|
/// separators (`:` or `,`). This function skips all of that and returns a
|
||||||
/// slice starting at the first ASCII alphanumeric character (the command).
|
/// slice starting at the first ASCII alphanumeric character (the command).
|
||||||
|
#[allow(clippy::string_slice)] // byte_skip comes from char_indices → guaranteed char boundary
|
||||||
fn strip_mention_separator(rest: &str) -> &str {
|
fn strip_mention_separator(rest: &str) -> &str {
|
||||||
let byte_skip = rest
|
let byte_skip = rest
|
||||||
.char_indices()
|
.char_indices()
|
||||||
@@ -132,6 +152,7 @@ fn is_inside_code_fence(text: &str) -> bool {
|
|||||||
/// block (delimited by ` ``` ` lines) is **not** treated as a paragraph
|
/// block (delimited by ` ``` ` lines) is **not** treated as a paragraph
|
||||||
/// boundary. This prevents a blank line inside a code block from splitting
|
/// boundary. This prevents a blank line inside a code block from splitting
|
||||||
/// the fence across multiple messages, which would corrupt the rendering.
|
/// the fence across multiple messages, which would corrupt the rendering.
|
||||||
|
#[allow(clippy::string_slice)] // abs_pos comes from str::find → always a char boundary
|
||||||
pub fn drain_complete_paragraphs(buffer: &mut String) -> Vec<String> {
|
pub fn drain_complete_paragraphs(buffer: &mut String) -> Vec<String> {
|
||||||
let mut paragraphs = Vec::new();
|
let mut paragraphs = Vec::new();
|
||||||
let mut search_from = 0;
|
let mut search_from = 0;
|
||||||
@@ -259,6 +280,47 @@ pub fn normalize_line_breaks(text: &str) -> String {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
// -- truncate_at_char_boundary ------------------------------------------
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_ascii_within_limit() {
|
||||||
|
assert_eq!(truncate_at_char_boundary("hello", 10), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_ascii_at_exact_limit() {
|
||||||
|
assert_eq!(truncate_at_char_boundary("hello", 5), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_ascii_over_limit() {
|
||||||
|
assert_eq!(truncate_at_char_boundary("hello world", 5), "hello");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_multibyte_mid_codepoint_snaps_back() {
|
||||||
|
// "héllo": 'é' is U+00E9, 2 bytes. max_bytes=2 lands inside it → snap to 1.
|
||||||
|
let s = "héllo";
|
||||||
|
assert_eq!(truncate_at_char_boundary(s, 2), "h");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_multibyte_on_char_boundary() {
|
||||||
|
// "héllo": h(1) + é(2) = 3 bytes. max_bytes=3 is a valid boundary.
|
||||||
|
let s = "héllo";
|
||||||
|
assert_eq!(truncate_at_char_boundary(s, 3), "hé");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_max_bytes_zero_returns_empty() {
|
||||||
|
assert_eq!(truncate_at_char_boundary("hello", 0), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn truncate_max_bytes_greater_than_len() {
|
||||||
|
assert_eq!(truncate_at_char_boundary("hi", 100), "hi");
|
||||||
|
}
|
||||||
|
|
||||||
// -- is_permission_approval ---------------------------------------------
|
// -- is_permission_approval ---------------------------------------------
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ pub(crate) struct CliArgs {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
|
/// Parse CLI arguments into `CliArgs`, or exit early for `--help` / `--version`.
|
||||||
|
#[allow(clippy::string_slice)] // all slices use ASCII prefix lengths (e.g. "--port=".len()), always valid
|
||||||
pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
pub(crate) fn parse_cli_args(args: &[String]) -> Result<CliArgs, String> {
|
||||||
let mut port: Option<u16> = None;
|
let mut port: Option<u16> = None;
|
||||||
let mut path: Option<String> = None;
|
let mut path: Option<String> = None;
|
||||||
|
|||||||
@@ -34,6 +34,7 @@ use crate::slog;
|
|||||||
/// Opens the SQLite database, loads or creates a node keypair, replays any
|
/// Opens the SQLite database, loads or creates a node keypair, replays any
|
||||||
/// persisted ops to reconstruct state, and spawns a background persistence
|
/// persisted ops to reconstruct state, and spawns a background persistence
|
||||||
/// task. Safe to call only once; subsequent calls are no-ops.
|
/// task. Safe to call only once; subsequent calls are no-ops.
|
||||||
|
#[allow(clippy::string_slice)] // op_id is hex::encode output (ASCII-only), &op_id[..12] is always valid
|
||||||
pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
pub async fn init(db_path: &Path) -> Result<(), sqlx::Error> {
|
||||||
if CRDT_STATE.get().is_some() {
|
if CRDT_STATE.get().is_some() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ pub fn name_from_story_id(story_id: &str) -> String {
|
|||||||
///
|
///
|
||||||
/// Returns `Some("664")` for `"664_story_my_feature"`, and `None` for IDs
|
/// Returns `Some("664")` for `"664_story_my_feature"`, and `None` for IDs
|
||||||
/// that are already numeric-only (`"664"`) or have no valid numeric prefix.
|
/// that are already numeric-only (`"664"`) or have no valid numeric prefix.
|
||||||
|
#[allow(clippy::string_slice)] // idx comes from find('_') → always a char boundary
|
||||||
pub(super) fn numeric_id_from_slug(story_id: &str) -> Option<String> {
|
pub(super) fn numeric_id_from_slug(story_id: &str) -> Option<String> {
|
||||||
// Already numeric-only — no migration needed.
|
// Already numeric-only — no migration needed.
|
||||||
if story_id.chars().all(|c: char| c.is_ascii_digit()) {
|
if story_id.chars().all(|c: char| c.is_ascii_digit()) {
|
||||||
|
|||||||
@@ -225,6 +225,7 @@ async fn proxy_and_respond(state: &GatewayState, bytes: &[u8], id: Option<Value>
|
|||||||
///
|
///
|
||||||
/// On sled disconnect mid-stream a JSON-RPC error event is emitted so the
|
/// On sled disconnect mid-stream a JSON-RPC error event is emitted so the
|
||||||
/// client does not hang forever.
|
/// client does not hang forever.
|
||||||
|
#[allow(clippy::string_slice)] // pos from buf.find('\n'); '\n' is ASCII so pos and pos+1 are valid boundaries
|
||||||
async fn proxy_and_respond_sse(state: &GatewayState, bytes: &[u8], id: Option<Value>) -> Response {
|
async fn proxy_and_respond_sse(state: &GatewayState, bytes: &[u8], id: Option<Value>) -> Response {
|
||||||
let url = match state.active_url().await {
|
let url = match state.active_url().await {
|
||||||
Ok(u) => u,
|
Ok(u) => u,
|
||||||
|
|||||||
@@ -90,6 +90,7 @@ pub(super) fn is_bug_item(stem: &str) -> bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Extract bug name from content (heading or front matter).
|
/// Extract bug name from content (heading or front matter).
|
||||||
|
#[allow(clippy::string_slice)] // colon_pos from find(": "); +2 skips ASCII ": " → valid boundary
|
||||||
pub(super) fn extract_bug_name_from_content(content: &str) -> Option<String> {
|
pub(super) fn extract_bug_name_from_content(content: &str) -> Option<String> {
|
||||||
// Try front matter first.
|
// Try front matter first.
|
||||||
if let Ok(meta) = parse_front_matter(content)
|
if let Ok(meta) = parse_front_matter(content)
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ use super::super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// Toggle an acceptance criterion checkbox (`- [ ]` → `- [x]`) by its 0-based index among unchecked items.
|
/// Toggle an acceptance criterion checkbox (`- [ ]` → `- [x]`) by its 0-based index among unchecked items.
|
||||||
|
#[allow(clippy::string_slice)] // indent_len = line.len() - trimmed.len(); trim() returns a sub-slice → valid boundary
|
||||||
pub fn check_criterion_in_file(
|
pub fn check_criterion_in_file(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
@@ -107,6 +108,7 @@ pub fn remove_criterion_from_file(
|
|||||||
///
|
///
|
||||||
/// Finds the criterion at `criterion_index` (0-based, counting all criteria regardless
|
/// Finds the criterion at `criterion_index` (0-based, counting all criteria regardless
|
||||||
/// of checked state) and replaces its text with `new_text`.
|
/// of checked state) and replaces its text with `new_text`.
|
||||||
|
#[allow(clippy::string_slice)] // indent_len = line.len() - trimmed.len(); trim() returns a sub-slice → valid boundary
|
||||||
pub fn edit_criterion_in_file(
|
pub fn edit_criterion_in_file(
|
||||||
project_root: &Path,
|
project_root: &Path,
|
||||||
story_id: &str,
|
story_id: &str,
|
||||||
|
|||||||
@@ -124,6 +124,7 @@ fn format_test_line(t: &TestCaseResult) -> String {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Parse `StoryTestResults` from the JSON embedded in the `## Test Results` section.
|
/// Parse `StoryTestResults` from the JSON embedded in the `## Test Results` section.
|
||||||
|
#[allow(clippy::string_slice)] // json_end from rfind("-->") → always a char boundary
|
||||||
fn parse_test_results_from_contents(contents: &str) -> Option<StoryTestResults> {
|
fn parse_test_results_from_contents(contents: &str) -> Option<StoryTestResults> {
|
||||||
for line in contents.lines() {
|
for line in contents.lines() {
|
||||||
let trimmed = line.trim();
|
let trimmed = line.trim();
|
||||||
|
|||||||
@@ -409,6 +409,7 @@ pub async fn swap_to_next_available_account(
|
|||||||
///
|
///
|
||||||
/// Returns the URL portion when the error indicates missing or expired credentials,
|
/// Returns the URL portion when the error indicates missing or expired credentials,
|
||||||
/// `None` otherwise.
|
/// `None` otherwise.
|
||||||
|
#[allow(clippy::string_slice)] // start + marker.len() is after the ASCII marker found by find() → valid boundary
|
||||||
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
|
pub fn extract_login_url_from_error(err: &str) -> Option<&str> {
|
||||||
let marker = "Please log in: ";
|
let marker = "Please log in: ";
|
||||||
let start = err.find(marker)?;
|
let start = err.find(marker)?;
|
||||||
|
|||||||
@@ -182,6 +182,7 @@ impl ClaudeCodeProvider {
|
|||||||
/// via `--permission-prompt-tool`. Claude Code calls the MCP tool when it
|
/// via `--permission-prompt-tool`. Claude Code calls the MCP tool when it
|
||||||
/// needs user approval, and the server bridges the request to the frontend.
|
/// needs user approval, and the server bridges the request to the frontend.
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
|
#[allow(clippy::string_slice)] // end is walked to a char boundary before slicing &trimmed[..end]
|
||||||
fn run_pty_session(
|
fn run_pty_session(
|
||||||
user_message: &str,
|
user_message: &str,
|
||||||
cwd: &str,
|
cwd: &str,
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ impl OllamaProvider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Streaming chat that calls `on_token` for each token chunk.
|
/// Streaming chat that calls `on_token` for each token chunk.
|
||||||
|
#[allow(clippy::string_slice)] // newline_pos from find('\n'); '\n' is ASCII so pos and pos+1 are valid
|
||||||
pub async fn chat_stream<F>(
|
pub async fn chat_stream<F>(
|
||||||
&self,
|
&self,
|
||||||
model: &str,
|
model: &str,
|
||||||
|
|||||||
@@ -3,6 +3,8 @@
|
|||||||
// matrix-sdk-crypto's deeply nested types require a higher recursion limit
|
// matrix-sdk-crypto's deeply nested types require a higher recursion limit
|
||||||
// when the `e2e-encryption` feature is enabled.
|
// when the `e2e-encryption` feature is enabled.
|
||||||
#![recursion_limit = "256"]
|
#![recursion_limit = "256"]
|
||||||
|
// Prevent panics from direct &str indexing that could split multi-byte UTF-8 codepoints.
|
||||||
|
#![deny(clippy::string_slice)]
|
||||||
|
|
||||||
mod agent_log;
|
mod agent_log;
|
||||||
mod agent_mode;
|
mod agent_mode;
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ fn hex_encode(bytes: &[u8]) -> String {
|
|||||||
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
bytes.iter().map(|b| format!("{b:02x}")).collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::string_slice)] // s is hex (ASCII-only); i advances in steps of 2, always within bounds
|
||||||
fn hex_decode(s: &str) -> Option<Vec<u8>> {
|
fn hex_decode(s: &str) -> Option<Vec<u8>> {
|
||||||
if !s.len().is_multiple_of(2) {
|
if !s.len().is_multiple_of(2) {
|
||||||
return None;
|
return None;
|
||||||
@@ -330,6 +331,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
#[allow(clippy::string_slice)] // sig is hex (ASCII-only); subtracting 4 stays within bounds for any valid sig
|
||||||
fn verify_rejects_truncated_signature() {
|
fn verify_rejects_truncated_signature() {
|
||||||
let kp = make_keypair();
|
let kp = make_keypair();
|
||||||
let pubkey = public_key_hex(&kp);
|
let pubkey = public_key_hex(&kp);
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
///
|
///
|
||||||
/// Returns `(staged, unstaged, untracked)` where each entry is the file path
|
/// Returns `(staged, unstaged, untracked)` where each entry is the file path
|
||||||
/// string from the porcelain line.
|
/// string from the porcelain line.
|
||||||
|
#[allow(clippy::string_slice)] // line[3..]: git porcelain format has 2 ASCII status chars + ASCII space
|
||||||
pub fn parse_git_status_porcelain(stdout: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
|
pub fn parse_git_status_porcelain(stdout: &str) -> (Vec<String>, Vec<String>, Vec<String>) {
|
||||||
let mut staged: Vec<String> = Vec::new();
|
let mut staged: Vec<String> = Vec::new();
|
||||||
let mut unstaged: Vec<String> = Vec::new();
|
let mut unstaged: Vec<String> = Vec::new();
|
||||||
|
|||||||
@@ -109,10 +109,11 @@ pub(super) fn save_credentials(
|
|||||||
|
|
||||||
// Build the pool key: prefer email, fall back to token prefix.
|
// Build the pool key: prefer email, fall back to token prefix.
|
||||||
let key = if email.is_empty() {
|
let key = if email.is_empty() {
|
||||||
format!(
|
let prefix = token
|
||||||
"account-{}",
|
.access_token
|
||||||
&token.access_token[..token.access_token.len().min(16)]
|
.get(..token.access_token.len().min(16))
|
||||||
)
|
.unwrap_or(&token.access_token);
|
||||||
|
format!("account-{prefix}")
|
||||||
} else {
|
} else {
|
||||||
email.to_string()
|
email.to_string()
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -88,6 +88,7 @@ pub fn parse_test_counts(output: &str) -> (u64, u64) {
|
|||||||
///
|
///
|
||||||
/// For example, `extract_count("5 passed; 0 failed", "passed")` returns
|
/// For example, `extract_count("5 passed; 0 failed", "passed")` returns
|
||||||
/// `Some(5)`. Returns `None` if no digit sequence precedes `label`.
|
/// `Some(5)`. Returns `None` if no digit sequence precedes `label`.
|
||||||
|
#[allow(clippy::string_slice)] // pos from line.find(label) → always a char boundary
|
||||||
pub fn extract_count(line: &str, label: &str) -> Option<u64> {
|
pub fn extract_count(line: &str, label: &str) -> Option<u64> {
|
||||||
let pos = line.find(label)?;
|
let pos = line.find(label)?;
|
||||||
let before = line[..pos].trim_end();
|
let before = line[..pos].trim_end();
|
||||||
|
|||||||
Reference in New Issue
Block a user