huskies: merge 1163 story Replace perm_rx lock-as-presence-signal with a permission router

This commit is contained in:
Huskies Agent
2026-07-16 13:19:20 +00:00
parent 6f8a8ffd87
commit 18b065f77a
26 changed files with 823 additions and 293 deletions
+18 -25
View File
@@ -118,13 +118,14 @@ fn truncate_lines(mut lines: Vec<HealthLine>) -> Vec<HealthLine> {
// ── Individual checks ────────────────────────────────────────────────────────
/// Check the `perm_rx` receiver — PASS when the permission listener holds the lock,
/// FAIL when no task is holding it (listener has died or was never started).
/// Check the permission registry — PASS when at least one responder (e.g. the
/// Matrix permission listener) is registered, FAIL when none is (listener has
/// died or was never started).
fn check_perm_rx(ctx: &BotContext) -> HealthLine {
if ctx.services.perm_rx.try_lock().is_err() {
HealthLine::pass("perm_rx")
if ctx.services.permission_registry.is_empty() {
HealthLine::fail("perm_rx", "no responder registered", "restart bot")
} else {
HealthLine::fail("perm_rx", "listener not holding lock", "restart bot")
HealthLine::pass("perm_rx")
}
}
@@ -513,25 +514,22 @@ mod tests {
#[tokio::test]
async fn perm_rx_pass_when_locked() {
use crate::service::permission_router::{PendingPermReplies, ResponderRegistry};
use crate::services::Services;
use std::sync::Arc;
use tokio::sync::Mutex as TokioMutex;
let (perm_tx, perm_rx) = tokio::sync::mpsc::unbounded_channel();
let perm_rx_arc = Arc::new(TokioMutex::new(perm_rx));
let registry = ResponderRegistry::new();
// Register a responder to simulate the permission listener being active.
let _guard_and_rx = registry.register();
// Acquire the lock to simulate the permission listener holding it.
let _guard = perm_rx_arc.try_lock().unwrap();
// Build a minimal services bundle referencing our locked perm_rx.
let services = Arc::new(Services {
project_root: std::path::PathBuf::from("/tmp"),
agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
bot_name: "test".to_string(),
bot_user_id: "@bot:test".to_string(),
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
perm_rx: Arc::clone(&perm_rx_arc),
pending_perm_replies: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
permission_registry: registry,
pending_perm_replies: PendingPermReplies::new(),
permission_timeout_secs: 120,
status: Arc::new(crate::service::status::StatusBroadcaster::new()),
chat_dispatcher: Arc::new(crate::chat::dispatcher::ChatDispatcher::new(1_500)),
@@ -544,30 +542,25 @@ mod tests {
assert_eq!(
line.status,
Status::Pass,
"perm_rx should PASS when a task holds the lock"
"perm_rx should PASS when a responder is registered"
);
drop(perm_tx); // suppress unused warning
}
#[tokio::test]
async fn perm_rx_fail_when_unlocked() {
use crate::service::permission_router::{PendingPermReplies, ResponderRegistry};
use crate::services::Services;
use std::sync::Arc;
use tokio::sync::Mutex as TokioMutex;
let (_perm_tx, perm_rx) = tokio::sync::mpsc::unbounded_channel();
let perm_rx_arc = Arc::new(TokioMutex::new(perm_rx));
// Lock is NOT held by anyone.
// No responder registered.
let services = Arc::new(Services {
project_root: std::path::PathBuf::from("/tmp"),
agents: Arc::new(crate::agents::AgentPool::new_test(3000)),
bot_name: "test".to_string(),
bot_user_id: "@bot:test".to_string(),
ambient_rooms: Arc::new(std::sync::Mutex::new(std::collections::HashSet::new())),
perm_rx: Arc::clone(&perm_rx_arc),
pending_perm_replies: Arc::new(TokioMutex::new(std::collections::HashMap::new())),
permission_registry: ResponderRegistry::new(),
pending_perm_replies: PendingPermReplies::new(),
permission_timeout_secs: 120,
status: Arc::new(crate::service::status::StatusBroadcaster::new()),
chat_dispatcher: Arc::new(crate::chat::dispatcher::ChatDispatcher::new(1_500)),
@@ -579,7 +572,7 @@ mod tests {
assert_eq!(
line.status,
Status::Fail,
"perm_rx should FAIL when no task holds the lock"
"perm_rx should FAIL when no responder is registered"
);
}