From 4e173335164c6bcdc2ef8bcc35fc0e93259a7570 Mon Sep 17 00:00:00 2001 From: carlotestor Date: Wed, 23 Sep 2026 15:52:14 +0000 Subject: [PATCH] fix: fingerprint .env key names with the provider .env loader --- api/config.py | 23 ++++--------------- ...profile_switch_models_cache_source_axes.py | 18 +++++++++++++++ 2 files changed, 23 insertions(+), 18 deletions(-) diff --git a/api/config.py b/api/config.py index 020b010c..05094e48 100644 --- a/api/config.py +++ b/api/config.py @@ -7918,28 +7918,15 @@ def _models_cache_env_fingerprint(path: Path) -> dict: Credential presence decides which providers are detected; values are never recorded, so rotating a key keeps the snapshot. """ + # Same parser provider detection uses, so the key names match exactly. + from api.providers import _load_env_file + p = Path(path).expanduser() fp: dict = {"path": str(p)} - try: - text = p.read_text(encoding="utf-8") - except FileNotFoundError: + if not p.exists(): fp["missing"] = True return fp - except OSError: - st = p.stat() - fp["mtime_ns"], fp["size"] = st.st_mtime_ns, st.st_size - return fp - keys = set() - for line in text.splitlines(): - line = line.strip() - if line.startswith("export "): - line = line[len("export "):].lstrip() - if not line or line.startswith("#") or "=" not in line: - continue - k, v = line.split("=", 1) - if v.strip().strip('"').strip("'"): - keys.add(k.strip()) - fp["present_keys"] = sorted(keys) + fp["present_keys"] = sorted(k for k, v in _load_env_file(p).items() if v) return fp diff --git a/tests/test_profile_switch_models_cache_source_axes.py b/tests/test_profile_switch_models_cache_source_axes.py index 90a86948..ffba7d5e 100644 --- a/tests/test_profile_switch_models_cache_source_axes.py +++ b/tests/test_profile_switch_models_cache_source_axes.py @@ -118,6 +118,24 @@ def test_switch_after_removing_env_key_rejects_snapshot(two_profiles): assert _switch_to_demo_and_fetch(two_profiles)["default_model"] == "fresh-model" +def test_switch_after_export_prefix_rejects_snapshot(two_profiles): + """`export KEY=` is key `export KEY` to the profile .env loaders, so the provider key is gone.""" + env_file = two_profiles.demo_home / ".env" + env_file.write_text("DEEPSEEK_API_KEY=sk-old\n", encoding="utf-8") + _save_demo_snapshot(two_profiles) + env_file.write_text("export DEEPSEEK_API_KEY=sk-old\n", encoding="utf-8") + assert _switch_to_demo_and_fetch(two_profiles)["default_model"] == "fresh-model" + + +def test_env_fingerprint_keys_match_provider_env_loader(two_profiles): + from api.providers import _load_env_file + + env_file = two_profiles.demo_home / ".env" + env_file.write_text("export A=1\nB='x'\nC=\n# D=1\n", encoding="utf-8") + loaded = sorted(k for k, v in _load_env_file(env_file).items() if v) + assert two_profiles.cfg._models_cache_env_fingerprint(env_file)["present_keys"] == loaded + + def test_env_value_rotation_keeps_snapshot_and_fingerprint_has_no_secret(two_profiles): env_file = two_profiles.demo_home / ".env" env_file.write_text("DEEPSEEK_API_KEY=sk-secret-one\n", encoding="utf-8")