From 4097253921d95ef78adc89a9786d701879704cd7 Mon Sep 17 00:00:00 2001 From: Hibryda Date: Thu, 12 Mar 2026 02:52:14 +0100 Subject: [PATCH] feat(e2e): add test mode infrastructure with BTERMINAL_TEST env isolation Rust: watcher.rs/fs_watcher.rs skip watchers in test mode, is_test_mode Tauri command. Frontend: wake-scheduler disable, App.svelte test mode detection. AppConfig centralization in bterminal-core (OnceLock pattern for path overrides). --- v2/Cargo.lock | 1 + v2/bterminal-core/Cargo.toml | 1 + v2/bterminal-core/src/config.rs | 193 +++++++++++++++++++++ v2/bterminal-core/src/lib.rs | 1 + v2/bterminal-core/src/sidecar.rs | 3 + v2/bterminal-relay/src/main.rs | 5 +- v2/src-tauri/src/btmsg.rs | 20 ++- v2/src-tauri/src/bttask.rs | 20 ++- v2/src-tauri/src/commands/misc.rs | 5 + v2/src-tauri/src/ctx.rs | 18 +- v2/src-tauri/src/fs_watcher.rs | 6 + v2/src-tauri/src/groups.rs | 20 ++- v2/src-tauri/src/lib.rs | 47 ++++- v2/src-tauri/src/memora.rs | 7 +- v2/src-tauri/src/telemetry.rs | 5 +- v2/src-tauri/src/watcher.rs | 6 + v2/src/App.svelte | 7 + v2/src/lib/stores/wake-scheduler.svelte.ts | 10 ++ 18 files changed, 346 insertions(+), 29 deletions(-) create mode 100644 v2/bterminal-core/src/config.rs diff --git a/v2/Cargo.lock b/v2/Cargo.lock index d8dabae..4ee4566 100644 --- a/v2/Cargo.lock +++ b/v2/Cargo.lock @@ -263,6 +263,7 @@ dependencies = [ name = "bterminal-core" version = "0.1.0" dependencies = [ + "dirs 5.0.1", "log", "portable-pty", "serde", diff --git a/v2/bterminal-core/Cargo.toml b/v2/bterminal-core/Cargo.toml index 263d9ef..7a23321 100644 --- a/v2/bterminal-core/Cargo.toml +++ b/v2/bterminal-core/Cargo.toml @@ -11,3 +11,4 @@ serde_json = "1.0" log = "0.4" portable-pty = "0.8" uuid = { version = "1", features = ["v4"] } +dirs = "5" diff --git a/v2/bterminal-core/src/config.rs b/v2/bterminal-core/src/config.rs new file mode 100644 index 0000000..5af2d26 --- /dev/null +++ b/v2/bterminal-core/src/config.rs @@ -0,0 +1,193 @@ +// AppConfig — centralized path resolution for all BTerminal subsystems. +// In production, paths resolve via dirs:: crate defaults. +// In test mode (BTERMINAL_TEST=1), paths resolve from env var overrides: +// BTERMINAL_TEST_DATA_DIR → replaces dirs::data_dir()/bterminal +// BTERMINAL_TEST_CONFIG_DIR → replaces dirs::config_dir()/bterminal +// BTERMINAL_TEST_CTX_DIR → replaces ~/.claude-context + +use std::path::PathBuf; + +#[derive(Debug, Clone)] +pub struct AppConfig { + /// Data directory for btmsg.db, sessions.db (default: ~/.local/share/bterminal) + pub data_dir: PathBuf, + /// Config directory for groups.json (default: ~/.config/bterminal) + pub config_dir: PathBuf, + /// ctx database path (default: ~/.claude-context/context.db) + pub ctx_db_path: PathBuf, + /// Memora database path (default: ~/.local/share/memora/memories.db) + pub memora_db_path: PathBuf, + /// Whether we are in test mode + pub test_mode: bool, +} + +impl AppConfig { + /// Build config from environment. In test mode, uses BTERMINAL_TEST_*_DIR env vars. + pub fn from_env() -> Self { + let test_mode = std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1"); + + let data_dir = std::env::var("BTERMINAL_TEST_DATA_DIR") + .ok() + .filter(|_| test_mode) + .map(PathBuf::from) + .unwrap_or_else(|| { + dirs::data_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("bterminal") + }); + + let config_dir = std::env::var("BTERMINAL_TEST_CONFIG_DIR") + .ok() + .filter(|_| test_mode) + .map(PathBuf::from) + .unwrap_or_else(|| { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("bterminal") + }); + + let ctx_db_path = std::env::var("BTERMINAL_TEST_CTX_DIR") + .ok() + .filter(|_| test_mode) + .map(|d| PathBuf::from(d).join("context.db")) + .unwrap_or_else(|| { + dirs::home_dir() + .unwrap_or_default() + .join(".claude-context") + .join("context.db") + }); + + let memora_db_path = if test_mode { + // In test mode, memora is optional — use data_dir/memora/memories.db + data_dir.join("memora").join("memories.db") + } else { + dirs::data_dir() + .unwrap_or_else(|| { + dirs::home_dir() + .unwrap_or_default() + .join(".local/share") + }) + .join("memora") + .join("memories.db") + }; + + Self { + data_dir, + config_dir, + ctx_db_path, + memora_db_path, + test_mode, + } + } + + /// Path to btmsg.db (shared between btmsg and bttask) + pub fn btmsg_db_path(&self) -> PathBuf { + self.data_dir.join("btmsg.db") + } + + /// Path to sessions.db + pub fn sessions_db_dir(&self) -> &PathBuf { + &self.data_dir + } + + /// Path to groups.json + pub fn groups_json_path(&self) -> PathBuf { + self.config_dir.join("groups.json") + } + + /// Whether running in test mode (BTERMINAL_TEST=1) + pub fn is_test_mode(&self) -> bool { + self.test_mode + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_production_paths_use_dirs() { + // Without BTERMINAL_TEST=1, paths should use dirs:: defaults + std::env::remove_var("BTERMINAL_TEST"); + std::env::remove_var("BTERMINAL_TEST_DATA_DIR"); + std::env::remove_var("BTERMINAL_TEST_CONFIG_DIR"); + std::env::remove_var("BTERMINAL_TEST_CTX_DIR"); + + let config = AppConfig::from_env(); + assert!(!config.is_test_mode()); + // Should end with "bterminal" for data and config + assert!(config.data_dir.ends_with("bterminal")); + assert!(config.config_dir.ends_with("bterminal")); + assert!(config.ctx_db_path.ends_with("context.db")); + assert!(config.memora_db_path.ends_with("memories.db")); + } + + #[test] + fn test_btmsg_db_path() { + std::env::remove_var("BTERMINAL_TEST"); + let config = AppConfig::from_env(); + let path = config.btmsg_db_path(); + assert!(path.ends_with("btmsg.db")); + assert!(path.parent().unwrap().ends_with("bterminal")); + } + + #[test] + fn test_groups_json_path() { + std::env::remove_var("BTERMINAL_TEST"); + let config = AppConfig::from_env(); + let path = config.groups_json_path(); + assert!(path.ends_with("groups.json")); + } + + #[test] + fn test_test_mode_uses_overrides() { + std::env::set_var("BTERMINAL_TEST", "1"); + std::env::set_var("BTERMINAL_TEST_DATA_DIR", "/tmp/bt-test-data"); + std::env::set_var("BTERMINAL_TEST_CONFIG_DIR", "/tmp/bt-test-config"); + std::env::set_var("BTERMINAL_TEST_CTX_DIR", "/tmp/bt-test-ctx"); + + let config = AppConfig::from_env(); + assert!(config.is_test_mode()); + assert_eq!(config.data_dir, PathBuf::from("/tmp/bt-test-data")); + assert_eq!(config.config_dir, PathBuf::from("/tmp/bt-test-config")); + assert_eq!(config.ctx_db_path, PathBuf::from("/tmp/bt-test-ctx/context.db")); + assert_eq!(config.btmsg_db_path(), PathBuf::from("/tmp/bt-test-data/btmsg.db")); + assert_eq!(config.groups_json_path(), PathBuf::from("/tmp/bt-test-config/groups.json")); + + // Cleanup + std::env::remove_var("BTERMINAL_TEST"); + std::env::remove_var("BTERMINAL_TEST_DATA_DIR"); + std::env::remove_var("BTERMINAL_TEST_CONFIG_DIR"); + std::env::remove_var("BTERMINAL_TEST_CTX_DIR"); + } + + #[test] + fn test_test_mode_without_overrides_uses_defaults() { + std::env::set_var("BTERMINAL_TEST", "1"); + std::env::remove_var("BTERMINAL_TEST_DATA_DIR"); + std::env::remove_var("BTERMINAL_TEST_CONFIG_DIR"); + std::env::remove_var("BTERMINAL_TEST_CTX_DIR"); + + let config = AppConfig::from_env(); + assert!(config.is_test_mode()); + // Without override vars, falls back to dirs:: defaults + assert!(config.data_dir.ends_with("bterminal")); + + std::env::remove_var("BTERMINAL_TEST"); + } + + #[test] + fn test_test_mode_memora_in_data_dir() { + std::env::set_var("BTERMINAL_TEST", "1"); + std::env::set_var("BTERMINAL_TEST_DATA_DIR", "/tmp/bt-test-data"); + + let config = AppConfig::from_env(); + assert_eq!( + config.memora_db_path, + PathBuf::from("/tmp/bt-test-data/memora/memories.db") + ); + + std::env::remove_var("BTERMINAL_TEST"); + std::env::remove_var("BTERMINAL_TEST_DATA_DIR"); + } +} diff --git a/v2/bterminal-core/src/lib.rs b/v2/bterminal-core/src/lib.rs index ceebab6..4da77b5 100644 --- a/v2/bterminal-core/src/lib.rs +++ b/v2/bterminal-core/src/lib.rs @@ -1,3 +1,4 @@ +pub mod config; pub mod event; pub mod pty; pub mod sidecar; diff --git a/v2/bterminal-core/src/sidecar.rs b/v2/bterminal-core/src/sidecar.rs index bc8a874..2d29679 100644 --- a/v2/bterminal-core/src/sidecar.rs +++ b/v2/bterminal-core/src/sidecar.rs @@ -44,6 +44,8 @@ fn default_provider() -> String { #[derive(Debug, Clone)] pub struct SidecarConfig { pub search_paths: Vec, + /// Extra env vars forwarded to sidecar processes (e.g. BTERMINAL_TEST=1 for test isolation) + pub env_overrides: std::collections::HashMap, } struct SidecarCommand { @@ -94,6 +96,7 @@ impl SidecarManager { .args(&cmd.args) .env_clear() .envs(clean_env) + .envs(self.config.env_overrides.iter().map(|(k, v)| (k.as_str(), v.as_str()))) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()) diff --git a/v2/bterminal-relay/src/main.rs b/v2/bterminal-relay/src/main.rs index ec03f96..47540c4 100644 --- a/v2/bterminal-relay/src/main.rs +++ b/v2/bterminal-relay/src/main.rs @@ -96,7 +96,10 @@ async fn main() { } search_paths.push(std::path::PathBuf::from("sidecar")); - let sidecar_config = SidecarConfig { search_paths }; + let sidecar_config = SidecarConfig { + search_paths, + env_overrides: std::collections::HashMap::new(), + }; let token = Arc::new(cli.token); // Rate limiting state for auth failures diff --git a/v2/src-tauri/src/btmsg.rs b/v2/src-tauri/src/btmsg.rs index e554d91..1c2edd4 100644 --- a/v2/src-tauri/src/btmsg.rs +++ b/v2/src-tauri/src/btmsg.rs @@ -1,15 +1,27 @@ // btmsg — Access to btmsg SQLite database // Database at ~/.local/share/bterminal/btmsg.db (created by btmsg CLI) +// Path configurable via init() for test isolation. use rusqlite::{params, Connection, OpenFlags}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::sync::OnceLock; + +static DB_PATH: OnceLock = OnceLock::new(); + +/// Set the btmsg database path. Must be called before any db access. +/// Called from lib.rs setup with AppConfig-resolved path. +pub fn init(path: PathBuf) { + let _ = DB_PATH.set(path); +} fn db_path() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("bterminal") - .join("btmsg.db") + DB_PATH.get().cloned().unwrap_or_else(|| { + dirs::data_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("bterminal") + .join("btmsg.db") + }) } fn open_db() -> Result { diff --git a/v2/src-tauri/src/bttask.rs b/v2/src-tauri/src/bttask.rs index 71ba2d9..2ea642a 100644 --- a/v2/src-tauri/src/bttask.rs +++ b/v2/src-tauri/src/bttask.rs @@ -1,15 +1,27 @@ // bttask — Read access to task board SQLite tables in btmsg.db // Tasks table created by bttask CLI, shared DB with btmsg +// Path configurable via init() for test isolation. use rusqlite::{params, Connection, OpenFlags}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::sync::OnceLock; + +static DB_PATH: OnceLock = OnceLock::new(); + +/// Set the bttask database path. Must be called before any db access. +/// Called from lib.rs setup with AppConfig-resolved path. +pub fn init(path: PathBuf) { + let _ = DB_PATH.set(path); +} fn db_path() -> PathBuf { - dirs::data_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("bterminal") - .join("btmsg.db") + DB_PATH.get().cloned().unwrap_or_else(|| { + dirs::data_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("bterminal") + .join("btmsg.db") + }) } fn open_db() -> Result { diff --git a/v2/src-tauri/src/commands/misc.rs b/v2/src-tauri/src/commands/misc.rs index a0a32ca..85dab07 100644 --- a/v2/src-tauri/src/commands/misc.rs +++ b/v2/src-tauri/src/commands/misc.rs @@ -29,6 +29,11 @@ pub fn open_url(url: String) -> Result<(), String> { Ok(()) } +#[tauri::command] +pub fn is_test_mode() -> bool { + std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1") +} + #[tauri::command] pub fn frontend_log(level: String, message: String, context: Option) { match level.as_str() { diff --git a/v2/src-tauri/src/ctx.rs b/v2/src-tauri/src/ctx.rs index 93bec31..5aa725e 100644 --- a/v2/src-tauri/src/ctx.rs +++ b/v2/src-tauri/src/ctx.rs @@ -1,8 +1,10 @@ // ctx — Read-only access to the Claude Code context manager database // Database: ~/.claude-context/context.db (managed by ctx CLI tool) +// Path configurable via new_with_path() for test isolation. use rusqlite::{Connection, params}; use serde::Serialize; +use std::path::PathBuf; use std::sync::Mutex; #[derive(Debug, Clone, Serialize)] @@ -22,10 +24,11 @@ pub struct CtxSummary { pub struct CtxDb { conn: Mutex>, + path: PathBuf, } impl CtxDb { - fn db_path() -> std::path::PathBuf { + fn default_db_path() -> PathBuf { dirs::home_dir() .unwrap_or_default() .join(".claude-context") @@ -33,8 +36,11 @@ impl CtxDb { } pub fn new() -> Self { - let db_path = Self::db_path(); + Self::new_with_path(Self::default_db_path()) + } + /// Create a CtxDb with a custom database path (for test isolation). + pub fn new_with_path(db_path: PathBuf) -> Self { let conn = if db_path.exists() { Connection::open_with_flags( &db_path, @@ -44,12 +50,12 @@ impl CtxDb { None }; - Self { conn: Mutex::new(conn) } + Self { conn: Mutex::new(conn), path: db_path } } /// Create the context database directory and schema, then open a read-only connection. pub fn init_db(&self) -> Result<(), String> { - let db_path = Self::db_path(); + let db_path = &self.path; // Create parent directory if let Some(parent) = db_path.parent() { @@ -136,7 +142,7 @@ impl CtxDb { /// Register a project in the ctx database (creates if not exists). /// Opens a brief read-write connection; the main self.conn stays read-only. pub fn register_project(&self, name: &str, description: &str, work_dir: Option<&str>) -> Result<(), String> { - let db_path = Self::db_path(); + let db_path = &self.path; let conn = Connection::open(&db_path) .map_err(|e| format!("ctx database not found: {e}"))?; @@ -257,7 +263,7 @@ mod tests { /// Create a CtxDb with conn set to None, simulating a missing database. fn make_missing_db() -> CtxDb { - CtxDb { conn: Mutex::new(None) } + CtxDb { conn: Mutex::new(None), path: PathBuf::from("/nonexistent/context.db") } } #[test] diff --git a/v2/src-tauri/src/fs_watcher.rs b/v2/src-tauri/src/fs_watcher.rs index 00cef7f..10409f4 100644 --- a/v2/src-tauri/src/fs_watcher.rs +++ b/v2/src-tauri/src/fs_watcher.rs @@ -73,6 +73,12 @@ impl ProjectFsWatcher { project_id: &str, cwd: &str, ) -> Result<(), String> { + // In test mode, skip inotify watchers to avoid resource contention and flaky events + if std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1") { + log::info!("Test mode: skipping fs watcher for project {project_id}"); + return Ok(()); + } + let cwd_path = Path::new(cwd); if !cwd_path.is_dir() { return Err(format!("Not a directory: {cwd}")); diff --git a/v2/src-tauri/src/groups.rs b/v2/src-tauri/src/groups.rs index 26b7942..d714022 100644 --- a/v2/src-tauri/src/groups.rs +++ b/v2/src-tauri/src/groups.rs @@ -1,8 +1,18 @@ // Project group configuration // Reads/writes ~/.config/bterminal/groups.json +// Path configurable via init() for test isolation. use serde::{Deserialize, Serialize}; use std::path::PathBuf; +use std::sync::OnceLock; + +static CONFIG_PATH: OnceLock = OnceLock::new(); + +/// Set the groups.json path. Must be called before any config access. +/// Called from lib.rs setup with AppConfig-resolved path. +pub fn init(path: PathBuf) { + let _ = CONFIG_PATH.set(path); +} #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -62,10 +72,12 @@ impl Default for GroupsFile { } fn config_path() -> PathBuf { - dirs::config_dir() - .unwrap_or_else(|| PathBuf::from(".")) - .join("bterminal") - .join("groups.json") + CONFIG_PATH.get().cloned().unwrap_or_else(|| { + dirs::config_dir() + .unwrap_or_else(|| PathBuf::from(".")) + .join("bterminal") + .join("groups.json") + }) } pub fn load_groups() -> Result { diff --git a/v2/src-tauri/src/lib.rs b/v2/src-tauri/src/lib.rs index 461e8e0..3008a7c 100644 --- a/v2/src-tauri/src/lib.rs +++ b/v2/src-tauri/src/lib.rs @@ -13,6 +13,7 @@ mod session; mod telemetry; mod watcher; +use bterminal_core::config::AppConfig; use event_sink::TauriEventSink; use pty::PtyManager; use remote::RemoteManager; @@ -32,6 +33,7 @@ pub(crate) struct AppState { pub ctx_db: Arc, pub memora_db: Arc, pub remote_manager: Arc, + pub app_config: Arc, _telemetry: telemetry::TelemetryGuard, } @@ -40,9 +42,26 @@ pub fn run() { // Force dark GTK theme for native dialogs (file chooser, etc.) std::env::set_var("GTK_THEME", "Adwaita:dark"); + // Resolve all paths via AppConfig (respects BTERMINAL_TEST_* env vars) + let app_config = AppConfig::from_env(); + if app_config.is_test_mode() { + log::info!( + "Test mode enabled: data_dir={}, config_dir={}", + app_config.data_dir.display(), + app_config.config_dir.display() + ); + } + + // Initialize subsystem paths from AppConfig (before any db access) + btmsg::init(app_config.btmsg_db_path()); + bttask::init(app_config.btmsg_db_path()); + groups::init(app_config.groups_json_path()); + // Initialize tracing + optional OTLP export (before any tracing macros) let telemetry_guard = telemetry::init(); + let app_config_arc = Arc::new(app_config); + tauri::Builder::default() .invoke_handler(tauri::generate_handler![ // PTY @@ -151,6 +170,7 @@ pub fn run() { // Misc commands::misc::cli_get_group, commands::misc::open_url, + commands::misc::is_test_mode, commands::misc::frontend_log, ]) .plugin(tauri_plugin_updater::Builder::new().build()) @@ -161,6 +181,8 @@ pub fn run() { // tracing's compatibility layer). Adding plugin-log would panic with // "attempted to set a logger after the logging system was already initialized". + let config = app_config_arc.clone(); + // Create TauriEventSink for core managers let sink: Arc = Arc::new(TauriEventSink(app.handle().clone())); @@ -178,28 +200,38 @@ pub fn run() { .parent() .unwrap() .to_path_buf(); + // Forward test mode env vars to sidecar processes + let mut env_overrides = std::collections::HashMap::new(); + if config.is_test_mode() { + env_overrides.insert("BTERMINAL_TEST".into(), "1".into()); + if let Ok(v) = std::env::var("BTERMINAL_TEST_DATA_DIR") { + env_overrides.insert("BTERMINAL_TEST_DATA_DIR".into(), v); + } + if let Ok(v) = std::env::var("BTERMINAL_TEST_CONFIG_DIR") { + env_overrides.insert("BTERMINAL_TEST_CONFIG_DIR".into(), v); + } + } + let sidecar_config = SidecarConfig { search_paths: vec![ resource_dir.join("sidecar"), dev_root.join("sidecar"), ], + env_overrides, }; let pty_manager = Arc::new(PtyManager::new(sink.clone())); let sidecar_manager = Arc::new(SidecarManager::new(sink, sidecar_config)); - // Initialize session database - let data_dir = dirs::data_dir() - .unwrap_or_else(|| std::path::PathBuf::from(".")) - .join("bterminal"); + // Initialize session database using AppConfig data_dir let session_db = Arc::new( - SessionDb::open(&data_dir).expect("Failed to open session database"), + SessionDb::open(config.sessions_db_dir()).expect("Failed to open session database"), ); let file_watcher = Arc::new(FileWatcherManager::new()); let fs_watcher = Arc::new(ProjectFsWatcher::new()); - let ctx_db = Arc::new(ctx::CtxDb::new()); - let memora_db = Arc::new(memora::MemoraDb::new()); + let ctx_db = Arc::new(ctx::CtxDb::new_with_path(config.ctx_db_path.clone())); + let memora_db = Arc::new(memora::MemoraDb::new_with_path(config.memora_db_path.clone())); let remote_manager = Arc::new(RemoteManager::new()); // Start local sidecar @@ -217,6 +249,7 @@ pub fn run() { ctx_db, memora_db, remote_manager, + app_config: config, _telemetry: telemetry_guard, }); diff --git a/v2/src-tauri/src/memora.rs b/v2/src-tauri/src/memora.rs index 57fb167..5297c90 100644 --- a/v2/src-tauri/src/memora.rs +++ b/v2/src-tauri/src/memora.rs @@ -26,7 +26,7 @@ pub struct MemoraDb { } impl MemoraDb { - fn db_path() -> std::path::PathBuf { + fn default_db_path() -> std::path::PathBuf { dirs::data_dir() .unwrap_or_else(|| dirs::home_dir().unwrap_or_default().join(".local/share")) .join("memora") @@ -34,8 +34,11 @@ impl MemoraDb { } pub fn new() -> Self { - let db_path = Self::db_path(); + Self::new_with_path(Self::default_db_path()) + } + /// Create a MemoraDb with a custom database path (for test isolation). + pub fn new_with_path(db_path: std::path::PathBuf) -> Self { let conn = if db_path.exists() { Connection::open_with_flags( &db_path, diff --git a/v2/src-tauri/src/telemetry.rs b/v2/src-tauri/src/telemetry.rs index 0251f6b..0391460 100644 --- a/v2/src-tauri/src/telemetry.rs +++ b/v2/src-tauri/src/telemetry.rs @@ -34,8 +34,11 @@ pub fn init() -> TelemetryGuard { .with_target(true) .compact(); + // In test mode, never export telemetry (avoid contaminating production data) + let is_test = std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1"); + match std::env::var("BTERMINAL_OTLP_ENDPOINT") { - Ok(endpoint) if !endpoint.is_empty() => { + Ok(endpoint) if !endpoint.is_empty() && !is_test => { match build_otlp_provider(&endpoint) { Ok(provider) => { let otel_layer = tracing_opentelemetry::layer() diff --git a/v2/src-tauri/src/watcher.rs b/v2/src-tauri/src/watcher.rs index 9aee5e5..8ecf0e4 100644 --- a/v2/src-tauri/src/watcher.rs +++ b/v2/src-tauri/src/watcher.rs @@ -37,6 +37,12 @@ impl FileWatcherManager { pane_id: &str, path: &str, ) -> Result { + // In test mode, skip file watching to avoid inotify noise and flaky events + if std::env::var("BTERMINAL_TEST").map_or(false, |v| v == "1") { + return std::fs::read_to_string(path) + .map_err(|e| format!("Failed to read file: {e}")); + } + let file_path = PathBuf::from(path); if !file_path.exists() { return Err(format!("File not found: {path}")); diff --git a/v2/src/App.svelte b/v2/src/App.svelte index b8952db..efd931a 100644 --- a/v2/src/App.svelte +++ b/v2/src/App.svelte @@ -12,6 +12,8 @@ import { registerMemoryAdapter } from './lib/adapters/memory-adapter'; import { MemoraAdapter } from './lib/adapters/memora-bridge'; import { loadWorkspace, getActiveTab, setActiveTab, setActiveProject, getEnabledProjects } from './lib/stores/workspace.svelte'; + import { disableWakeScheduler } from './lib/stores/wake-scheduler.svelte'; + import { invoke } from '@tauri-apps/api/core'; // Workspace components import GlobalTabBar from './lib/components/Workspace/GlobalTabBar.svelte'; @@ -82,6 +84,11 @@ startAgentDispatcher(); startHealthTick(); + // Disable wake scheduler in test mode to prevent timer interference + invoke('is_test_mode').then(isTest => { + if (isTest) disableWakeScheduler(); + }); + if (!detached) { loadWorkspace().then(() => { loaded = true; }); } diff --git a/v2/src/lib/stores/wake-scheduler.svelte.ts b/v2/src/lib/stores/wake-scheduler.svelte.ts index e03e0b2..414e728 100644 --- a/v2/src/lib/stores/wake-scheduler.svelte.ts +++ b/v2/src/lib/stores/wake-scheduler.svelte.ts @@ -36,9 +36,17 @@ export interface WakeEvent { let registrations = $state>(new Map()); let pendingWakes = $state>(new Map()); +/** When true, registerManager() becomes a no-op (set in test mode) */ +let schedulerDisabled = false; // --- Public API --- +/** Disable the wake scheduler (call during app init in test mode) */ +export function disableWakeScheduler(): void { + schedulerDisabled = true; + clearWakeScheduler(); +} + /** Register a Manager agent for wake scheduling */ export function registerManager( agentId: AgentId, @@ -48,6 +56,8 @@ export function registerManager( intervalMin: number, threshold: number, ): void { + if (schedulerDisabled) return; + // Unregister first to clear any existing timer unregisterManager(agentId);