Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crates/forge_app/src/fmt/fmt_input.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ impl FormatContent for ToolCatalog {
ToolCatalog::MultiPatch(input) => {
let display_path = display_path_for(&input.file_path);
Some(
TitleFormat::debug("Multi-patch")
TitleFormat::debug("Replace")
.sub_title(format!("{} ({} edits)", display_path, input.edits.len()))
.into(),
)
Expand Down
25 changes: 19 additions & 6 deletions crates/forge_config/src/reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,13 @@ impl ConfigReader {
///
/// If the `FORGE_CONFIG` environment variable is set, its value is used
/// directly as the base path. Otherwise defaults to `~/.forge`.
/// Falls back to the legacy `~/forge` path if it exists and `~/.forge`
/// does not.
/// Falls back to the legacy `~/forge` path if it exists, even if `~/.forge`
/// also exists. This prevents tools that eagerly create `~/.forge` (such as
/// the shell plugin's config-edit action) from silently switching the
/// active base path while the user's credentials and config still live
/// in `~/forge`. Once the user runs `forge config migrate` the
/// `~/forge` directory is removed, so this fallback naturally stops
/// applying.
pub fn base_path() -> PathBuf {
if let Ok(path) = std::env::var("FORGE_CONFIG") {
return PathBuf::from(path);
Expand All @@ -64,8 +69,10 @@ impl ConfigReader {
let path = home.join(".forge");
let legacy_path = home.join("forge");

// Prefer the new dotfile path, but fall back to legacy if only it exists
if !path.exists() && legacy_path.exists() {
// Prefer the legacy ~/forge path while it still exists so that an
// empty ~/.forge directory (e.g. created by `mkdir -p` in the shell
// plugin) does not cause the base path to flip before migration.
if legacy_path.exists() {
tracing::info!("Using legacy path");
return legacy_path;
}
Expand Down Expand Up @@ -195,8 +202,14 @@ mod tests {
#[test]
fn test_base_path_falls_back_to_home_dir_when_env_var_absent() {
let actual = ConfigReader::base_path();
// Without FORGE_CONFIG set the path must end with ".forge"
assert_eq!(actual.file_name().unwrap(), ".forge");
// Without FORGE_CONFIG set the path must be either ".forge" (new) or
// "forge" (legacy fallback when ~/forge exists on this machine).
let name = actual.file_name().unwrap();
assert!(
name == ".forge" || name == "forge",
"Expected base_path to end with '.forge' or 'forge', got: {:?}",
name
);
Comment on lines +205 to +212
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Test is now non-deterministic and depends on the host filesystem state. If ~/forge exists on the CI/CD runner or developer's machine, the test validates different behavior than if it doesn't exist. This makes test results non-reproducible and can hide regressions.

Impact: Different environments will test different code paths, reducing test reliability.

Fix: Use filesystem isolation for deterministic testing:

#[test]
fn test_base_path_prefers_legacy_when_it_exists() {
    let temp = TempDir::new().unwrap();
    env::set_var("HOME", temp.path());
    
    fs::create_dir(temp.path().join("forge")).unwrap();
    let actual = ConfigReader::base_path();
    assert_eq!(actual.file_name().unwrap(), "forge");
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

}

#[test]
Expand Down
10 changes: 8 additions & 2 deletions crates/forge_infra/src/env.rs
Original file line number Diff line number Diff line change
Expand Up @@ -213,8 +213,14 @@ mod tests {
#[test]
fn test_to_environment_falls_back_to_home_dir_when_env_var_absent() {
let actual = to_environment(PathBuf::from("/any/cwd"));
// Without FORGE_CONFIG the base_path must end with ".forge"
assert_eq!(actual.base_path.file_name().unwrap(), ".forge");
// Without FORGE_CONFIG the base_path must be either ".forge" (new default)
// or "forge" (legacy fallback when ~/forge exists on this machine).
let name = actual.base_path.file_name().unwrap();
assert!(
name == ".forge" || name == "forge",
"Expected base_path to end with '.forge' or 'forge', got: {:?}",
name
);
}

#[test]
Expand Down
4 changes: 2 additions & 2 deletions shell-plugin/lib/actions/config.zsh
Original file line number Diff line number Diff line change
Expand Up @@ -466,9 +466,9 @@ function _forge_action_config_edit() {
# Resolve config file path via the forge binary (honours FORGE_CONFIG,
# new ~/.forge path, and legacy ~/forge fallback automatically)
local config_file
config_file=$(forge config path 2>/dev/null)
config_file=$($FORGE_BIN config path 2>/dev/null)
if [[ -z "$config_file" ]]; then
_forge_log error "Failed to resolve config path from 'forge config path'"
_forge_log error "Failed to resolve config path from '$FORGE_BIN config path'"
return 1
fi

Expand Down
Loading