From 7069732e0155d7616821c725154ea47cc29fb08e Mon Sep 17 00:00:00 2001 From: damocles Date: Sat, 11 Jul 2026 09:34:28 +0200 Subject: [PATCH] fix(#2369): skip statically-configured accounts in matrix token discovery to silence spurious sidecar warning --- hive-matrix-mcp/src/accounts.rs | 83 +++++++++++++++++++++++++++++---- 1 file changed, 75 insertions(+), 8 deletions(-) diff --git a/hive-matrix-mcp/src/accounts.rs b/hive-matrix-mcp/src/accounts.rs index 9b2a2bf0..03ea1077 100644 --- a/hive-matrix-mcp/src/accounts.rs +++ b/hive-matrix-mcp/src/accounts.rs @@ -100,14 +100,13 @@ pub fn configured() -> anyhow::Result> { // its `matrix-account-.json` homeserver sidecar) that aren't // already declared in config, so an account logged in via the dashboard // form works without a `matrixAccounts` edit + rebuild. Explicit config - // wins on name collision. + // wins on name collision — pass the configured set so discovery skips + // those names silently (a statically-configured account keeps its token + // on disk but is brought up from config, not discovery, so it must not + // log a spurious "no homeserver sidecar" warning). let configured: std::collections::HashSet = accounts.iter().map(|a| a.name.clone()).collect(); - for disc in discover_token_accounts() { - if !configured.contains(&disc.name) { - accounts.push(disc); - } - } + accounts.extend(discover_token_accounts(&configured)); Ok(accounts) } @@ -120,11 +119,25 @@ pub fn configured() -> anyhow::Result> { /// Best-effort: an unreadable dir or malformed sidecar yields fewer /// accounts, never an error — explicit `HIVE_MATRIX_ACCOUNTS` config stays /// authoritative. -fn discover_token_accounts() -> Vec { +/// +/// `configured` is the set of names already declared in +/// `HIVE_MATRIX_ACCOUNTS`; those are brought up from config regardless of +/// their on-disk sidecar, so discovery skips them silently rather than +/// warning about a missing sidecar for an account that isn't actually down. +fn discover_token_accounts(configured: &std::collections::HashSet) -> Vec { let token_path = paths::token_file(); let Some(state_dir) = token_path.parent() else { return Vec::new(); }; + discover_token_accounts_in(state_dir, configured) +} + +/// Body of [`discover_token_accounts`] with the state dir injected, so the +/// scan (and the configured-name skip) is unit-testable against a tempdir. +fn discover_token_accounts_in( + state_dir: &Path, + configured: &std::collections::HashSet, +) -> Vec { let Ok(rd) = std::fs::read_dir(state_dir) else { return Vec::new(); }; @@ -142,6 +155,13 @@ fn discover_token_accounts() -> Vec { if name.is_empty() { continue; } + // Already declared in config: it's brought up from `HIVE_MATRIX_ACCOUNTS`, + // not discovery, and its on-disk token needs no sidecar. Skip silently so + // a statically-configured account doesn't log a spurious missing-sidecar + // warning. + if configured.contains(name) { + continue; + } let sidecar = state_dir.join(format!("matrix-account-{name}.json")); let Some(homeserver) = read_account_homeserver(&sidecar) else { tracing::warn!( @@ -392,7 +412,54 @@ fn write_accounts_snapshot_inner( #[cfg(test)] mod tests { - use super::{AccountStatus, heartbeat_accounts_snapshot, pick_name, write_accounts_snapshot}; + use std::collections::HashSet; + + use super::{ + AccountStatus, discover_token_accounts_in, heartbeat_accounts_snapshot, pick_name, + write_accounts_snapshot, + }; + + #[test] + fn discovery_skips_configured_names_and_orphan_tokens() { + let dir = std::env::temp_dir().join(format!( + "hh-acct-disc-{}-{}", + std::process::id(), + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .unwrap() + .as_nanos() + )); + std::fs::create_dir_all(&dir).unwrap(); + + // A statically-configured account: token on disk, NO sidecar. + std::fs::write(dir.join("matrix-token-catgirl"), "tok").unwrap(); + // An orphan dashboard token: no sidecar, not configured. + std::fs::write(dir.join("matrix-token-orphan"), "tok").unwrap(); + // A fully dashboard-provisioned account: token + sidecar. + std::fs::write(dir.join("matrix-token-good"), "tok").unwrap(); + std::fs::write( + dir.join("matrix-account-good.json"), + r#"{"homeserver":"https://good.example"}"#, + ) + .unwrap(); + // The hive account's own token must never be treated as an extra. + std::fs::write(dir.join("matrix-token"), "tok").unwrap(); + + let configured: HashSet = ["main", "catgirl"] + .iter() + .map(|s| (*s).to_owned()) + .collect(); + let mut found: Vec = discover_token_accounts_in(&dir, &configured) + .into_iter() + .map(|a| a.name) + .collect(); + found.sort(); + + // catgirl (configured) + orphan (no sidecar) skipped; only `good` discovered. + assert_eq!(found, vec!["good".to_owned()]); + + std::fs::remove_dir_all(&dir).ok(); + } #[test] fn pick_name_single_account_defaults_to_primary() {