"""The per-path ``load_env_file()`` memo in ``agent.secret_scope``. ``build_profile_secret_scope()`` sits on the gateway's hot paths (every turn, cron fire, MCP/browser adoption, housekeeping drain) and used to re-read and re-parse the profile ``.env`` on each call. A stale credential map is a worse failure than a slow one, so the contract under test is: parse once while the file is unchanged, hand every caller its own dict, and see every real change on disk. Files are written as bytes: ``Path.write_text`` translates ``\\n`` to ``\\r\\n`` on Windows, which would break the same-size assertion. """ from __future__ import annotations import os import pytest import agent.secret_scope as ss # Comfortably past the coarsest mtime resolution in common use (FAT32 truncates to two seconds). _MTIME_STEP_NS = 10_000_000_000 @pytest.fixture(autouse=True) def _clear_cache(): ss.invalidate_env_file_cache() yield ss.invalidate_env_file_cache() def _count_parses(monkeypatch) -> list: calls: list = [] real = ss._parse_env_text monkeypatch.setattr(ss, "_parse_env_text", lambda t: (calls.append(t), real(t))[1]) return calls def _rewrite_same_size(path, text: str) -> None: """Same length, different content: only the mtime distinguishes the versions.""" previous = path.stat() path.write_bytes(text.encode("utf-8")) assert path.stat().st_size == previous.st_size bumped = previous.st_mtime_ns + _MTIME_STEP_NS os.utime(path, ns=(bumped, bumped)) def test_unchanged_file_is_parsed_once_and_each_caller_gets_its_own_dict(tmp_path, monkeypatch): env = tmp_path / ".env" env.write_bytes(b"A=1\nB=2\n") calls = _count_parses(monkeypatch) monkeypatch.setattr("hermes_cli.env_loader.get_secret_source_values", lambda home: {"EXT": "vault"}) first = ss.load_env_file(env) first["INJECTED"] = "must-not-persist" # callers mutate what they get back for _ in range(5): assert ss.load_env_file(env) == {"A": "1", "B": "2"} # The motivating path layers external secrets onto the result; that must not poison the memo. scope = ss.build_profile_secret_scope(tmp_path) assert scope["EXT"] == "vault" assert ss.load_env_file(env) == {"A": "1", "B": "2"} assert len(calls) == 1, f"expected one parse, got {len(calls)}" def test_edits_and_deletion_are_seen(tmp_path, monkeypatch): env = tmp_path / ".env" env.write_bytes(b"KEY=aaa\n") calls = _count_parses(monkeypatch) assert ss.load_env_file(env) == {"KEY": "aaa"} _rewrite_same_size(env, "KEY=bbb\n") assert ss.load_env_file(env) == {"KEY": "bbb"} assert len(calls) == 2 env.unlink() assert ss.load_env_file(env) == {} env.write_bytes(b"KEY=ccc\n") assert ss.load_env_file(env) == {"KEY": "ccc"} assert len(calls) == 3