agent: code-atomiser-fix — test_llm_model respects caller GPU preference (Race-8)
Add an Option<bool> use_gpu parameter to test_llm_model with the same default-true semantics as load_llm_model. Hard-coding true triggered an engine tear-down/rebuild when a parallel load_llm_model(use_gpu=false) was in flight (LlmEngine::load_model triples on id+path+use_gpu) and silently flipped the user's GPU mode underneath the test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
20
Cargo.lock
generated
20
Cargo.lock
generated
@@ -2784,6 +2784,7 @@ dependencies = [
|
|||||||
"tempfile",
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
"tracing-appender",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
"uuid",
|
"uuid",
|
||||||
"webkit2gtk",
|
"webkit2gtk",
|
||||||
@@ -5314,6 +5315,12 @@ dependencies = [
|
|||||||
"serde_json",
|
"serde_json",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "symlink"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "a7973cce6668464ea31f176d85b13c7ab3bba2cb3b77a2ed26abd7801688010a"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "symphonia"
|
name = "symphonia"
|
||||||
version = "0.5.5"
|
version = "0.5.5"
|
||||||
@@ -6370,6 +6377,19 @@ dependencies = [
|
|||||||
"tracing-core",
|
"tracing-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tracing-appender"
|
||||||
|
version = "0.2.5"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "050686193eb999b4bb3bc2acfa891a13da00f79734704c4b8b4ef1a10b368a3c"
|
||||||
|
dependencies = [
|
||||||
|
"crossbeam-channel",
|
||||||
|
"symlink",
|
||||||
|
"thiserror 2.0.18",
|
||||||
|
"time",
|
||||||
|
"tracing-subscriber",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tracing-attributes"
|
name = "tracing-attributes"
|
||||||
version = "0.1.31"
|
version = "0.1.31"
|
||||||
|
|||||||
@@ -21,8 +21,16 @@ pub fn crashes_dir() -> PathBuf {
|
|||||||
lumotia_core::paths::app_paths().crashes_dir()
|
lumotia_core::paths::app_paths().crashes_dir()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Directory for the rolling Rust log file (lumotia.log + rotated lumotia.log.1, etc).
|
/// Directory for the rolling Rust log file.
|
||||||
/// Subscribers configured in src-tauri/src/lib.rs at startup.
|
///
|
||||||
|
/// The base filename is `lumotia.log`; daily rotation produces dated
|
||||||
|
/// siblings (e.g. `lumotia.log.2026-05-12`). The appender keeps the
|
||||||
|
/// 7 most-recent days and prunes older files on rotation.
|
||||||
|
///
|
||||||
|
/// Subscriber and rotation policy are configured by `init_tracing` in
|
||||||
|
/// `src-tauri/src/lib.rs` at startup; the diagnostic-report bundler in
|
||||||
|
/// `src-tauri/src/commands/diagnostics.rs` attaches the live file from
|
||||||
|
/// this directory to user-submitted crash reports.
|
||||||
pub fn logs_dir() -> PathBuf {
|
pub fn logs_dir() -> PathBuf {
|
||||||
lumotia_core::paths::app_paths().logs_dir()
|
lumotia_core::paths::app_paths().logs_dir()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -54,6 +54,11 @@ serde = { version = "1", features = ["derive"] }
|
|||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
tracing = "0.1"
|
tracing = "0.1"
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
# Rolling daily file appender that backs `lumotia.log` (the forensic log
|
||||||
|
# bundled into user-submitted diagnostic reports — see
|
||||||
|
# src-tauri/src/commands/diagnostics.rs and crates/storage/src/file_storage.rs).
|
||||||
|
# 0.2.3+ is required for `max_log_files`; 0.2.5 is the current crates.io head.
|
||||||
|
tracing-appender = "0.2.5"
|
||||||
|
|
||||||
# Async runtime (spawn_blocking for inference)
|
# Async runtime (spawn_blocking for inference)
|
||||||
tokio = { version = "1", features = ["rt", "sync"] }
|
tokio = { version = "1", features = ["rt", "sync"] }
|
||||||
|
|||||||
@@ -188,6 +188,7 @@ pub async fn test_llm_model(
|
|||||||
window: tauri::WebviewWindow,
|
window: tauri::WebviewWindow,
|
||||||
state: State<'_, AppState>,
|
state: State<'_, AppState>,
|
||||||
model_id: String,
|
model_id: String,
|
||||||
|
use_gpu: Option<bool>,
|
||||||
) -> Result<LlmTestResult, String> {
|
) -> Result<LlmTestResult, String> {
|
||||||
ensure_main_window(&window)?;
|
ensure_main_window(&window)?;
|
||||||
let id = parse_model_id(model_id)?;
|
let id = parse_model_id(model_id)?;
|
||||||
@@ -238,10 +239,17 @@ pub async fn test_llm_model(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Not currently loaded. Attempt a real load (with GPU by default
|
// Not currently loaded. Attempt a real load with the caller's
|
||||||
// — matches load_llm_model's default) and classify any failure.
|
// GPU preference (matching load_llm_model's default of `true` when
|
||||||
|
// unspecified). Hard-coding `true` here used to race with a parallel
|
||||||
|
// load_llm_model(use_gpu=false) — LlmEngine::load_model decides
|
||||||
|
// "same model?" via (id, path, use_gpu) triple-equality and tears
|
||||||
|
// the engine down on mismatch, silently flipping the user's GPU
|
||||||
|
// mode underneath the in-flight test.
|
||||||
|
let resolved_use_gpu = use_gpu.unwrap_or(true);
|
||||||
let engine = state.llm_engine.clone();
|
let engine = state.llm_engine.clone();
|
||||||
let load_result = tokio::task::spawn_blocking(move || engine.load_model(id, &path, true))
|
let load_result =
|
||||||
|
tokio::task::spawn_blocking(move || engine.load_model(id, &path, resolved_use_gpu))
|
||||||
.await
|
.await
|
||||||
.map_err(|e| e.to_string())?;
|
.map_err(|e| e.to_string())?;
|
||||||
|
|
||||||
|
|||||||
@@ -5,13 +5,18 @@ mod tauri_app_data_migration;
|
|||||||
#[cfg(not(target_os = "android"))]
|
#[cfg(not(target_os = "android"))]
|
||||||
mod tray;
|
mod tray;
|
||||||
|
|
||||||
|
use std::path::Path;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use std::sync::Once;
|
use std::sync::{Once, OnceLock};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use sqlx::SqlitePool;
|
use sqlx::SqlitePool;
|
||||||
use tauri::Manager;
|
use tauri::Manager;
|
||||||
use tracing_subscriber::EnvFilter;
|
use tracing_appender::non_blocking::WorkerGuard;
|
||||||
|
use tracing_appender::rolling::{RollingFileAppender, Rotation};
|
||||||
|
use tracing_subscriber::layer::SubscriberExt;
|
||||||
|
use tracing_subscriber::util::SubscriberInitExt;
|
||||||
|
use tracing_subscriber::{EnvFilter, Layer};
|
||||||
|
|
||||||
use lumotia_core::paths::{
|
use lumotia_core::paths::{
|
||||||
check_target_ambiguity, migrate_legacy_data_dir, MigrationStatus,
|
check_target_ambiguity, migrate_legacy_data_dir, MigrationStatus,
|
||||||
@@ -31,6 +36,13 @@ use lumotia_transcription::LocalEngine;
|
|||||||
|
|
||||||
static TRACING_INIT: Once = Once::new();
|
static TRACING_INIT: Once = Once::new();
|
||||||
|
|
||||||
|
/// Parks the non-blocking file-appender's `WorkerGuard` for the process
|
||||||
|
/// lifetime. Dropping the guard halts the background log-writing thread
|
||||||
|
/// and discards any pending log lines, so it must outlive every
|
||||||
|
/// `tracing::*!` call. A `OnceLock` here gives us a free `'static`
|
||||||
|
/// home and matches the `Once`-guarded init pattern above.
|
||||||
|
static FILE_APPENDER_GUARD: OnceLock<WorkerGuard> = OnceLock::new();
|
||||||
|
|
||||||
/// Shared app state holding the transcription engines and database pool.
|
/// Shared app state holding the transcription engines and database pool.
|
||||||
pub struct AppState {
|
pub struct AppState {
|
||||||
pub whisper_engine: Arc<LocalEngine>,
|
pub whisper_engine: Arc<LocalEngine>,
|
||||||
@@ -165,18 +177,112 @@ fn warn_if_x11_env_unset_on_wayland() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default `EnvFilter` string for the stderr (developer-facing) layer.
|
||||||
|
///
|
||||||
|
/// `lumotia_lib::commands::live=info` is listed explicitly so that the
|
||||||
|
/// operator's documented triage filter (per
|
||||||
|
/// docs/superpowers/audits/2026-05-10-phase10a-dogfood-notes.md) works
|
||||||
|
/// without a manual `RUST_LOG=` override on every dogfooding session.
|
||||||
|
const DEFAULT_STDERR_FILTER: &str =
|
||||||
|
"warn,lumotia=info,lumotia_lib=info,lumotia_lib::commands::live=info,lumotia_core=info,lumotia_audio=info,lumotia_hotkey=info,lumotia_ai_formatting=info,lumotia_llm=info,lumotia_storage=info,lumotia_transcription=info,lumotia_startup=info";
|
||||||
|
|
||||||
|
/// Default `EnvFilter` string for the rolling-file (forensic) layer.
|
||||||
|
///
|
||||||
|
/// More verbose than stderr by design: the file is what users attach to
|
||||||
|
/// diagnostic reports, so DEBUG-level Lumotia internals are worth the
|
||||||
|
/// disk cost. Third-party crates that spam at DEBUG (`sqlx`, `hyper`,
|
||||||
|
/// `reqwest`, `h2`, `rustls`, `tokio_util`) are pinned at INFO so they
|
||||||
|
/// don't drown out our own log lines.
|
||||||
|
const DEFAULT_FILE_FILTER: &str =
|
||||||
|
"info,lumotia=debug,lumotia_lib=debug,lumotia_lib::commands::live=debug,lumotia_core=debug,lumotia_audio=debug,lumotia_hotkey=debug,lumotia_ai_formatting=debug,lumotia_llm=debug,lumotia_storage=debug,lumotia_transcription=debug,lumotia_startup=debug,sqlx=info,hyper=info,reqwest=info,h2=info,rustls=info,tokio_util=info";
|
||||||
|
|
||||||
|
/// Build the stderr-side `EnvFilter`, honouring `RUST_LOG` if set.
|
||||||
|
fn stderr_env_filter() -> EnvFilter {
|
||||||
|
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(DEFAULT_STDERR_FILTER))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the file-side `EnvFilter`, honouring `LUMOTIA_LOG_FILE` if set
|
||||||
|
/// (falling back to `RUST_LOG`, then the verbose default).
|
||||||
|
fn file_env_filter() -> EnvFilter {
|
||||||
|
EnvFilter::try_from_env("LUMOTIA_LOG_FILE")
|
||||||
|
.or_else(|_| EnvFilter::try_from_default_env())
|
||||||
|
.unwrap_or_else(|_| EnvFilter::new(DEFAULT_FILE_FILTER))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the rolling daily appender for `lumotia.log` inside `logs_dir`.
|
||||||
|
///
|
||||||
|
/// Rotation policy: one file per day, keep 7 most-recent days. Older
|
||||||
|
/// files are pruned by `tracing-appender` itself. The base filename is
|
||||||
|
/// `lumotia.log`; rotated files get a date suffix (e.g.
|
||||||
|
/// `lumotia.log.2026-05-12`).
|
||||||
|
fn build_rolling_appender(logs_dir: &Path) -> std::io::Result<RollingFileAppender> {
|
||||||
|
std::fs::create_dir_all(logs_dir)?;
|
||||||
|
RollingFileAppender::builder()
|
||||||
|
.rotation(Rotation::DAILY)
|
||||||
|
.filename_prefix("lumotia")
|
||||||
|
.filename_suffix("log")
|
||||||
|
.max_log_files(7)
|
||||||
|
.build(logs_dir)
|
||||||
|
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install a `tracing` subscriber that writes to stderr (developer
|
||||||
|
/// stream, ANSI-coloured) AND to a rolling daily file at
|
||||||
|
/// `logs_dir/lumotia.log` (forensic stream, plain text). The file
|
||||||
|
/// stream is what diagnostic-report bundles attach.
|
||||||
|
///
|
||||||
|
/// Returns the `WorkerGuard` for the file appender so the caller can
|
||||||
|
/// park it for the process lifetime; dropping it discards pending logs.
|
||||||
|
/// Returns `None` if the file appender could not be built (e.g.
|
||||||
|
/// permissions); in that case only the stderr layer is installed.
|
||||||
|
///
|
||||||
|
/// Exposed `pub` so the `tracing_appender_smoke` integration test can
|
||||||
|
/// drive it against a tempdir; production code calls it via
|
||||||
|
/// `init_tracing` against the platform-default `logs_dir()`.
|
||||||
|
pub fn install_subscriber(logs_dir: &Path) -> Option<WorkerGuard> {
|
||||||
|
let stderr_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(std::io::stderr)
|
||||||
|
.with_filter(stderr_env_filter());
|
||||||
|
|
||||||
|
match build_rolling_appender(logs_dir) {
|
||||||
|
Ok(appender) => {
|
||||||
|
let (non_blocking, guard) = tracing_appender::non_blocking(appender);
|
||||||
|
let file_layer = tracing_subscriber::fmt::layer()
|
||||||
|
.with_writer(non_blocking)
|
||||||
|
.with_ansi(false)
|
||||||
|
.with_filter(file_env_filter());
|
||||||
|
|
||||||
|
let _ = tracing_subscriber::registry()
|
||||||
|
.with(stderr_layer)
|
||||||
|
.with(file_layer)
|
||||||
|
.try_init();
|
||||||
|
Some(guard)
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
// Best-effort: keep stderr logging even if the file path is
|
||||||
|
// unwritable, and surface the failure to stderr so dogfooders
|
||||||
|
// notice the missing forensic stream.
|
||||||
|
let _ = tracing_subscriber::registry()
|
||||||
|
.with(stderr_layer)
|
||||||
|
.try_init();
|
||||||
|
eprintln!(
|
||||||
|
"lumotia: failed to install rolling file log appender at {}: {e}",
|
||||||
|
logs_dir.display()
|
||||||
|
);
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn init_tracing() {
|
fn init_tracing() {
|
||||||
TRACING_INIT.call_once(|| {
|
TRACING_INIT.call_once(|| {
|
||||||
let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| {
|
let logs_dir = lumotia_storage::file_storage::logs_dir();
|
||||||
EnvFilter::new(
|
if let Some(guard) = install_subscriber(&logs_dir) {
|
||||||
"warn,lumotia=info,lumotia_lib=info,lumotia_core=info,lumotia_audio=info,lumotia_hotkey=info,lumotia_ai_formatting=info,lumotia_llm=info,lumotia_storage=info,lumotia_transcription=info,lumotia_startup=info",
|
// Park the guard for the process lifetime. set() only fails
|
||||||
)
|
// if already set (impossible inside Once::call_once), so we
|
||||||
});
|
// ignore the result.
|
||||||
|
let _ = FILE_APPENDER_GUARD.set(guard);
|
||||||
let _ = tracing_subscriber::fmt()
|
}
|
||||||
.with_env_filter(env_filter)
|
|
||||||
.with_writer(std::io::stderr)
|
|
||||||
.try_init();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
87
src-tauri/tests/tracing_appender_smoke.rs
Normal file
87
src-tauri/tests/tracing_appender_smoke.rs
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
//! Smoke test for the rolling-file tracing appender wired into
|
||||||
|
//! `init_tracing`. Pre-fix, `init_tracing` only installed an stderr
|
||||||
|
//! writer — meaning `lumotia.log` (referenced by the diagnostic-report
|
||||||
|
//! bundler and documented in `crates/storage/src/file_storage.rs`) was
|
||||||
|
//! never written and every user crash report shipped an empty log
|
||||||
|
//! attachment.
|
||||||
|
//!
|
||||||
|
//! This test asserts the post-fix behaviour: calling `install_subscriber`
|
||||||
|
//! against a tempdir creates the rolling log file and writes any
|
||||||
|
//! emitted `tracing::*!` lines to it after the worker thread drains.
|
||||||
|
//!
|
||||||
|
//! Notes:
|
||||||
|
//! - The non-blocking appender writes asynchronously. We force the
|
||||||
|
//! drain by dropping the returned `WorkerGuard` before reading the
|
||||||
|
//! file (the guard's drop blocks until the queue is empty).
|
||||||
|
//! - `tracing-subscriber`'s global dispatcher can only be initialised
|
||||||
|
//! once per process; integration tests each run in their own process
|
||||||
|
//! binary so this is safe.
|
||||||
|
//! - The default file-side `EnvFilter` includes `lumotia=debug` and
|
||||||
|
//! `lumotia_lib=debug`. We emit the test line with explicit target
|
||||||
|
//! `"lumotia_lib"` so the filter allows it through regardless of
|
||||||
|
//! where this test's module path lives.
|
||||||
|
|
||||||
|
use std::fs;
|
||||||
|
use std::thread::sleep;
|
||||||
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn init_tracing_creates_log_file() {
|
||||||
|
let dir = tempdir().expect("create tempdir");
|
||||||
|
let logs_dir = dir.path().to_path_buf();
|
||||||
|
|
||||||
|
// Install the subscriber against the tempdir. The guard is returned
|
||||||
|
// so the test can deterministically flush by dropping it below.
|
||||||
|
let guard = lumotia_lib::install_subscriber(&logs_dir)
|
||||||
|
.expect("install_subscriber should succeed on a writable tempdir");
|
||||||
|
|
||||||
|
// Emit a known-unique marker. `target: "lumotia_lib"` matches the
|
||||||
|
// default file-side EnvFilter (`lumotia_lib=debug`).
|
||||||
|
tracing::info!(target: "lumotia_lib", "tracing_appender_smoke_marker_xyzzy");
|
||||||
|
|
||||||
|
// Drop the guard to flush the non-blocking worker queue. After this
|
||||||
|
// returns, any buffered log lines have been written to disk.
|
||||||
|
drop(guard);
|
||||||
|
|
||||||
|
// Belt-and-braces: a brief sleep in case the OS hasn't yet made
|
||||||
|
// the write visible to a subsequent read (unlikely on Linux, but
|
||||||
|
// cheap insurance for CI on macOS/Windows).
|
||||||
|
sleep(Duration::from_millis(50));
|
||||||
|
|
||||||
|
// Find any file matching `lumotia*log*` in the logs dir. The daily
|
||||||
|
// rotation policy means the live file is named like
|
||||||
|
// `lumotia.log.2026-05-12` (date suffix appended by the appender).
|
||||||
|
let entries: Vec<_> = fs::read_dir(&logs_dir)
|
||||||
|
.expect("read tempdir")
|
||||||
|
.filter_map(Result::ok)
|
||||||
|
.filter(|e| {
|
||||||
|
e.file_name()
|
||||||
|
.to_string_lossy()
|
||||||
|
.starts_with("lumotia")
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!entries.is_empty(),
|
||||||
|
"no lumotia*.log file was created in {}",
|
||||||
|
logs_dir.display()
|
||||||
|
);
|
||||||
|
|
||||||
|
// At least one of those files must contain the marker we emitted.
|
||||||
|
let mut found = false;
|
||||||
|
for entry in &entries {
|
||||||
|
let contents = fs::read_to_string(entry.path()).unwrap_or_default();
|
||||||
|
if contents.contains("tracing_appender_smoke_marker_xyzzy") {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
found,
|
||||||
|
"marker line not found in any of {:?}",
|
||||||
|
entries.iter().map(|e| e.path()).collect::<Vec<_>>()
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user