From da83fcb78dd706e2b92eef4f85bf3e52c4ced16f Mon Sep 17 00:00:00 2001 From: dave Date: Thu, 14 May 2026 23:54:38 +0000 Subject: [PATCH] huskies: merge 1074 --- server/build.rs | 14 +++++++++++ server/src/http/mcp/diagnostics/mod.rs | 34 +++++++++++++++++++++++--- server/src/main.rs | 4 +-- 3 files changed, 46 insertions(+), 6 deletions(-) diff --git a/server/build.rs b/server/build.rs index 1977aaea..dde322e8 100644 --- a/server/build.rs +++ b/server/build.rs @@ -17,6 +17,20 @@ fn run(cmd: &str, args: &[&str], dir: &Path) { fn main() { println!("cargo:rerun-if-changed=build.rs"); println!("cargo:rerun-if-env-changed=PROFILE"); + + // Embed the current git commit hash at compile time so `get_version` always + // reflects the binary that is actually running, not a potentially-stale file. + println!("cargo:rerun-if-changed=../.git/HEAD"); + println!("cargo:rerun-if-changed=../.git/refs/"); + let git_hash = std::process::Command::new("git") + .args(["rev-parse", "--short", "HEAD"]) + .output() + .ok() + .filter(|o| o.status.success()) + .and_then(|o| String::from_utf8(o.stdout).ok()) + .map(|s| s.trim().to_string()) + .unwrap_or_else(|| "unknown".to_string()); + println!("cargo:rustc-env=BUILD_GIT_HASH={git_hash}"); println!("cargo:rerun-if-changed=../frontend/package.json"); println!("cargo:rerun-if-changed=../frontend/package-lock.json"); println!("cargo:rerun-if-changed=../frontend/vite.config.ts"); diff --git a/server/src/http/mcp/diagnostics/mod.rs b/server/src/http/mcp/diagnostics/mod.rs index b34bf76d..8cd17573 100644 --- a/server/src/http/mcp/diagnostics/mod.rs +++ b/server/src/http/mcp/diagnostics/mod.rs @@ -123,11 +123,10 @@ pub(crate) fn tool_dump_crdt(args: &Value) -> Result { /// MCP tool: return the server version, build hash, and running port. pub(crate) fn tool_get_version(ctx: &AppContext) -> Result { - let build_hash = - std::fs::read_to_string(".huskies/build_hash").unwrap_or_else(|_| "unknown".to_string()); + let build_hash = option_env!("BUILD_GIT_HASH").unwrap_or("unknown"); serde_json::to_string_pretty(&json!({ "version": env!("CARGO_PKG_VERSION"), - "build_hash": build_hash.trim(), + "build_hash": build_hash, "port": ctx.services.agents.port(), })) .map_err(|e| format!("Serialization error: {e}")) @@ -312,4 +311,33 @@ mod tests { let result = tool_get_server_logs(&json!({"lines": 9999})).unwrap(); let _ = result; } + + #[test] + fn tool_get_version_ignores_build_hash_file_and_reports_compile_time_value() { + // Regression: get_version must NOT read .huskies/build_hash at runtime. + // Write a deliberately wrong value to the file and assert get_version + // returns the compile-time hash, not the file content. + let dir = tempfile::tempdir().expect("tempdir"); + let huskies_dir = dir.path().join(".huskies"); + std::fs::create_dir_all(&huskies_dir).unwrap(); + std::fs::write(huskies_dir.join("build_hash"), "wrong_hash_sentinel_xyz").unwrap(); + + let ctx = crate::http::test_helpers::test_ctx(dir.path()); + let result = tool_get_version(&ctx).expect("tool_get_version must not fail"); + let parsed: serde_json::Value = serde_json::from_str(&result).expect("must be valid JSON"); + + let returned_hash = parsed["build_hash"] + .as_str() + .expect("build_hash must be a string"); + assert_ne!( + returned_hash, "wrong_hash_sentinel_xyz", + "get_version must not read .huskies/build_hash; got '{returned_hash}'" + ); + // The returned hash must equal the compile-time value. + let compile_time_hash = option_env!("BUILD_GIT_HASH").unwrap_or("unknown"); + assert_eq!( + returned_hash, compile_time_hash, + "get_version must return compile-time BUILD_GIT_HASH" + ); + } } diff --git a/server/src/main.rs b/server/src/main.rs index 0b78af7a..152cd025 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -82,12 +82,10 @@ async fn main() -> Result<(), std::io::Error> { }); // Log version and build hash so we can verify what's running. - let build_hash = - std::fs::read_to_string(".huskies/build_hash").unwrap_or_else(|_| "unknown".to_string()); slog!( "[startup] huskies v{} (build {})", env!("CARGO_PKG_VERSION"), - build_hash.trim() + option_env!("BUILD_GIT_HASH").unwrap_or("unknown") ); let app_state = Arc::new(SessionState::default());