diff --git a/api/config.py b/api/config.py index 5028a92c7..2704d066d 100644 --- a/api/config.py +++ b/api/config.py @@ -510,15 +510,18 @@ def get_config_for_profile_home(profile_home: "Path | str | None") -> dict: target = Path(profile_home).expanduser() except Exception: return get_config() - if not target.exists(): - return {} # If the ambient resolver already points at this profile home, defer to - # get_config() so in-memory overrides (monkeypatched cfg) are honored. + # get_config() so in-memory overrides (monkeypatched cfg) are honored. This + # MUST run before the nonexistent-home guard below: a matching ambient home + # whose directory doesn't physically exist yet (fresh install, monkeypatched + # cfg) must still resolve through get_config(), not return {} (#4516 gate). try: if _get_config_path().parent == target: return get_config() except Exception: pass + if not target.exists(): + return {} # Read the profile file directly and apply documented defaults locally so the # returned dict matches ambient get_config() shape (including built-in # personalities) without mutating any global cache state. diff --git a/tests/test_issue3294_profile_toolset_scoping.py b/tests/test_issue3294_profile_toolset_scoping.py index 7799f53b1..d3e4b40de 100644 --- a/tests/test_issue3294_profile_toolset_scoping.py +++ b/tests/test_issue3294_profile_toolset_scoping.py @@ -108,6 +108,22 @@ def test_matching_home_defers_to_get_config(_two_profiles): ] +def test_matching_ambient_home_that_does_not_exist_still_defers_to_get_config(_two_profiles, monkeypatch, tmp_path): + """Regression (#4516 gate): the nonexistent-home guard must run AFTER the + ambient-resolver short-circuit. A home that matches the ambient config path + but whose directory doesn't physically exist (fresh install / monkeypatched + cfg with no dir on disk) must still defer to get_config() — NOT return {}.""" + cfg, default_home, profile_b_home = _two_profiles + ghost_ambient = tmp_path / "ghost-ambient" + monkeypatch.setenv("HERMES_CONFIG_PATH", str(ghost_ambient / "config.yaml")) + cfg.reload_config() + assert not ghost_ambient.exists() + # The requested home matches the (nonexistent) ambient home → must defer to + # get_config(), honoring the in-memory cfg, not short-circuit to {}. + result = cfg.get_config_for_profile_home(ghost_ambient) + assert result == cfg.get_config() + + def test_empty_or_none_home_falls_back_to_get_config(_two_profiles): """A missing/empty profile home (ImportError fallback path in the worker) must not crash — it falls back to the ambient get_config()."""