diff --git a/server/src/chat/transport/matrix/bot/format.rs b/server/src/chat/transport/matrix/bot/format.rs index 649083e4..0e4d51b4 100644 --- a/server/src/chat/transport/matrix/bot/format.rs +++ b/server/src/chat/transport/matrix/bot/format.rs @@ -1,12 +1,22 @@ //! Matrix message formatting — Markdown-to-HTML conversion and startup announcements. use pulldown_cmark::{Options, Parser, html}; -/// Format the startup greeting the bot sends to each room when it comes online. +/// Format the startup greeting the bot sends to each room when it comes online +/// via a normal (non-trampoline) process start. /// -/// Uses the bot's configured display name so the message reads naturally -/// (e.g. "Timmy is online."). -pub fn format_startup_announcement(bot_name: &str) -> String { - format!("{bot_name} is online.") +/// Uses the bot's configured display name plus the same version/git-hash/model +/// details as [`format_gateway_ready_announcement`], so a plain restart carries +/// the same diagnostic information as a trampoline-triggered rebuild instead of +/// just "Timmy is online.". `git_hash` should already have been resolved to +/// `"unknown"` by the caller if unavailable. `model` is `None` when `bot.toml` +/// has no `model` override configured, in which case the CLI's own default +/// applies. +pub fn format_startup_announcement(bot_name: &str, git_hash: &str, model: Option<&str>) -> String { + format!( + "{bot_name} is online — gateway {} ({git_hash}) — model: {}", + env!("CARGO_PKG_VERSION"), + model.unwrap_or("default") + ) } /// Format the ready announcement sent after a successful gateway trampoline restart. @@ -166,15 +176,57 @@ mod tests { #[test] fn startup_announcement_uses_bot_name() { - assert_eq!(format_startup_announcement("Timmy"), "Timmy is online."); + let msg = format_startup_announcement("Timmy", "abc1234", Some("claude-sonnet-5")); + assert!( + msg.starts_with("Timmy is online"), + "expected bot name: {msg}" + ); } #[test] fn startup_announcement_uses_configured_display_name_not_hardcoded() { - assert_eq!(format_startup_announcement("HAL"), "HAL is online."); - assert_eq!( - format_startup_announcement("Assistant"), - "Assistant is online." + let msg = format_startup_announcement("HAL", "abc1234", Some("claude-sonnet-5")); + assert!(msg.starts_with("HAL is online"), "expected bot name: {msg}"); + let msg = format_startup_announcement("Assistant", "abc1234", Some("claude-sonnet-5")); + assert!( + msg.starts_with("Assistant is online"), + "expected bot name: {msg}" + ); + } + + #[test] + fn startup_announcement_includes_version_hash_and_model() { + let msg = format_startup_announcement("Timmy", "abc1234", Some("claude-sonnet-5")); + assert!( + msg.contains(env!("CARGO_PKG_VERSION")), + "expected crate version in announcement: {msg}" + ); + assert!(msg.contains("abc1234"), "expected git hash: {msg}"); + assert!( + msg.contains("claude-sonnet-5"), + "expected configured model: {msg}" + ); + } + + #[test] + fn startup_announcement_renders_unknown_git_hash_without_suppressing_message() { + let msg = format_startup_announcement("Timmy", "unknown", Some("claude-sonnet-5")); + assert!( + msg.contains("unknown"), + "expected 'unknown' fallback for missing git hash: {msg}" + ); + assert!( + msg.starts_with("Timmy is online"), + "message must still be sent: {msg}" + ); + } + + #[test] + fn startup_announcement_falls_back_to_default_when_model_absent() { + let msg = format_startup_announcement("Timmy", "abc1234", None); + assert!( + msg.contains("default"), + "expected 'default' fallback when no model configured: {msg}" ); } diff --git a/server/src/chat/transport/matrix/bot/run.rs b/server/src/chat/transport/matrix/bot/run.rs index 884011b2..7ca2295d 100644 --- a/server/src/chat/transport/matrix/bot/run.rs +++ b/server/src/chat/transport/matrix/bot/run.rs @@ -400,9 +400,11 @@ pub async fn run_bot( // reconnects internally so this code is never reached again on a network // blip or sync resumption. // - // When started by the trampoline the message is specialised: + // A normal start and a trampoline-triggered restart both announce version, + // git hash, and configured model; the trampoline path is specialised further: // - HUSKIES_TRAMPOLINE_STARTED=1 → "gateway X.Y.Z (git_hash) ready — model: ..." // - HUSKIES_TRAMPOLINE_FAILURE= → rollback failure notice + // - otherwise (normal start) → "{bot_name} is online — gateway X.Y.Z (git_hash) — model: ..." let announce_msg = if let Ok(reason) = std::env::var("HUSKIES_TRAMPOLINE_FAILURE") { super::format::format_gateway_rollback_announcement(&reason) } else if std::env::var("HUSKIES_TRAMPOLINE_STARTED").is_ok() { @@ -411,7 +413,11 @@ pub async fn run_bot( config.model.as_deref(), ) } else { - format_startup_announcement(&announce_bot_name) + format_startup_announcement( + &announce_bot_name, + option_env!("BUILD_GIT_HASH").unwrap_or("unknown"), + config.model.as_deref(), + ) }; let announce_html = markdown_to_html(&announce_msg); slog!("[matrix-bot] Sending startup announcement: {announce_msg}");