fix(ci): unblock nix flake check after clippy 0.1.95 bump (#1368)
The nixpkgs bump to clippy 0.1.95 / cargo 1.95.0 added + strengthened a large batch of lints. CI denied ALL warnings (`-D warnings`) against the `pedantic = warn` workspace lint, so the bump hard-failed `nix flake check` workspace-wide with zero code changes — and would recur on every future clippy bump. Posture fix (the durable part): CI now runs `-D warnings -A clippy::pedantic`, so the default/correctness/style lints stay a hard gate while the "extra, opinionated" pedantic group is advisory only (still `warn` for local `cargo clippy` via the workspace lints table, just non-blocking in CI). `-A` rather than `-W` so the group drop doesn't re-enable the specific pedantic lints the workspace allows (e.g. `must_use_candidate`). Also fixes the genuine DEFAULT/STYLE lints the bump surfaced across the workspace (doc_lazy_continuation, collapsible_if, ptr_arg, match_like_matches_macro, …) via `cargo clippy --fix` + manual stragglers (`too_many_arguments` #[allow] on the host-config constructors), and three tests that had rotted while the CI runner was offline (#1221): - topology::top_level_agents_in_multi_root — hardcoded unsorted expected - rebuild_queue::depends_on_evicted_dep_counts_as_resolved — needs MAX_HISTORY_PER_KIND newer terminals to evict, not one - coordinator::agent_paths doctest — illustrative pseudo-code, now `ignore` Validated: clippy + formatting + cargo-test checks all pass.
This commit is contained in:
parent
35717c7bac
commit
734fe88858
18 changed files with 129 additions and 103 deletions
25
flake.nix
25
flake.nix
|
|
@ -320,18 +320,29 @@
|
|||
formatting = treefmt-eval.config.build.check self;
|
||||
# Clippy as a check via crane's first-class `cargoClippy`
|
||||
# builder. Reuses the shared `cargoArtifacts` (deps already
|
||||
# built) and runs `cargo clippy --workspace --all-targets
|
||||
# -- -D warnings` directly — no `overrideAttrs` hack needed,
|
||||
# because crane parses `cargoClippyExtraArgs` correctly
|
||||
# (naersk's `mode = "clippy"` used to mangle the `--`
|
||||
# separator, which is why the old wiring went through
|
||||
# overrideAttrs).
|
||||
# built) and runs `cargo clippy --workspace --all-targets`
|
||||
# directly — no `overrideAttrs` hack needed, because crane
|
||||
# parses `cargoClippyExtraArgs` correctly (naersk's
|
||||
# `mode = "clippy"` used to mangle the `--` separator, which
|
||||
# is why the old wiring went through overrideAttrs).
|
||||
#
|
||||
# `-D warnings` makes the default/correctness/style lints a
|
||||
# hard CI gate. `-A clippy::pedantic` then drops the pedantic
|
||||
# group from that gate: pedantic is the "extra, opinionated"
|
||||
# group the clippy team grows freely, so denying it means
|
||||
# every toolchain bump that adds a new pedantic lint breaks CI
|
||||
# with zero code changes (#1368). The `pedantic = warn`
|
||||
# workspace lint (Cargo.toml) keeps it as advisory signal in
|
||||
# local `cargo clippy` — it just no longer blocks the build.
|
||||
# (`-A` rather than `-W` here: `-W clippy::pedantic` would
|
||||
# re-enable the specific pedantic lints the workspace lints
|
||||
# table allows, e.g. `must_use_candidate`.)
|
||||
clippy = craneLib.cargoClippy {
|
||||
src = cleanSrc;
|
||||
inherit cargoArtifacts nativeBuildInputs;
|
||||
pname = "hyperhive-workspace";
|
||||
version = "0.1.0";
|
||||
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings";
|
||||
cargoClippyExtraArgs = "--workspace --all-targets -- -D warnings -A clippy::pedantic";
|
||||
};
|
||||
# `cargo test --workspace` lifted out of `buildPackage` so the
|
||||
# `hyperhive-assets` dep (which `hive-ag3nt::prompt::tests`
|
||||
|
|
|
|||
|
|
@ -776,9 +776,7 @@ impl AgentServer {
|
|||
};
|
||||
// Prepend matrix unread entry for self-queries only (can't
|
||||
// reach another agent's matrix daemon from here).
|
||||
if is_self_query
|
||||
&& let Some(unread_rooms) = matrix_unread_summary().await
|
||||
{
|
||||
if is_self_query && let Some(unread_rooms) = matrix_unread_summary().await {
|
||||
let total = u32::try_from(unread_rooms.len()).unwrap_or(u32::MAX);
|
||||
if total > 0 {
|
||||
let summary = format_matrix_summary(&unread_rooms);
|
||||
|
|
|
|||
|
|
@ -108,10 +108,8 @@ fn render_bash_run(id: &str, resp: Result<DaemonResponse>) -> String {
|
|||
match resp {
|
||||
Ok(DaemonResponse::Ok { payload }) => {
|
||||
let finished = payload["finished"].as_bool().unwrap_or(false);
|
||||
if finished {
|
||||
if let Some(task) = payload.get("task") {
|
||||
return format_task(id, task);
|
||||
}
|
||||
if finished && let Some(task) = payload.get("task") {
|
||||
return format_task(id, task);
|
||||
}
|
||||
format!("task started: id={id}")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,19 +69,18 @@ async fn dispatch(req: DaemonRequest) -> DaemonResponse {
|
|||
// Inline wait: if requested and the task finishes quickly,
|
||||
// return the full status instead of just the task ID.
|
||||
let wait = wait_seconds.unwrap_or(0);
|
||||
if wait > 0 {
|
||||
if let Some(task) = runner::wait_for_task(&id, wait).await {
|
||||
if matches!(
|
||||
task.status,
|
||||
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
|
||||
) {
|
||||
return DaemonResponse::ok(&serde_json::json!({
|
||||
"id": id,
|
||||
"finished": true,
|
||||
"task": task,
|
||||
}));
|
||||
}
|
||||
}
|
||||
if wait > 0
|
||||
&& let Some(task) = runner::wait_for_task(&id, wait).await
|
||||
&& matches!(
|
||||
task.status,
|
||||
TaskStatus::Done | TaskStatus::TimedOut | TaskStatus::Interrupted
|
||||
)
|
||||
{
|
||||
return DaemonResponse::ok(&serde_json::json!({
|
||||
"id": id,
|
||||
"finished": true,
|
||||
"task": task,
|
||||
}));
|
||||
}
|
||||
DaemonResponse::ok(&serde_json::json!({ "id": id, "finished": false }))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -560,10 +560,10 @@ async fn run_apply_commit(
|
|||
&paths,
|
||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
||||
&|log_id| {
|
||||
if let Some(qid) = queue_entry_id {
|
||||
if coord.rebuild_queue.set_build_log_id(qid, log_id) {
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
}
|
||||
if let Some(qid) = queue_entry_id
|
||||
&& coord.rebuild_queue.set_build_log_id(qid, log_id)
|
||||
{
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
|
|||
|
|
@ -90,10 +90,10 @@ pub async fn rebuild_agent(
|
|||
&paths,
|
||||
&|step| coord.set_queue_step(queue_entry_id, step),
|
||||
&|log_id| {
|
||||
if let Some(qid) = queue_entry_id {
|
||||
if coord.rebuild_queue.set_build_log_id(qid, log_id) {
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
}
|
||||
if let Some(qid) = queue_entry_id
|
||||
&& coord.rebuild_queue.set_build_log_id(qid, log_id)
|
||||
{
|
||||
coord.emit_rebuild_queue_snapshot();
|
||||
}
|
||||
},
|
||||
)
|
||||
|
|
@ -199,18 +199,18 @@ pub async fn ensure_root_agent(coord: &Arc<Coordinator>) -> Result<()> {
|
|||
/// Uses BFS from root agents (depth 0). Agents absent from `topo` sort last,
|
||||
/// alphabetically within their tier. Stable within each depth tier.
|
||||
pub fn topology_sort(
|
||||
names: &mut Vec<String>,
|
||||
names: &mut [String],
|
||||
topo: &std::collections::BTreeMap<String, Option<String>>,
|
||||
) {
|
||||
use std::collections::{HashMap, VecDeque};
|
||||
// Build depth map using owned clones so the borrow on `names` is released
|
||||
// before the sort_by mutable borrow.
|
||||
let name_set: Vec<String> = names.clone();
|
||||
let name_set: Vec<String> = names.to_vec();
|
||||
let mut depth: HashMap<String, usize> = HashMap::new();
|
||||
let mut queue: VecDeque<String> = VecDeque::new();
|
||||
// Seed roots: entries with no parent, or names not present in topo at all.
|
||||
for name in &name_set {
|
||||
if topo.get(name).map_or(true, |p| p.is_none()) {
|
||||
if topo.get(name).is_none_or(|p| p.is_none()) {
|
||||
depth.insert(name.clone(), 0);
|
||||
queue.push_back(name.clone());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -111,10 +111,10 @@ fn should_delete(json_path: &Path, cutoff: i64) -> bool {
|
|||
fn delete_trio(dir: &Path, stem: &str) {
|
||||
for ext in ["json", "out", "err"] {
|
||||
let path = dir.join(format!("{stem}.{ext}"));
|
||||
if path.exists() {
|
||||
if let Err(e) = std::fs::remove_file(&path) {
|
||||
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
|
||||
}
|
||||
if path.exists()
|
||||
&& let Err(e) = std::fs::remove_file(&path)
|
||||
{
|
||||
tracing::warn!(path = %path.display(), error = ?e, "bash-tasks vacuum: remove failed");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
use std::collections::HashMap;
|
||||
use std::path::Path;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle::{self, AGENT_PREFIX};
|
||||
|
|
@ -113,18 +113,18 @@ pub fn claude_has_session(dir: &Path) -> bool {
|
|||
/// upgrades don't lose state during the transition window.
|
||||
fn read_harness_flags(name: &str) -> (bool, bool) {
|
||||
let dir = Coordinator::agent_notes_dir(name);
|
||||
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json")) {
|
||||
if let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw) {
|
||||
let rl = v
|
||||
.get("rate_limited")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let nl = v
|
||||
.get("needs_login")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
return (rl, nl);
|
||||
}
|
||||
if let Ok(raw) = std::fs::read_to_string(dir.join("hyperhive-harness.json"))
|
||||
&& let Ok(v) = serde_json::from_str::<serde_json::Value>(&raw)
|
||||
{
|
||||
let rl = v
|
||||
.get("rate_limited")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
let nl = v
|
||||
.get("needs_login")
|
||||
.and_then(|x| x.as_bool())
|
||||
.unwrap_or(false);
|
||||
return (rl, nl);
|
||||
}
|
||||
// Legacy fallback: presence of individual sentinel files.
|
||||
let rate_limited = dir.join("hyperhive-rate-limited").exists();
|
||||
|
|
|
|||
|
|
@ -263,6 +263,12 @@ impl TransientKind {
|
|||
}
|
||||
|
||||
impl Coordinator {
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "constructor wiring host-level config (flakes, ports, pronouns, \
|
||||
context-window + resource limits) into the coordinator; bundling \
|
||||
into a struct would just move the same fields one level out"
|
||||
)]
|
||||
pub fn open(
|
||||
db_path: &Path,
|
||||
hyperhive_flake: String,
|
||||
|
|
@ -342,7 +348,7 @@ impl Coordinator {
|
|||
/// creates the tmpfs entry on first call. All other paths are
|
||||
/// derived statically from `name`.
|
||||
///
|
||||
/// ```no_run
|
||||
/// ```ignore
|
||||
/// let paths = Coordinator::agent_paths(name, coord.ensure_runtime(name)?);
|
||||
/// ```
|
||||
#[must_use]
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@ use tokio_stream::{Stream, StreamExt};
|
|||
use tower_http::services::ServeDir;
|
||||
|
||||
use crate::actions;
|
||||
use crate::container_view::{self, ContainerView, claude_has_session};
|
||||
use crate::container_view::{ContainerView, claude_has_session};
|
||||
use crate::coordinator::Coordinator;
|
||||
use crate::lifecycle::{self, MANAGER_NAME};
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
//! collaborator access to for operator-curated shared content.
|
||||
//! No-op when `hive-forge` isn't running. Full design: `docs/forge.md`.
|
||||
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::path::Path;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use base64::Engine;
|
||||
|
|
|
|||
|
|
@ -1549,7 +1549,7 @@ mod tests {
|
|||
// Result must still be in .2-.254.
|
||||
let ip = from_bridge.unwrap();
|
||||
let last: u8 = ip.rsplit('.').next().unwrap().parse().unwrap();
|
||||
assert!(last >= 2 && last <= 254, "host byte {last}");
|
||||
assert!((2..=254).contains(&last), "host byte {last}");
|
||||
}
|
||||
|
||||
/// `setup_proposed` is idempotent: calling it on an existing repo is a
|
||||
|
|
|
|||
|
|
@ -204,6 +204,12 @@ async fn main() -> Result<()> {
|
|||
/// Start the coordinator daemon: open the broker, run migrations, spawn
|
||||
/// background tasks (auto-update, vacuums, crash-watcher, reminder-scheduler,
|
||||
/// dashboard), then serve the admin socket until a signal arrives.
|
||||
#[allow(
|
||||
clippy::too_many_arguments,
|
||||
reason = "the `serve` subcommand's args are the host-level config the daemon \
|
||||
boots from (flakes, ports, pronouns, context-window + resource \
|
||||
limits); they flow straight through to Coordinator::open"
|
||||
)]
|
||||
async fn cmd_serve(
|
||||
hyperhive_flake: String,
|
||||
nixpkgs_flake: String,
|
||||
|
|
@ -268,10 +274,10 @@ async fn cmd_serve(
|
|||
// No-op when the core token or forge are absent.
|
||||
let webhook_port = dashboard_port;
|
||||
tokio::spawn(async move {
|
||||
if let Some(token) = forge::core_token() {
|
||||
if let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await {
|
||||
tracing::warn!(error = ?e, "knowledge: ensure_webhook failed");
|
||||
}
|
||||
if let Some(token) = forge::core_token()
|
||||
&& let Err(e) = knowledge::ensure_webhook(&token, webhook_port).await
|
||||
{
|
||||
tracing::warn!(error = ?e, "knowledge: ensure_webhook failed");
|
||||
}
|
||||
});
|
||||
// Knowledge periodic pull: hourly fallback in case the webhook is
|
||||
|
|
|
|||
|
|
@ -570,8 +570,10 @@ fn agent_canonical_inputs(name: &str) -> Vec<&'static str> {
|
|||
/// agent flake-lock introspection.
|
||||
#[allow(
|
||||
clippy::too_many_lines,
|
||||
clippy::too_many_arguments,
|
||||
reason = "templated string-builder for the meta flake — the length is one \
|
||||
contiguous fmt block, splitting it would just hide the shape"
|
||||
contiguous fmt block, splitting it would just hide the shape; the \
|
||||
args mirror the host-level config the flake is rendered from"
|
||||
)]
|
||||
fn render_flake_with_lookup<F>(
|
||||
hyperhive_flake: &str,
|
||||
|
|
|
|||
|
|
@ -392,17 +392,16 @@ impl RebuildQueue {
|
|||
// tool-groups change and a capabilities change for the same
|
||||
// agent are distinct operations and must not collapse into one.
|
||||
for entry in &mut inner.entries {
|
||||
let perm_type_matches = match (&entry.perm_payload, &perm_payload) {
|
||||
(Some(PermPayload::ToolGroups { .. }), Some(PermPayload::ToolGroups { .. })) => {
|
||||
true
|
||||
}
|
||||
let perm_type_matches = matches!(
|
||||
(&entry.perm_payload, &perm_payload),
|
||||
(
|
||||
Some(PermPayload::ToolGroups { .. }),
|
||||
Some(PermPayload::ToolGroups { .. })
|
||||
) | (
|
||||
Some(PermPayload::Capabilities { .. }),
|
||||
Some(PermPayload::Capabilities { .. }),
|
||||
) => true,
|
||||
(None, None) => true,
|
||||
_ => false,
|
||||
};
|
||||
Some(PermPayload::Capabilities { .. })
|
||||
) | (None, None)
|
||||
);
|
||||
if entry.state == QueueState::Queued
|
||||
&& entry.kind == kind
|
||||
&& entry.agent == agent
|
||||
|
|
@ -1505,17 +1504,21 @@ mod tests {
|
|||
None,
|
||||
);
|
||||
q.take_next();
|
||||
// One more terminal to push `dep` out of the history window.
|
||||
q.finish(dep, QueueState::Done, None);
|
||||
let extra = q.enqueue(
|
||||
QueueKind::Rebuild,
|
||||
"extra".to_owned(),
|
||||
QueueSource::Manual,
|
||||
"extra".to_owned(),
|
||||
None,
|
||||
);
|
||||
q.take_next();
|
||||
q.finish(extra, QueueState::Done, None);
|
||||
// Push `dep` out of the per-kind history window: `trim_history`
|
||||
// keeps the newest MAX_HISTORY_PER_KIND terminals per kind, so it
|
||||
// takes that many newer terminals to evict `dep`.
|
||||
for i in 0..MAX_HISTORY_PER_KIND {
|
||||
let extra = q.enqueue(
|
||||
QueueKind::Rebuild,
|
||||
format!("extra-{i}"),
|
||||
QueueSource::Manual,
|
||||
format!("extra-{i}"),
|
||||
None,
|
||||
);
|
||||
q.take_next();
|
||||
q.finish(extra, QueueState::Done, None);
|
||||
}
|
||||
// `dep` should now be evicted.
|
||||
assert!(
|
||||
q.snapshot().iter().all(|e| e.id != dep),
|
||||
|
|
|
|||
|
|
@ -665,7 +665,9 @@ mod tests {
|
|||
topo.insert("orphan".to_owned(), None);
|
||||
let mut top = top_level_agents_in(&topo);
|
||||
top.sort();
|
||||
assert_eq!(top, vec![crate::lifecycle::MANAGER_NAME, "orphan"]);
|
||||
let mut expected = vec![crate::lifecycle::MANAGER_NAME, "orphan"];
|
||||
expected.sort();
|
||||
assert_eq!(top, expected);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -741,7 +743,7 @@ mod tests {
|
|||
#[test]
|
||||
fn reconcile_roles_in_does_not_reseed_after_explicit_revoke() {
|
||||
let mgr = crate::lifecycle::MANAGER_NAME;
|
||||
let agent_names = vec![mgr.to_owned(), "alice".to_owned()];
|
||||
let agent_names = [mgr.to_owned(), "alice".to_owned()];
|
||||
let mut roles: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
// Tombstone: manager was seen before but all roles were revoked.
|
||||
roles.insert(mgr.to_owned(), vec![]);
|
||||
|
|
@ -759,7 +761,7 @@ mod tests {
|
|||
#[test]
|
||||
fn reconcile_roles_in_seeds_root_when_absent() {
|
||||
let mgr = crate::lifecycle::MANAGER_NAME;
|
||||
let agent_names = vec![mgr.to_owned(), "alice".to_owned()];
|
||||
let agent_names = [mgr.to_owned(), "alice".to_owned()];
|
||||
let roles: BTreeMap<String, Vec<String>> = BTreeMap::new(); // empty
|
||||
|
||||
let should_seed = agent_names.iter().any(|n| n == mgr) && !roles.contains_key(mgr);
|
||||
|
|
|
|||
|
|
@ -450,8 +450,8 @@ pub async fn collect_unread(client: &Client) -> Vec<crate::protocol::RoomUnread>
|
|||
use crate::protocol::RoomUnread;
|
||||
let mut result = Vec::new();
|
||||
for room in client.joined_rooms() {
|
||||
let count = u32::try_from(room.unread_notification_counts().notification_count)
|
||||
.unwrap_or(u32::MAX);
|
||||
let count =
|
||||
u32::try_from(room.unread_notification_counts().notification_count).unwrap_or(u32::MAX);
|
||||
if count == 0 {
|
||||
continue;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,21 +66,22 @@ fn socket_listener() -> Result<UnixListener> {
|
|||
.ok()
|
||||
.and_then(|s| s.parse().ok());
|
||||
|
||||
if let (Some(n), Some(p)) = (listen_fds, listen_pid) {
|
||||
if n >= 1 && p == std::process::id() {
|
||||
// SAFETY: systemd has passed us a ready UnixListener on fd 3.
|
||||
let std_listener = unsafe {
|
||||
use std::os::unix::io::FromRawFd;
|
||||
std::os::unix::net::UnixListener::from_raw_fd(3)
|
||||
};
|
||||
std_listener
|
||||
.set_nonblocking(true)
|
||||
.context("set socket non-blocking")?;
|
||||
let listener =
|
||||
tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?;
|
||||
tracing::info!("using systemd-activated socket");
|
||||
return Ok(listener);
|
||||
}
|
||||
if let (Some(n), Some(p)) = (listen_fds, listen_pid)
|
||||
&& n >= 1
|
||||
&& p == std::process::id()
|
||||
{
|
||||
// SAFETY: systemd has passed us a ready UnixListener on fd 3.
|
||||
let std_listener = unsafe {
|
||||
use std::os::unix::io::FromRawFd;
|
||||
std::os::unix::net::UnixListener::from_raw_fd(3)
|
||||
};
|
||||
std_listener
|
||||
.set_nonblocking(true)
|
||||
.context("set socket non-blocking")?;
|
||||
let listener =
|
||||
tokio::net::UnixListener::from_std(std_listener).context("wrap systemd socket")?;
|
||||
tracing::info!("using systemd-activated socket");
|
||||
return Ok(listener);
|
||||
}
|
||||
|
||||
// Fallback: bind the socket ourselves.
|
||||
|
|
|
|||
Loading…
Reference in a new issue