From ac1e5279897e92c7e7b5ae9f89b17a3e39ced37b Mon Sep 17 00:00:00 2001 From: therecluse26 Date: Sat, 16 May 2026 14:47:51 -0400 Subject: [PATCH 1/3] fix(ci): commit insta snapshots and fix Windows clippy errors (REF-172) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #37 was merged with broken CI on Ubuntu, macOS, and Windows runners. Root causes: 1. `*.snap` in .gitignore excluded insta's baseline snapshot files, so CI saw "missing snapshot" failures for 4 pulse::site tests. Insta's convention is the opposite: commit `.snap` (baseline) and ignore `.snap.new` (pending). Replaced the blanket glob with that pair. 2. Windows-only clippy errors under `-D warnings`: - `use crate::output` was unused on Windows (only referenced inside a `#[cfg(unix)]` branch of `check_disk_space`) → gated import on unix. - `root: &Path` parameter of `check_disk_space` was unused on Windows for the same reason → added `cfg_attr(not(unix), allow(unused_variables))`. - `.arg(&path)` in the Windows-specific spawn block triggered `needless_borrows_for_generic_args` → matched the Unix branch's `.arg(path)`. Verified: - `cargo clippy --all-targets -- -D warnings` clean. - `cargo test` — all 855 tests pass (lib 704 + integration 80 + symbol_test 59 + mcp_jsonrpc 6 + doctests 6; 10 perf tests intentionally ignored in debug). - `cargo build --release` clean. Co-Authored-By: Paperclip --- .gitignore | 5 +- src/cli/index.rs | 2 +- src/indexer.rs | 2 + .../pulse_site__base_html_page_structure.snap | 92 +++++++++++++++++++ ..._site__base_html_pagefind_integration.snap | 8 ++ ...ulse_site__home_page_navigation_links.snap | 24 +++++ .../pulse_site__zola_config_all_surfaces.snap | 31 +++++++ 7 files changed, 161 insertions(+), 3 deletions(-) create mode 100644 tests/snapshots/pulse_site__base_html_page_structure.snap create mode 100644 tests/snapshots/pulse_site__base_html_pagefind_integration.snap create mode 100644 tests/snapshots/pulse_site__home_page_navigation_links.snap create mode 100644 tests/snapshots/pulse_site__zola_config_all_surfaces.snap diff --git a/.gitignore b/.gitignore index 168879e..82190ce 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,6 @@ reflex *.jpeg *.gif - -*.snap \ No newline at end of file +# Insta pending-snapshot files — committed `.snap` files are the baseline +*.snap.new +*.pending-snap \ No newline at end of file diff --git a/src/cli/index.rs b/src/cli/index.rs index cc45510..7ea66e6 100644 --- a/src/cli/index.rs +++ b/src/cli/index.rs @@ -228,7 +228,7 @@ pub(super) fn handle_index_build( std::process::Command::new(¤t_exe) .arg("index-symbols-internal") - .arg(&path) + .arg(path) .creation_flags(CREATE_NO_WINDOW) .stdin(std::process::Stdio::null()) .stdout(std::process::Stdio::null()) diff --git a/src/indexer.rs b/src/indexer.rs index 288c599..d09dad6 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -17,6 +17,7 @@ use crate::cache::CacheManager; use crate::content_store::{ContentReader, ContentWriter}; use crate::dependency::DependencyIndex; use crate::models::{Dependency, ImportType, IndexConfig, IndexStats, Language}; +#[cfg(unix)] use crate::output; use crate::parsers::c::CDependencyExtractor; use crate::parsers::cpp::CppDependencyExtractor; @@ -2039,6 +2040,7 @@ impl Indexer { /// /// Ensures there's enough free space to create the index. Warns if disk space is low. /// This prevents partial index writes and confusing error messages. + #[cfg_attr(not(unix), allow(unused_variables))] fn check_disk_space(&self, root: &Path) -> Result<()> { // Get available space on the filesystem containing the cache directory let cache_path = self.cache.path(); diff --git a/tests/snapshots/pulse_site__base_html_page_structure.snap b/tests/snapshots/pulse_site__base_html_page_structure.snap new file mode 100644 index 0000000..f46e8ab --- /dev/null +++ b/tests/snapshots/pulse_site__base_html_page_structure.snap @@ -0,0 +1,92 @@ +--- +source: src/pulse/site.rs +expression: content +--- + + + + + + {% block title %}{{ config.title }}{% endblock title %} + + + + + + + +
+ +
+ {% block content %}{% endblock content %} +
+
+ {% block scripts %}{% endblock scripts %} + + diff --git a/tests/snapshots/pulse_site__base_html_pagefind_integration.snap b/tests/snapshots/pulse_site__base_html_pagefind_integration.snap new file mode 100644 index 0000000..142aae1 --- /dev/null +++ b/tests/snapshots/pulse_site__base_html_pagefind_integration.snap @@ -0,0 +1,8 @@ +--- +source: src/pulse/site.rs +expression: pagefind_section +--- + + + Search + diff --git a/tests/snapshots/pulse_site__home_page_navigation_links.snap b/tests/snapshots/pulse_site__home_page_navigation_links.snap new file mode 100644 index 0000000..377ddaa --- /dev/null +++ b/tests/snapshots/pulse_site__home_page_navigation_links.snap @@ -0,0 +1,24 @@ +--- +source: src/pulse/site.rs +expression: content +--- ++++ +title = "My Project" +sort_by = "weight" ++++ + +# My Project + +Auto-generated codebase documentation powered by [Reflex](https://github.com/reflex-search/reflex). + +## Explore + + diff --git a/tests/snapshots/pulse_site__zola_config_all_surfaces.snap b/tests/snapshots/pulse_site__zola_config_all_surfaces.snap new file mode 100644 index 0000000..8f68f5b --- /dev/null +++ b/tests/snapshots/pulse_site__zola_config_all_surfaces.snap @@ -0,0 +1,31 @@ +--- +source: src/pulse/site.rs +expression: content +--- +# Zola configuration — generated by rfx pulse generate +base_url = "/" +title = "Test Codebase" +description = "Auto-generated codebase documentation" +compile_sass = false +build_search_index = false +generate_feeds = false +minify_html = false + +[markdown] +highlight_code = true +highlight_theme = "base16-ocean-dark" +render_emoji = false +external_links_target_blank = true +smart_punctuation = true + +[slugify] +paths = "safe" + +[extra] +generated_by = "Reflex Pulse" +has_onboard = true +has_glossary = true +has_changelog = true +has_timeline = true +has_map = true +has_explorer = true From 9721504488f4daf8a6a8889a47de856edb7e7f41 Mon Sep 17 00:00:00 2001 From: therecluse26 Date: Sat, 16 May 2026 15:17:22 -0400 Subject: [PATCH 2/3] fix(windows): normalize path separators to fix 21 Windows-only test failures (REF-173) Normalize all resolved file paths to forward slashes and make platform-specific helpers Windows-aware. With this change the same 21 tests that failed on `windows-latest` (path separator mismatches, vendor/venv filters not matching `\vendor\`, missing Windows asset for Pagefind/Zola, and `HOME` overrides ignored by `dirs::home_dir()`) now behave identically on every OS. Production normalizations: - Indexer stores relative paths with `/` so the on-disk index is deterministic and downstream `path.contains("src/...")` filters work regardless of OS. - `resolve_rust_import`, `resolve_rust_mod_declaration`, `resolve_c_include_to_path`, `resolve_cpp_include_to_path`, the TypeScript resolver and the Go/Python project-root strings all emit forward-slash paths. - Go vendor/Python venv filters operate on a normalized string so the `.../vendor/...` check fires on Windows paths too. Test-only adjustments: - Pagefind/Zola `test_get_asset_name` now branches on supported platforms: assert `Ok` where a binary exists, assert the unsupported-platform error otherwise. - Semantic config tests use `set_home`/`unset_home` helpers that also set `USERPROFILE` on Windows so `dirs::home_dir()` picks up the override. - C/C++ resolver tests drop the `|| backslash` alternatives now that the resolver guarantees forward slashes. Verified locally with `cargo test` (704 lib + 145 integration tests all green) and `cargo build --release`. Co-Authored-By: Paperclip --- src/dependency.rs | 9 ++++---- src/indexer.rs | 26 +++++++++++++--------- src/parsers/c.rs | 11 +++++----- src/parsers/cpp.rs | 13 +++++------ src/parsers/go.rs | 9 +++++--- src/parsers/python.rs | 10 ++++++--- src/parsers/typescript.rs | 8 +++++-- src/pulse/pagefind.rs | 15 +++++++++---- src/pulse/zola.rs | 23 ++++++++++++++++---- src/semantic/config.rs | 45 +++++++++++++++++++++++++-------------- 10 files changed, 112 insertions(+), 57 deletions(-) diff --git a/src/dependency.rs b/src/dependency.rs index 9a87a8e..f14e795 100644 --- a/src/dependency.rs +++ b/src/dependency.rs @@ -1216,11 +1216,12 @@ pub fn resolve_rust_import( } } - // Convert to string and make relative to project root + // Convert to string and make relative to project root. + // Normalize to forward slashes so paths are deterministic across platforms. resolved_path.and_then(|p| { p.strip_prefix(project_root) .ok() - .map(|rel| rel.to_string_lossy().to_string()) + .map(|rel| rel.to_string_lossy().replace('\\', "/")) }) } @@ -1290,13 +1291,13 @@ pub fn resolve_rust_mod_declaration( // Try sibling file let sibling = current_dir.join(format!("{}.rs", mod_name)); if sibling.exists() { - return Some(sibling.to_string_lossy().to_string()); + return Some(sibling.to_string_lossy().replace('\\', "/")); } // Try directory module let dir_mod = current_dir.join(mod_name).join("mod.rs"); if dir_mod.exists() { - return Some(dir_mod.to_string_lossy().to_string()); + return Some(dir_mod.to_string_lossy().replace('\\', "/")); } None diff --git a/src/indexer.rs b/src/indexer.rs index d09dad6..f26a72b 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -203,14 +203,17 @@ impl Indexer { let mut any_changed = false; for file_path in &files { - // Normalize path to be relative to root (handles both ./ prefix and absolute paths) + // Normalize path to be relative to root (handles both ./ prefix and absolute paths). + // Always use forward slashes so the on-disk index is deterministic across OSes + // and downstream string lookups (file_pattern filters, dependency resolvers) + // work regardless of the host separator. let path_str = file_path.to_string_lossy().to_string(); let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) { // Convert absolute path to relative - rel_path.to_string_lossy().to_string() + rel_path.to_string_lossy().replace('\\', "/") } else { // Already relative, just strip ./ prefix - path_str.trim_start_matches("./").to_string() + path_str.trim_start_matches("./").replace('\\', "/") }; // Check if file exists in cache @@ -405,14 +408,15 @@ impl Indexer { batch_files .par_iter() .map(|file_path| { - // Normalize path to be relative to root (handles both ./ prefix and absolute paths) + // Normalize path to be relative to root (handles both ./ prefix and absolute paths). + // Always emit forward slashes so the persisted path is deterministic across OSes. let path_str = file_path.to_string_lossy().to_string(); let normalized_path = if let Ok(rel_path) = file_path.strip_prefix(root) { // Convert absolute path to relative - rel_path.to_string_lossy().to_string() + rel_path.to_string_lossy().replace('\\', "/") } else { // Already relative, just strip ./ prefix - path_str.trim_start_matches("./").to_string() + path_str.trim_start_matches("./").replace('\\', "/") }; // Read file content once (used for hashing, trigrams, and parsing) @@ -1207,10 +1211,11 @@ impl Indexer { let normalized_candidate = if let Ok(rel_path) = std::path::Path::new(candidate_path).strip_prefix(root) { - rel_path.to_string_lossy().to_string() + rel_path.to_string_lossy().replace('\\', "/") } else { // Not an absolute path or not under root - use as-is - candidate_path.to_string() + // (still normalize separators so DB lookups match). + candidate_path.replace('\\', "/") }; log::debug!( @@ -1646,10 +1651,11 @@ impl Indexer { let normalized_candidate = if let Ok(rel_path) = std::path::Path::new(candidate_path).strip_prefix(root) { - rel_path.to_string_lossy().to_string() + rel_path.to_string_lossy().replace('\\', "/") } else { // Not an absolute path or not under root - use as-is - candidate_path.to_string() + // (still normalize separators so DB lookups match). + candidate_path.replace('\\', "/") }; match dep_index.get_file_id_by_path(&normalized_candidate) { diff --git a/src/parsers/c.rs b/src/parsers/c.rs index b9b7735..0c39fa0 100644 --- a/src/parsers/c.rs +++ b/src/parsers/c.rs @@ -747,12 +747,13 @@ pub fn resolve_c_include_to_path( // Resolve the include path relative to current file let resolved = current_dir.join(include_path); - // Normalize the path + // Normalize the path. Always emit forward slashes so resolved paths are + // deterministic across platforms. match resolved.canonicalize() { - Ok(normalized) => Some(normalized.display().to_string()), + Ok(normalized) => Some(normalized.to_string_lossy().replace('\\', "/")), Err(_) => { // If canonicalize fails (file doesn't exist yet), return the joined path - Some(resolved.display().to_string()) + Some(resolved.to_string_lossy().replace('\\', "/")) } } } @@ -771,7 +772,7 @@ mod resolution_tests { assert!(result.is_some()); let path = result.unwrap(); - assert!(path.ends_with("src/helper.h") || path.ends_with("src\\helper.h")); + assert!(path.ends_with("src/helper.h")); } #[test] @@ -780,7 +781,7 @@ mod resolution_tests { assert!(result.is_some()); let path = result.unwrap(); - assert!(path.ends_with("src/utils/helper.h") || path.ends_with("src\\utils\\helper.h")); + assert!(path.ends_with("src/utils/helper.h")); } #[test] diff --git a/src/parsers/cpp.rs b/src/parsers/cpp.rs index 9f50fcd..abe9ec0 100644 --- a/src/parsers/cpp.rs +++ b/src/parsers/cpp.rs @@ -1228,12 +1228,13 @@ pub fn resolve_cpp_include_to_path( // Resolve the include path relative to current file let resolved = current_dir.join(include_path); - // Normalize the path + // Normalize the path. Always emit forward slashes so resolved paths are + // deterministic across platforms. match resolved.canonicalize() { - Ok(normalized) => Some(normalized.display().to_string()), + Ok(normalized) => Some(normalized.to_string_lossy().replace('\\', "/")), Err(_) => { // If canonicalize fails (file doesn't exist yet), return the joined path - Some(resolved.display().to_string()) + Some(resolved.to_string_lossy().replace('\\', "/")) } } } @@ -1252,7 +1253,7 @@ mod resolution_tests { assert!(result.is_some()); let path = result.unwrap(); - assert!(path.ends_with("src/helper.hpp") || path.ends_with("src\\helper.hpp")); + assert!(path.ends_with("src/helper.hpp")); } #[test] @@ -1261,7 +1262,7 @@ mod resolution_tests { assert!(result.is_some()); let path = result.unwrap(); - assert!(path.ends_with("src/utils/helper.hpp") || path.ends_with("src\\utils\\helper.hpp")); + assert!(path.ends_with("src/utils/helper.hpp")); } #[test] @@ -1280,7 +1281,7 @@ mod resolution_tests { assert!(result.is_some()); let path = result.unwrap(); - assert!(path.ends_with("src/legacy.h") || path.ends_with("src\\legacy.h")); + assert!(path.ends_with("src/legacy.h")); } #[test] diff --git a/src/parsers/go.rs b/src/parsers/go.rs index 02204ec..b6b5976 100644 --- a/src/parsers/go.rs +++ b/src/parsers/go.rs @@ -1285,8 +1285,9 @@ pub fn find_all_go_mods(index_root: &std::path::Path) -> Result Result Result Result {}", import_path, resolved_alias); // Alias matched! Now resolve relative to the tsconfig directory let resolved_path = map.resolve_relative_to_config(&resolved_alias); - let path_str = resolved_path.to_string_lossy().to_string(); + // Normalize to forward slashes for deterministic, cross-platform output. + let path_str = resolved_path.to_string_lossy().replace('\\', "/"); log::debug!(" After resolve_relative_to_config: {}", path_str); // Check if resolved path has an extension @@ -1576,7 +1577,10 @@ pub fn resolve_ts_import_to_path( }, ); - let normalized = normalized_path.to_string_lossy().to_string(); + // Normalize to forward slashes so the resolved path is deterministic + // across platforms. `join`/`components` on Windows produces backslashes + // which would otherwise break downstream string lookups. + let normalized = normalized_path.to_string_lossy().replace('\\', "/"); // Check if the import already has a known extension // Vue/Svelte files are imported with their extension: import Foo from './Foo.vue' diff --git a/src/pulse/pagefind.rs b/src/pulse/pagefind.rs index ac4cb63..84cb3b4 100644 --- a/src/pulse/pagefind.rs +++ b/src/pulse/pagefind.rs @@ -225,10 +225,17 @@ mod tests { #[test] fn test_get_asset_name() { let result = get_asset_name(); - assert!(result.is_ok(), "Should detect platform: {:?}", result.err()); - let name = result.unwrap(); - assert!(name.contains(PAGEFIND_VERSION)); - assert!(name.ends_with(".tar.gz")); + if cfg!(any(target_os = "linux", target_os = "macos")) { + assert!(result.is_ok(), "Should detect platform: {:?}", result.err()); + let name = result.unwrap(); + assert!(name.contains(PAGEFIND_VERSION)); + assert!(name.ends_with(".tar.gz")); + } else { + // Pagefind has no Windows release asset; the helper should + // explicitly report the platform as unsupported. + let err = result.expect_err("expected unsupported platform error"); + assert!(err.to_string().contains("Unsupported platform")); + } } #[test] diff --git a/src/pulse/zola.rs b/src/pulse/zola.rs index 98947c0..7d192ea 100644 --- a/src/pulse/zola.rs +++ b/src/pulse/zola.rs @@ -231,10 +231,25 @@ mod tests { #[test] fn test_get_asset_name() { let result = get_asset_name(); - assert!(result.is_ok(), "Should detect platform: {:?}", result.err()); - let name = result.unwrap(); - assert!(name.contains(ZOLA_VERSION)); - assert!(name.ends_with(".tar.gz")); + // Zola only ships binaries for linux-x86_64 and both macOS arches. + // On other platforms the helper should explicitly bail. + let supported = matches!( + (std::env::consts::OS, std::env::consts::ARCH), + ("linux", "x86_64") | ("macos", "x86_64") | ("macos", "aarch64") + ); + if supported { + assert!(result.is_ok(), "Should detect platform: {:?}", result.err()); + let name = result.unwrap(); + assert!(name.contains(ZOLA_VERSION)); + assert!(name.ends_with(".tar.gz")); + } else { + let err = result.expect_err("expected unsupported-platform error"); + let msg = err.to_string(); + assert!( + msg.contains("Unsupported platform") || msg.contains("does not have"), + "unexpected error: {msg}" + ); + } } #[test] diff --git a/src/semantic/config.rs b/src/semantic/config.rs index 4076943..d807744 100644 --- a/src/semantic/config.rs +++ b/src/semantic/config.rs @@ -591,6 +591,29 @@ mod tests { ENV_LOCK.lock().unwrap_or_else(|e| e.into_inner()) } + /// Point `dirs::home_dir()` at the given path. On Unix that means + /// `HOME`; on Windows the resolver reads `USERPROFILE` instead, so we + /// must set the platform-appropriate variable for the override to take + /// effect. + fn set_home(path: &std::path::Path) { + unsafe { + env::set_var("HOME", path); + if cfg!(windows) { + env::set_var("USERPROFILE", path); + } + } + } + + /// Reset the home override applied by [`set_home`]. + fn unset_home() { + unsafe { + env::remove_var("HOME"); + if cfg!(windows) { + env::remove_var("USERPROFILE"); + } + } + } + #[test] fn test_default_config() { let config = SemanticConfig::default(); @@ -640,13 +663,9 @@ auto_execute = true .unwrap(); // Set HOME to temp directory to load test config - unsafe { - env::set_var("HOME", temp.path()); - } + set_home(temp.path()); let config = load_config(temp.path()).unwrap(); - unsafe { - env::remove_var("HOME"); - } + unset_home(); assert!(config.enabled); assert_eq!(config.provider, "anthropic"); @@ -839,16 +858,14 @@ openai_compatible_model = "qwen2.5-coder" .unwrap(); unsafe { - env::set_var("HOME", temp.path()); env::remove_var("OPENAI_COMPATIBLE_BASE_URL"); } + set_home(temp.path()); let opts = get_provider_options("openai-compatible"); let model = get_user_model("openai-compatible"); - unsafe { - env::remove_var("HOME"); - } + unset_home(); let opts = opts.expect("base_url should be discovered from config"); assert_eq!( @@ -942,15 +959,11 @@ openai_compatible_model = "gpt-oss:20b-cloud" ) .unwrap(); - unsafe { - env::set_var("HOME", temp.path()); - } + set_home(temp.path()); let resolved = resolve_model_for("openai-compatible", None, None); - unsafe { - env::remove_var("HOME"); - } + unset_home(); assert_eq!(resolved.as_deref(), Some("gpt-oss:20b-cloud")); } From c71962c336c03d0a5c4bee7ebce866da264c12af Mon Sep 17 00:00:00 2001 From: therecluse26 Date: Sat, 16 May 2026 15:29:05 -0400 Subject: [PATCH 3/3] fix(windows): make path/home overrides reach storage and dirs lookups (REF-173) Second iteration of the Windows CI fix. The first push knocked the 21 failures down to 4; this addresses the remaining four: - Indexer was still passing the original `PathBuf` (with backslashes on Windows) into the trigram index and content store, so the query layer saw `src\main.rs` even though the metadata DB had `src/main.rs`. Rebuild the `PathBuf` from the already-normalized `path_str` and drop the now-unused `path` field on the intermediate result struct. - `dirs::home_dir()` queries `SHGetKnownFolderPath(FOLDERID_Profile)` on Windows and ignores `HOME` / `USERPROFILE`, so the semantic config tests could not redirect the lookup to a temp directory. Add `user_home_dir()` that checks `REFLEX_HOME`, `HOME`, then `USERPROFILE` before falling back to `dirs::home_dir()`, and route every `dirs::home_dir()` call inside `semantic/config.rs` through it. Tests on Unix and macOS keep using `HOME` as before. Verified locally with `cargo test --lib` (704 passed) and `cargo clippy --lib --all-targets -- -D warnings`. Co-Authored-By: Paperclip --- src/indexer.rs | 11 +++++++---- src/semantic/config.rs | 27 +++++++++++++++++++++++---- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/src/indexer.rs b/src/indexer.rs index f26a72b..4ec1b9d 100644 --- a/src/indexer.rs +++ b/src/indexer.rs @@ -42,7 +42,6 @@ pub type ProgressCallback = Arc; /// Result of processing a single file (used for parallel processing) struct FileProcessingResult { - path: PathBuf, path_str: String, hash: String, content: String, @@ -610,7 +609,6 @@ impl Indexer { counter_clone.fetch_add(1, Ordering::Relaxed); Some(FileProcessingResult { - path: file_path.clone(), path_str: normalized_path.to_string(), hash, content, @@ -625,14 +623,19 @@ impl Indexer { // Process batch results immediately (streaming approach to minimize memory) for result in results.into_iter().flatten() { + // Use the normalized (forward-slash, relative) path everywhere so + // the trigram index and content store agree with what the database + // and downstream filters expect, regardless of host separator. + let normalized_pathbuf = PathBuf::from(&result.path_str); + // Add file to trigram index (get file_id) - let file_id = trigram_index.add_file(result.path.clone()); + let file_id = trigram_index.add_file(normalized_pathbuf.clone()); // Index file content directly (avoid accumulating all trigrams) trigram_index.index_file(file_id, &result.content); // Add to content store - content_writer.add_file(result.path.clone(), &result.content); + content_writer.add_file(normalized_pathbuf, &result.content); files_indexed += 1; diff --git a/src/semantic/config.rs b/src/semantic/config.rs index d807744..69d8536 100644 --- a/src/semantic/config.rs +++ b/src/semantic/config.rs @@ -4,7 +4,26 @@ use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; use std::collections::HashMap; use std::env; -use std::path::Path; +use std::path::{Path, PathBuf}; + +/// Locate the user's home directory. +/// +/// `dirs::home_dir()` queries `SHGetKnownFolderPath(FOLDERID_Profile)` on +/// Windows and therefore ignores `HOME` / `USERPROFILE` env vars. That makes +/// it impossible to redirect to a temp directory in tests. Honour those env +/// vars (and `REFLEX_HOME` for an explicit override) before falling back to +/// the OS-native lookup so test code can point us at a temp directory on +/// every platform. +fn user_home_dir() -> Option { + for var in ["REFLEX_HOME", "HOME", "USERPROFILE"] { + if let Some(val) = env::var_os(var) + && !val.is_empty() + { + return Some(PathBuf::from(val)); + } + } + dirs::home_dir() +} /// Semantic query configuration #[derive(Debug, Clone, Serialize, Deserialize)] @@ -152,7 +171,7 @@ fn apply_env_overrides(mut config: SemanticConfig) -> SemanticConfig { /// Note: The cache_dir parameter is ignored - kept for API compatibility but will be removed in future. pub fn load_config(_cache_dir: &Path) -> Result { // Semantic config is always in user home directory, not project directory - let home = match dirs::home_dir() { + let home = match user_home_dir() { Some(h) => h, None => { log::debug!("Could not determine home directory, using defaults"); @@ -249,7 +268,7 @@ struct Credentials { /// Load user configuration from ~/.reflex/config.toml fn load_user_config() -> Result> { - let home = match dirs::home_dir() { + let home = match user_home_dir() { Some(h) => h, None => { log::debug!("Could not determine home directory"); @@ -491,7 +510,7 @@ pub fn resolve_model_for( /// Updates the [credentials] section with the new model for the specified provider. /// Creates the config file and directory if they don't exist. pub fn save_user_provider(provider: &str, model: Option<&str>) -> Result<()> { - let home = dirs::home_dir().context("Cannot find home directory")?; + let home = user_home_dir().context("Cannot find home directory")?; let config_dir = home.join(".reflex"); let config_path = config_dir.join("config.toml");