vtcode_config/loader/session_override.rs
1//! Process-wide explicit config-file override for a session.
2//!
3//! When the user launches vtcode with `--config PATH` or
4//! `VTCODE_CONFIG_PATH`, every later configuration reload must honor that
5//! explicit file instead of silently drifting back to the default layer
6//! hierarchy. The resolved path is captured once during startup and stored
7//! here as a session snapshot, so runtime reloads via
8//! [`ConfigManager::load_from_workspace`] stay deterministic.
9//!
10//! Reloads reuse this stored snapshot rather than re-reading the
11//! environment, which keeps them deterministic and matches `--config`
12//! semantics.
13
14use std::path::PathBuf;
15use std::sync::Mutex;
16use std::sync::OnceLock;
17
18use super::manager::ConfigManager;
19
20static EXPLICIT_CONFIG_PATH: OnceLock<Mutex<Option<PathBuf>>> = OnceLock::new();
21
22fn cell() -> &'static Mutex<Option<PathBuf>> {
23 EXPLICIT_CONFIG_PATH.get_or_init(|| Mutex::new(None))
24}
25
26/// Capture the session's explicit config-file override.
27///
28/// Called during startup after the CLI/env path has been resolved to an
29/// absolute file path. Passing `None` clears the override and invalidates the
30/// workspace config cache so subsequent loads return to the default layer
31/// hierarchy instead of a stale override-loaded manager.
32///
33/// This is process-global and intended to be set once per process at startup;
34/// changing it mid-session is supported for tests and reloads, but consumers
35/// must not race concurrent calls.
36pub fn set_explicit_config_path(path: Option<PathBuf>) {
37 {
38 let mut guard = cell().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
39 *guard = path;
40 }
41 // A cleared or changed override must not leave a stale manager in the
42 // workspace cache: the cache key is the canonical workspace only, so an
43 // override-loaded manager would otherwise leak into later default loads
44 // for any workspace. Invalidate every entry.
45 ConfigManager::invalidate_all_workspace_cache();
46}
47
48/// Return the session's explicit config-file override, if one was captured.
49pub fn explicit_config_path() -> Option<PathBuf> {
50 let guard = cell().lock().unwrap_or_else(|poisoned| poisoned.into_inner());
51 guard.clone()
52}
53
54/// RAII helper for tests: set an override and restore the previous value on drop.
55#[cfg(test)]
56pub(crate) struct ExplicitConfigPathGuard {
57 previous: Option<PathBuf>,
58}
59
60#[cfg(test)]
61impl ExplicitConfigPathGuard {
62 pub(crate) fn set(path: Option<PathBuf>) -> Self {
63 let previous = explicit_config_path();
64 set_explicit_config_path(path);
65 Self { previous }
66 }
67}
68
69#[cfg(test)]
70impl Drop for ExplicitConfigPathGuard {
71 fn drop(&mut self) {
72 set_explicit_config_path(self.previous.take());
73 }
74}