huskies: merge 1212 story Sled chat messages show "workspace" instead of the real project name

This commit is contained in:
Huskies Agent
2026-07-18 10:45:40 +00:00
parent 36ec6d0f93
commit b0f19eb0c5
4 changed files with 123 additions and 14 deletions
@@ -21,10 +21,10 @@ use super::super::format::{
format_disk_recovery_notification, format_disk_warning_notification, format_error_notification,
format_merge_auto_retry_notification, format_new_items_notification,
format_oauth_account_swapped, format_oauth_accounts_exhausted, format_rate_limit_notification,
project_display_name, truncate_gate_output,
truncate_gate_output,
};
use super::super::route::rooms_for_notification;
use super::{find_story_name_any_stage, read_story_name};
use super::{find_story_name_any_stage, read_story_name, resolve_project_display_name};
/// Format and send any pending new-item-creation notifications as a single
/// combined message, then clear the pending buffer.
@@ -70,8 +70,7 @@ pub fn spawn_notification_listener(
let mut rx = watcher_rx;
// Load initial config; re-loaded on ConfigChanged events.
let mut config = ProjectConfig::load(&project_root).unwrap_or_default();
let mut project_name =
project_display_name(config.gateway_project.as_deref(), &project_root);
let mut project_name = resolve_project_display_name(&config, &project_root);
// Tracks when a rate-limit notification was last sent for each
// "story_id:agent_name" key, to debounce repeated warnings.
let mut rate_limit_last_notified: HashMap<String, Instant> = HashMap::new();
@@ -465,8 +464,7 @@ pub fn spawn_notification_listener(
EventAction::ReloadConfig => {
if let Ok(new_cfg) = ProjectConfig::load(&project_root) {
config = new_cfg;
project_name =
project_display_name(config.gateway_project.as_deref(), &project_root);
project_name = resolve_project_display_name(&config, &project_root);
}
}
EventAction::Skip => {}
+112
View File
@@ -6,6 +6,10 @@
use std::path::Path;
use crate::config::ProjectConfig;
use super::format::project_display_name;
mod listener;
pub use listener::spawn_notification_listener;
@@ -39,3 +43,111 @@ pub fn read_story_name(_project_root: &Path, _stage: &str, item_id: &str) -> Str
fn find_story_name_any_stage(project_root: &Path, item_id: &str) -> String {
read_story_name(project_root, "", item_id)
}
/// Resolve the human-readable project display name using the full precedence
/// chain (story 1212): config `gateway_project` → `HUSKIES_GATEWAY_PROJECT`
/// env → `HUSKIES_PROJECT_NAME` env → project-root directory basename →
/// literal `"project"`.
///
/// Shared by the notification listener (initial load and `ReloadConfig`) and
/// the gateway relay startup path so precedence cannot drift between them —
/// the env-var overrides matter because sled containers commonly mount the
/// project at a generic path (e.g. `/workspace`), so the directory-basename
/// fallback alone produces a useless display name.
pub fn resolve_project_display_name(config: &ProjectConfig, project_root: &Path) -> String {
let gateway_project = config
.gateway_project
.clone()
.or_else(|| std::env::var("HUSKIES_GATEWAY_PROJECT").ok())
.or_else(|| std::env::var("HUSKIES_PROJECT_NAME").ok());
project_display_name(gateway_project.as_deref(), project_root)
}
#[cfg(test)]
mod resolve_project_display_name_tests {
use super::*;
/// `HUSKIES_GATEWAY_PROJECT` and `HUSKIES_PROJECT_NAME` are read by
/// `resolve_project_display_name` and are process-global, so tests that
/// touch them must not run concurrently with each other.
fn env_test_lock() -> std::sync::MutexGuard<'static, ()> {
static LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
LOCK.lock().unwrap_or_else(|p| p.into_inner())
}
#[test]
fn falls_back_to_huskies_project_name_when_config_and_gateway_env_absent() {
let _guard = env_test_lock();
// SAFETY: serialised by `env_test_lock` — no other test in this
// process reads/writes these two vars concurrently.
unsafe {
std::env::remove_var("HUSKIES_GATEWAY_PROJECT");
std::env::set_var("HUSKIES_PROJECT_NAME", "my-sled-project");
}
let config = ProjectConfig::default();
let root = Path::new("/workspace");
let result = resolve_project_display_name(&config, root);
unsafe {
std::env::remove_var("HUSKIES_PROJECT_NAME");
}
assert_eq!(result, "my-sled-project");
}
#[test]
fn config_gateway_project_wins_over_all_env_vars() {
let _guard = env_test_lock();
unsafe {
std::env::set_var("HUSKIES_GATEWAY_PROJECT", "env-gateway-project");
std::env::set_var("HUSKIES_PROJECT_NAME", "env-project-name");
}
let config = ProjectConfig {
gateway_project: Some("configured-name".to_string()),
..Default::default()
};
let result = resolve_project_display_name(&config, Path::new("/workspace"));
unsafe {
std::env::remove_var("HUSKIES_GATEWAY_PROJECT");
std::env::remove_var("HUSKIES_PROJECT_NAME");
}
assert_eq!(result, "configured-name");
}
#[test]
fn huskies_gateway_project_env_wins_over_huskies_project_name_env() {
let _guard = env_test_lock();
unsafe {
std::env::set_var("HUSKIES_GATEWAY_PROJECT", "env-gateway-project");
std::env::set_var("HUSKIES_PROJECT_NAME", "env-project-name");
}
let config = ProjectConfig::default();
let result = resolve_project_display_name(&config, Path::new("/workspace"));
unsafe {
std::env::remove_var("HUSKIES_GATEWAY_PROJECT");
std::env::remove_var("HUSKIES_PROJECT_NAME");
}
assert_eq!(result, "env-gateway-project");
}
#[test]
fn falls_back_to_directory_basename_when_nothing_configured() {
let _guard = env_test_lock();
unsafe {
std::env::remove_var("HUSKIES_GATEWAY_PROJECT");
std::env::remove_var("HUSKIES_PROJECT_NAME");
}
let config = ProjectConfig::default();
let result = resolve_project_display_name(&config, Path::new("/tmp/my-project-dir"));
assert_eq!(result, "my-project-dir");
}
}
+1
View File
@@ -22,6 +22,7 @@ pub use format::{
format_blocked_notification, format_disk_recovery_notification,
format_disk_warning_notification, format_error_notification, format_stage_notification,
};
pub use io::resolve_project_display_name;
pub use io::spawn_notification_listener;
pub use io::spawn_stage_notification_subscriber;