"""TDD: [force_track] whitelist in .museignore overrides the built-in secrets blocklist. Force-track lets a repo explicitly commit files that would otherwise be blocked by the engine-level secrets blocklist (*.key, *.pem, .env, …). The canonical use-case is dev infrastructure: a self-signed localhost TLS cert/key that lives in the repo so it survives clean pulls. Design constraints tested here: - exact paths only in [force_track].paths — no globs (deliberate, unambiguous) - overrides BOTH the built-in secrets blocklist AND user .museignore patterns - parse errors are handled gracefully; absent section → empty whitelist - walk_workdir / walk_workdir_with_dirs both respect the whitelist """ from __future__ import annotations import pathlib import pytest from muse.core.ignore import ( MuseIgnoreConfig, load_force_track_paths, load_ignore_config, ) from muse.core.snapshot import _BUILTIN_SECRET_PATTERNS, walk_workdir from muse.core.paths import muse_dir, repo_json_path # --------------------------------------------------------------------------- # load_ignore_config — [force_track] parsing # --------------------------------------------------------------------------- class TestLoadIgnoreConfigForceTrack: def test_missing_section_returns_no_force_track_key( self, tmp_path: pathlib.Path ) -> None: (tmp_path / ".museignore").write_text('[global]\npatterns = ["*.tmp"]\n') config = load_ignore_config(tmp_path) assert "force_track" not in config def test_force_track_section_parsed(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text( "[force_track]\npaths = [\"deploy/local-tls/localhost.key\"]\n" ) config = load_ignore_config(tmp_path) assert config.get("force_track", {}).get("paths") == [ "deploy/local-tls/localhost.key" ] def test_force_track_multiple_paths(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text( "[force_track]\npaths = [\n" " \"deploy/local-tls/localhost.key\",\n" " \"deploy/local-tls/localhost.crt\",\n" "]\n" ) config = load_ignore_config(tmp_path) paths = config.get("force_track", {}).get("paths", []) assert "deploy/local-tls/localhost.key" in paths assert "deploy/local-tls/localhost.crt" in paths assert len(paths) == 2 def test_force_track_empty_paths_list(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text("[force_track]\npaths = []\n") config = load_ignore_config(tmp_path) assert config.get("force_track", {}).get("paths") == [] def test_force_track_missing_paths_key(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text("[force_track]\n") config = load_ignore_config(tmp_path) assert config.get("force_track", {}).get("paths") is None def test_force_track_non_string_paths_silently_dropped( self, tmp_path: pathlib.Path ) -> None: (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["good/path.key", 42, true, "other.pem"]\n' ) config = load_ignore_config(tmp_path) paths = config.get("force_track", {}).get("paths", []) assert paths == ["good/path.key", "other.pem"] def test_force_track_coexists_with_global_and_domain( self, tmp_path: pathlib.Path ) -> None: (tmp_path / ".museignore").write_text( '[global]\npatterns = ["*.tmp"]\n' '[domain.code]\npatterns = ["build/"]\n' '[force_track]\npaths = ["infra/dev.key"]\n' ) config = load_ignore_config(tmp_path) assert config.get("global", {}).get("patterns") == ["*.tmp"] assert config.get("domain", {}).get("code", {}).get("patterns") == ["build/"] assert config.get("force_track", {}).get("paths") == ["infra/dev.key"] def test_no_museignore_returns_empty_config(self, tmp_path: pathlib.Path) -> None: config = load_ignore_config(tmp_path) assert config == {} # --------------------------------------------------------------------------- # load_force_track_paths — convenience helper # --------------------------------------------------------------------------- class TestLoadForceTrackPaths: def test_no_museignore_returns_empty_frozenset( self, tmp_path: pathlib.Path ) -> None: result = load_force_track_paths(tmp_path) assert result == frozenset() def test_no_force_track_section_returns_empty( self, tmp_path: pathlib.Path ) -> None: (tmp_path / ".museignore").write_text('[global]\npatterns = ["*.tmp"]\n') result = load_force_track_paths(tmp_path) assert result == frozenset() def test_returns_frozenset_of_paths(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["deploy/tls/dev.key", "deploy/tls/dev.crt"]\n' ) result = load_force_track_paths(tmp_path) assert isinstance(result, frozenset) assert result == frozenset({"deploy/tls/dev.key", "deploy/tls/dev.crt"}) def test_empty_paths_returns_empty_frozenset( self, tmp_path: pathlib.Path ) -> None: (tmp_path / ".museignore").write_text("[force_track]\npaths = []\n") result = load_force_track_paths(tmp_path) assert result == frozenset() def test_paths_use_posix_separators(self, tmp_path: pathlib.Path) -> None: (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["deploy/local-tls/localhost.key"]\n' ) result = load_force_track_paths(tmp_path) assert "deploy/local-tls/localhost.key" in result # --------------------------------------------------------------------------- # Integration: walk_workdir respects [force_track] # --------------------------------------------------------------------------- def _init_code_repo(root: pathlib.Path) -> None: """Minimal .muse/repo.json so load_ignore_patterns detects domain=code.""" muse_dir(root).mkdir(exist_ok=True) (repo_json_path(root)).write_text('{"repo_id": "x", "domain": "code"}') class TestForceTrackWalkWorkdir: def test_key_file_excluded_without_force_track( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "deploy").mkdir() (tmp_path / "deploy" / "server.key").write_bytes(b"private key bytes") (tmp_path / "app.py").write_text("x = 1\n") manifest = walk_workdir(tmp_path) assert "deploy/server.key" not in manifest assert "app.py" in manifest def test_key_file_included_when_force_tracked( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "deploy").mkdir() (tmp_path / "deploy" / "server.key").write_bytes(b"private key bytes") (tmp_path / "app.py").write_text("x = 1\n") (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["deploy/server.key"]\n' ) manifest = walk_workdir(tmp_path) assert "deploy/server.key" in manifest assert "app.py" in manifest def test_pem_file_included_when_force_tracked( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "infra").mkdir() (tmp_path / "infra" / "ca.pem").write_bytes(b"cert chain") (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["infra/ca.pem"]\n' ) manifest = walk_workdir(tmp_path) assert "infra/ca.pem" in manifest def test_env_file_included_when_force_tracked( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / ".env").write_text("DEV_ONLY=true\n") (tmp_path / ".museignore").write_text( '[force_track]\npaths = [".env"]\n' ) manifest = walk_workdir(tmp_path) assert ".env" in manifest def test_non_force_tracked_secret_still_blocked( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "other.key").write_bytes(b"another key") (tmp_path / "deploy").mkdir() (tmp_path / "deploy" / "server.key").write_bytes(b"dev key") (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["deploy/server.key"]\n' ) manifest = walk_workdir(tmp_path) assert "deploy/server.key" in manifest assert "other.key" not in manifest # not whitelisted def test_force_track_overrides_user_ignore_patterns( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "config").mkdir() (tmp_path / "config" / "local.cfg").write_text("debug=true\n") (tmp_path / ".museignore").write_text( '[global]\npatterns = ["config/"]\n' '[force_track]\npaths = ["config/local.cfg"]\n' ) manifest = walk_workdir(tmp_path) assert "config/local.cfg" in manifest def test_force_track_exact_path_only_no_glob_expansion( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "a.key").write_bytes(b"key a") (tmp_path / "b.key").write_bytes(b"key b") # Listing "*.key" in force_track should NOT glob-expand — it's an exact path. (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["*.key"]\n' ) manifest = walk_workdir(tmp_path) # "*.key" is not a real filename so neither file matches the exact path. assert "a.key" not in manifest assert "b.key" not in manifest def test_force_track_nonexistent_path_no_error( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / "app.py").write_text("x = 1\n") (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["ghost.key"]\n' ) manifest = walk_workdir(tmp_path) assert "app.py" in manifest # normal files still tracked def test_localhost_tls_use_case(self, tmp_path: pathlib.Path) -> None: """The canonical use-case: dev TLS cert+key committed to the repo.""" _init_code_repo(tmp_path) tls = tmp_path / "deploy" / "local-tls" tls.mkdir(parents=True) (tls / "localhost.crt").write_bytes(b"cert bytes") (tls / "localhost.key").write_bytes(b"private key bytes") (tmp_path / "app.py").write_text("x = 1\n") (tmp_path / ".museignore").write_text( "[force_track]\npaths = [\n" " \"deploy/local-tls/localhost.crt\",\n" " \"deploy/local-tls/localhost.key\",\n" "]\n" ) manifest = walk_workdir(tmp_path) assert "deploy/local-tls/localhost.crt" in manifest assert "deploy/local-tls/localhost.key" in manifest assert "app.py" in manifest def test_force_track_does_not_affect_unrelated_secrets( self, tmp_path: pathlib.Path ) -> None: _init_code_repo(tmp_path) (tmp_path / ".env").write_text("SECRET=hunter2\n") (tmp_path / "deploy").mkdir() (tmp_path / "deploy" / "localhost.key").write_bytes(b"key") # Only localhost.key is whitelisted — .env must still be blocked. (tmp_path / ".museignore").write_text( '[force_track]\npaths = ["deploy/localhost.key"]\n' ) manifest = walk_workdir(tmp_path) assert "deploy/localhost.key" in manifest assert ".env" not in manifest