fix #265: resolve all remaining clippy warnings (cast, too_many_lines, doc nits)

This commit is contained in:
damocles 2026-05-22 19:02:11 +02:00
parent 30d82148e0
commit 484cea62c7
12 changed files with 95 additions and 86 deletions

View file

@ -149,7 +149,7 @@ async fn main() -> Result<()> {
} }
} }
#[allow(clippy::too_many_arguments, clippy::similar_names)] #[allow(clippy::too_many_arguments, clippy::similar_names, clippy::too_many_lines)]
async fn serve( async fn serve(
socket: &Path, socket: &Path,
interval: Duration, interval: Duration,
@ -307,14 +307,14 @@ async fn serve(
} }
} }
/// Per-turn user prompt. The role/tools/etc. is in the system prompt // Per-turn user prompt: the role/tools/etc. is in the system prompt
/// (`prompts/agent.md` → `claude --system-prompt-file`); this is just the // (`prompts/agent.md` → `claude --system-prompt-file`); this is just the
/// wake signal claude reacts to. `unread` is the count of *other* // wake signal claude reacts to. `unread` is the count of *other*
/// messages in the inbox right after this one was popped. // messages in the inbox right after this one was popped.
/// `redelivered` flags messages that were popped in a prior harness // `redelivered` flags messages that were popped in a prior harness
/// session, never acked, and resurfaced after a restart — a banner // session, never acked, and resurfaced after a restart — a banner
/// at the top of the wake prompt warns that any side-effects of // at the top of the wake prompt warns that any side-effects of
/// previous handling may already have happened. // previous handling may already have happened.
/// Best-effort: tell the broker every message we popped during the /// Best-effort: tell the broker every message we popped during the
/// turn is now fully handled (turn-end-OK). Swallows transport /// turn is now fully handled (turn-end-OK). Swallows transport

View file

@ -113,6 +113,7 @@ async fn main() -> Result<()> {
} }
} }
#[allow(clippy::too_many_lines)] // linear startup sequence; splitting adds indirection without clarity
async fn serve( async fn serve(
socket: &Path, socket: &Path,
interval: Duration, interval: Duration,

View file

@ -22,6 +22,7 @@
//! are silently marked read — the agent already knows it opened them. //! are silently marked read — the agent already knows it opened them.
//! - Comment notifications where the comment author matches this agent's own //! - Comment notifications where the comment author matches this agent's own
//! forge login are silently marked read. //! forge login are silently marked read.
//!
//! Own login is fetched once at startup via `GET /user` and cached for the //! Own login is fetched once at startup via `GET /user` and cached for the
//! lifetime of the polling loop. //! lifetime of the polling loop.
//! //!
@ -32,6 +33,7 @@
//! generic `[comment on PR #N repo]` so agents can action it immediately. //! generic `[comment on PR #N repo]` so agents can action it immediately.
use std::collections::HashSet; use std::collections::HashSet;
use std::fmt::Write as _;
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::time::Duration; use std::time::Duration;
@ -209,6 +211,7 @@ fn review_state_label(state: &str) -> Option<&str> {
/// ///
/// Number is extracted from `html_url` last path segment before any `#`. /// Number is extracted from `html_url` last path segment before any `#`.
/// Repo slug (`owner/name`) is always included — agents may watch multiple repos. /// Repo slug (`owner/name`) is always included — agents may watch multiple repos.
#[allow(clippy::too_many_lines)] // multiple notification types handled in one place by design
async fn format_notification( async fn format_notification(
client: &reqwest::Client, client: &reqwest::Client,
token: &str, token: &str,
@ -329,9 +332,9 @@ async fn format_notification(
let kind = format!("PR {review_label}{num}{repo}"); let kind = format!("PR {review_label}{num}{repo}");
let mut out = format!("[{kind}] {title}\nurl: {url}"); let mut out = format!("[{kind}] {title}\nurl: {url}");
if body_text.is_empty() { if body_text.is_empty() {
out.push_str(&format!("\n\nreviewer: {author}")); write!(out, "\n\nreviewer: {author}").ok();
} else { } else {
out.push_str(&format!("\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE))); write!(out, "\n\n{author}: {}", truncate(body_text, BODY_TRUNCATE)).ok();
} }
out.push_str(&meta_suffix); out.push_str(&meta_suffix);
Some(out) Some(out)
@ -449,15 +452,12 @@ async fn poll_once(
debug!(count = notifications.len(), "forge_notify: delivering notifications"); debug!(count = notifications.len(), "forge_notify: delivering notifications");
for notif in &notifications { for notif in &notifications {
let id = match notif["id"].as_u64() { let Some(id) = notif["id"].as_u64() else { continue };
Some(n) => n,
None => continue,
};
let body_opt = format_notification(client, token, notif, own_login).await; let body_opt = format_notification(client, token, notif, own_login).await;
// None means self-echo — mark read silently, no delivery. // None means self-echo — mark read silently, no delivery.
let body = if let Some(b) = body_opt { b } else { let Some(body) = body_opt else {
mark_read(client, forge_url, token, id).await; mark_read(client, forge_url, token, id).await;
continue; continue;
}; };

View file

@ -198,6 +198,7 @@ pub fn rate_limit_sleep_secs() -> u64 {
/// 1. `HIVE_AUTO_RESET_WATERMARK_TOKENS` env var (explicit override). /// 1. `HIVE_AUTO_RESET_WATERMARK_TOKENS` env var (explicit override).
/// 2. 50% of the model's context window (derived from `bus.model()` + /// 2. 50% of the model's context window (derived from `bus.model()` +
/// `events::context_window_tokens`). /// `events::context_window_tokens`).
///
/// `0` disables auto-reset entirely. /// `0` disables auto-reset entirely.
fn auto_reset_watermark_tokens(bus: &Bus) -> u64 { fn auto_reset_watermark_tokens(bus: &Bus) -> u64 {
if let Some(v) = std::env::var("HIVE_AUTO_RESET_WATERMARK_TOKENS") if let Some(v) = std::env::var("HIVE_AUTO_RESET_WATERMARK_TOKENS")
@ -223,6 +224,7 @@ fn cache_ttl_secs() -> u64 {
/// 1. `HIVE_COMPACT_WATERMARK_TOKENS` env var (explicit override). /// 1. `HIVE_COMPACT_WATERMARK_TOKENS` env var (explicit override).
/// 2. 75% of the model's context window (derived from `bus.model()` + /// 2. 75% of the model's context window (derived from `bus.model()` +
/// `events::context_window_tokens`). /// `events::context_window_tokens`).
///
/// `0` disables proactive compaction (reactive path still applies). /// `0` disables proactive compaction (reactive path still applies).
fn compact_watermark_tokens(bus: &Bus) -> u64 { fn compact_watermark_tokens(bus: &Bus) -> u64 {
if let Some(v) = std::env::var("HIVE_COMPACT_WATERMARK_TOKENS") if let Some(v) = std::env::var("HIVE_COMPACT_WATERMARK_TOKENS")

View file

@ -335,7 +335,9 @@ async fn api_stats(
// Pass the window span to the reminder-stats RPC so the broker // Pass the window span to the reminder-stats RPC so the broker
// filters its counts to the same time range as the chart data. // filters its counts to the same time range as the chart data.
let window_secs = window.span_secs(); let window_secs = window.span_secs();
snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs as u64).await; #[allow(clippy::cast_sign_loss)] // window span is always a positive duration
let window_secs_u = window_secs.max(0) as u64;
snapshot.reminder_stats = fetch_reminder_stats(&state.socket, state.flavor(), window_secs_u).await;
axum::Json(snapshot) axum::Json(snapshot)
} }

View file

@ -23,6 +23,7 @@ use crate::lifecycle::{self, MANAGER_NAME};
/// ///
/// In all cases an `ApprovalResolved` helper event lands in the manager's /// In all cases an `ApprovalResolved` helper event lands in the manager's
/// inbox when the work resolves. /// inbox when the work resolves.
#[allow(clippy::too_many_lines)] // approval dispatch covers several independent approval kinds
pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> { pub async fn approve(coord: Arc<Coordinator>, id: i64) -> Result<()> {
let approval = coord.approvals.mark_approved(id)?; let approval = coord.approvals.mark_approved(id)?;
tracing::info!( tracing::info!(
@ -268,6 +269,7 @@ fn finish_approval(
/// and reset the working tree back to the last known-good main. main /// and reset the working tree back to the last known-good main. main
/// never advances on a failed build, so a crash-and-recover doesn't /// never advances on a failed build, so a crash-and-recover doesn't
/// leave the agent pointing at a tree it can't evaluate. /// leave the agent pointing at a tree it can't evaluate.
#[allow(clippy::too_many_lines)] // sequential build/tag/notify pipeline; splitting would obscure the flow
async fn run_apply_commit( async fn run_apply_commit(
coord: &Arc<Coordinator>, coord: &Arc<Coordinator>,
approval: &hive_sh4re::Approval, approval: &hive_sh4re::Approval,

View file

@ -609,6 +609,7 @@ impl Broker {
/// in the last `since_secs` seconds (0 = all reminders). /// in the last `since_secs` seconds (0 = all reminders).
pub fn reminder_rollup_for(&self, agent: &str, since_secs: u64) -> Result<hive_sh4re::ReminderStats> { pub fn reminder_rollup_for(&self, agent: &str, since_secs: u64) -> Result<hive_sh4re::ReminderStats> {
let conn = self.conn.lock().unwrap(); let conn = self.conn.lock().unwrap();
#[allow(clippy::cast_possible_wrap)] // unix epoch secs fit in i64 until year 292B
let cutoff_time = if since_secs > 0 { let cutoff_time = if since_secs > 0 {
let now = std::time::SystemTime::now() let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH) .duration_since(std::time::UNIX_EPOCH)

View file

@ -164,8 +164,8 @@ fn is_rate_limited(name: &str) -> bool {
/// silently yields `None` so a missing/corrupt file never blocks /// silently yields `None` so a missing/corrupt file never blocks
/// `build_all`. /// `build_all`.
/// ///
/// Context tokens = `last_input_tokens + last_cache_read_input_tokens /// Context tokens are the sum of `last_input_tokens`, `last_cache_read_input_tokens`,
/// + last_cache_creation_input_tokens`, mirroring /// and `last_cache_creation_input_tokens`, mirroring
/// `hive_ag3nt::events::TokenUsage::context_tokens`. /// `hive_ag3nt::events::TokenUsage::context_tokens`.
fn read_last_ctx_tokens(name: &str) -> Option<u64> { fn read_last_ctx_tokens(name: &str) -> Option<u64> {
let path = Coordinator::agent_notes_dir(name).join("hyperhive-turn-stats.sqlite"); let path = Coordinator::agent_notes_dir(name).join("hyperhive-turn-stats.sqlite");

View file

@ -23,7 +23,7 @@ const KEEP_SECS: i64 = 7 * 24 * 3600;
/// Background loop: sweep every existing agent state dir hourly, run /// Background loop: sweep every existing agent state dir hourly, run
/// the vacuum SQL against its events.sqlite if present. Errors are /// the vacuum SQL against its events.sqlite if present. Errors are
/// logged but don't tear the loop down. /// logged but don't tear the loop down.
pub fn spawn(coord: Arc<Coordinator>) { pub fn spawn(coord: &Arc<Coordinator>) {
let mut shutdown = coord.shutdown_rx(); let mut shutdown = coord.shutdown_rx();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {

View file

@ -767,69 +767,6 @@ async fn systemd_daemon_reload() -> Result<()> {
Ok(()) Ok(())
} }
#[cfg(test)]
mod tests {
use super::*;
/// Regression test: setup_proposed must seed both agent.nix and flake.nix
/// in the initial commit. Before commit 5b5a93e flake.nix was missing from
/// the scaffold, requiring manual creation (seen with the damocles agent).
#[tokio::test]
async fn setup_proposed_seeds_flake_nix() {
let dir = tempfile::tempdir().expect("tempdir");
let proposed = dir.path().join("proposed");
setup_proposed(&proposed, "test-agent")
.await
.expect("setup_proposed");
// Both files must exist on disk.
assert!(proposed.join("agent.nix").exists(), "agent.nix missing");
assert!(proposed.join("flake.nix").exists(), "flake.nix missing");
// flake.nix must export nixosModules.default (the meta-flake contract).
let flake = std::fs::read_to_string(proposed.join("flake.nix")).unwrap();
assert!(
flake.contains("nixosModules.default"),
"flake.nix does not export nixosModules.default"
);
// Both files must be tracked in the initial git commit.
let out = git_command()
.current_dir(&proposed)
.args(["show", "--name-only", "--format=", "HEAD"])
.output()
.await
.expect("git show");
let tracked = String::from_utf8_lossy(&out.stdout);
assert!(tracked.contains("agent.nix"), "agent.nix not committed");
assert!(tracked.contains("flake.nix"), "flake.nix not committed");
}
/// setup_proposed is idempotent: calling it on an existing repo is a
/// no-op (the fresh guard skips all writes).
#[tokio::test]
async fn setup_proposed_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let proposed = dir.path().join("proposed");
setup_proposed(&proposed, "test-agent")
.await
.expect("first call");
// Second call must not error even though .git already exists.
setup_proposed(&proposed, "test-agent")
.await
.expect("second call");
// Still one commit.
let out = git_command()
.current_dir(&proposed)
.args(["rev-list", "--count", "HEAD"])
.output()
.await
.expect("git rev-list");
let count = String::from_utf8_lossy(&out.stdout).trim().to_owned();
assert_eq!(count, "1", "expected exactly one commit after idempotent call");
}
}
/// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf` /// Idempotently rewrite the lines in `/etc/nixos-containers/<container>.conf`
/// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port /// that hive-c0re owns: `PRIVATE_NETWORK` (forced 0 so the agent's web UI port
/// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind). /// is reachable on the host) and `EXTRA_NSPAWN_FLAGS` (the runtime-dir bind).
@ -1097,3 +1034,66 @@ async fn container_journal_tail(args: &[&str]) -> String {
_ => String::new(), _ => String::new(),
} }
} }
#[cfg(test)]
mod tests {
use super::*;
/// Regression test: `setup_proposed` must seed both agent.nix and flake.nix
/// in the initial commit. Before commit 5b5a93e flake.nix was missing from
/// the scaffold, requiring manual creation (seen with the damocles agent).
#[tokio::test]
async fn setup_proposed_seeds_flake_nix() {
let dir = tempfile::tempdir().expect("tempdir");
let proposed = dir.path().join("proposed");
setup_proposed(&proposed, "test-agent")
.await
.expect("setup_proposed");
// Both files must exist on disk.
assert!(proposed.join("agent.nix").exists(), "agent.nix missing");
assert!(proposed.join("flake.nix").exists(), "flake.nix missing");
// flake.nix must export nixosModules.default (the meta-flake contract).
let flake = std::fs::read_to_string(proposed.join("flake.nix")).unwrap();
assert!(
flake.contains("nixosModules.default"),
"flake.nix does not export nixosModules.default"
);
// Both files must be tracked in the initial git commit.
let out = git_command()
.current_dir(&proposed)
.args(["show", "--name-only", "--format=", "HEAD"])
.output()
.await
.expect("git show");
let tracked = String::from_utf8_lossy(&out.stdout);
assert!(tracked.contains("agent.nix"), "agent.nix not committed");
assert!(tracked.contains("flake.nix"), "flake.nix not committed");
}
/// `setup_proposed` is idempotent: calling it on an existing repo is a
/// no-op (the fresh guard skips all writes).
#[tokio::test]
async fn setup_proposed_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let proposed = dir.path().join("proposed");
setup_proposed(&proposed, "test-agent")
.await
.expect("first call");
// Second call must not error even though .git already exists.
setup_proposed(&proposed, "test-agent")
.await
.expect("second call");
// Still one commit.
let out = git_command()
.current_dir(&proposed)
.args(["rev-list", "--count", "HEAD"])
.output()
.await
.expect("git rev-list");
let count = String::from_utf8_lossy(&out.stdout).trim().to_owned();
assert_eq!(count, "1", "expected exactly one commit after idempotent call");
}
}

View file

@ -100,6 +100,7 @@ enum Cmd {
} }
#[tokio::main] #[tokio::main]
#[allow(clippy::too_many_lines)] // top-level dispatch; hard to split without losing readability
async fn main() -> Result<()> { async fn main() -> Result<()> {
tracing_subscriber::fmt() tracing_subscriber::fmt()
.with_env_filter( .with_env_filter(
@ -186,10 +187,10 @@ async fn main() -> Result<()> {
}); });
// Per-agent events.sqlite vacuum: host-side so the harness // Per-agent events.sqlite vacuum: host-side so the harness
// doesn't need any retention wiring of its own. // doesn't need any retention wiring of its own.
events_vacuum::spawn(coord.clone()); events_vacuum::spawn(&coord);
// Per-agent turn-stats.sqlite vacuum: same pattern, 90-day // Per-agent turn-stats.sqlite vacuum: same pattern, 90-day
// retention so trend analysis has enough history. // retention so trend analysis has enough history.
stats_vacuum::spawn(coord.clone()); stats_vacuum::spawn(&coord);
// Container crash watcher: emits HelperEvent::ContainerCrash // Container crash watcher: emits HelperEvent::ContainerCrash
// when a previously-running container goes away without an // when a previously-running container goes away without an
// operator-initiated transient state. // operator-initiated transient state.

View file

@ -21,7 +21,7 @@ const KEEP_SECS: i64 = 90 * 24 * 3600;
/// Background loop: sweep every existing agent state dir hourly, run /// Background loop: sweep every existing agent state dir hourly, run
/// the vacuum SQL against its turn-stats.sqlite if present. Errors /// the vacuum SQL against its turn-stats.sqlite if present. Errors
/// are logged but don't tear the loop down. /// are logged but don't tear the loop down.
pub fn spawn(coord: Arc<Coordinator>) { pub fn spawn(coord: &Arc<Coordinator>) {
let mut shutdown = coord.shutdown_rx(); let mut shutdown = coord.shutdown_rx();
tokio::spawn(async move { tokio::spawn(async move {
loop { loop {