test_force_track.py
python
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b
fix: try fetch/presign before fetch/mpack to avoid Cloudfla…
Sonnet 4.6
patch
6 days ago
| 1 | """TDD: [force_track] whitelist in .museignore overrides the built-in secrets blocklist. |
| 2 | |
| 3 | Force-track lets a repo explicitly commit files that would otherwise be blocked |
| 4 | by the engine-level secrets blocklist (*.key, *.pem, .env, …). The canonical |
| 5 | use-case is dev infrastructure: a self-signed localhost TLS cert/key that lives |
| 6 | in the repo so it survives clean pulls. |
| 7 | |
| 8 | Design constraints tested here: |
| 9 | - exact paths only in [force_track].paths — no globs (deliberate, unambiguous) |
| 10 | - overrides BOTH the built-in secrets blocklist AND user .museignore patterns |
| 11 | - parse errors are handled gracefully; absent section → empty whitelist |
| 12 | - walk_workdir / walk_workdir_with_dirs both respect the whitelist |
| 13 | """ |
| 14 | |
| 15 | from __future__ import annotations |
| 16 | |
| 17 | import pathlib |
| 18 | |
| 19 | import pytest |
| 20 | |
| 21 | from muse.core.ignore import ( |
| 22 | MuseIgnoreConfig, |
| 23 | load_force_track_paths, |
| 24 | load_ignore_config, |
| 25 | ) |
| 26 | from muse.core.snapshot import _BUILTIN_SECRET_PATTERNS, walk_workdir |
| 27 | from muse.core.paths import muse_dir, repo_json_path |
| 28 | |
| 29 | |
| 30 | # --------------------------------------------------------------------------- |
| 31 | # load_ignore_config — [force_track] parsing |
| 32 | # --------------------------------------------------------------------------- |
| 33 | |
| 34 | |
| 35 | class TestLoadIgnoreConfigForceTrack: |
| 36 | def test_missing_section_returns_no_force_track_key( |
| 37 | self, tmp_path: pathlib.Path |
| 38 | ) -> None: |
| 39 | (tmp_path / ".museignore").write_text('[global]\npatterns = ["*.tmp"]\n') |
| 40 | config = load_ignore_config(tmp_path) |
| 41 | assert "force_track" not in config |
| 42 | |
| 43 | def test_force_track_section_parsed(self, tmp_path: pathlib.Path) -> None: |
| 44 | (tmp_path / ".museignore").write_text( |
| 45 | "[force_track]\npaths = [\"deploy/local-tls/localhost.key\"]\n" |
| 46 | ) |
| 47 | config = load_ignore_config(tmp_path) |
| 48 | assert config.get("force_track", {}).get("paths") == [ |
| 49 | "deploy/local-tls/localhost.key" |
| 50 | ] |
| 51 | |
| 52 | def test_force_track_multiple_paths(self, tmp_path: pathlib.Path) -> None: |
| 53 | (tmp_path / ".museignore").write_text( |
| 54 | "[force_track]\npaths = [\n" |
| 55 | " \"deploy/local-tls/localhost.key\",\n" |
| 56 | " \"deploy/local-tls/localhost.crt\",\n" |
| 57 | "]\n" |
| 58 | ) |
| 59 | config = load_ignore_config(tmp_path) |
| 60 | paths = config.get("force_track", {}).get("paths", []) |
| 61 | assert "deploy/local-tls/localhost.key" in paths |
| 62 | assert "deploy/local-tls/localhost.crt" in paths |
| 63 | assert len(paths) == 2 |
| 64 | |
| 65 | def test_force_track_empty_paths_list(self, tmp_path: pathlib.Path) -> None: |
| 66 | (tmp_path / ".museignore").write_text("[force_track]\npaths = []\n") |
| 67 | config = load_ignore_config(tmp_path) |
| 68 | assert config.get("force_track", {}).get("paths") == [] |
| 69 | |
| 70 | def test_force_track_missing_paths_key(self, tmp_path: pathlib.Path) -> None: |
| 71 | (tmp_path / ".museignore").write_text("[force_track]\n") |
| 72 | config = load_ignore_config(tmp_path) |
| 73 | assert config.get("force_track", {}).get("paths") is None |
| 74 | |
| 75 | def test_force_track_non_string_paths_silently_dropped( |
| 76 | self, tmp_path: pathlib.Path |
| 77 | ) -> None: |
| 78 | (tmp_path / ".museignore").write_text( |
| 79 | '[force_track]\npaths = ["good/path.key", 42, true, "other.pem"]\n' |
| 80 | ) |
| 81 | config = load_ignore_config(tmp_path) |
| 82 | paths = config.get("force_track", {}).get("paths", []) |
| 83 | assert paths == ["good/path.key", "other.pem"] |
| 84 | |
| 85 | def test_force_track_coexists_with_global_and_domain( |
| 86 | self, tmp_path: pathlib.Path |
| 87 | ) -> None: |
| 88 | (tmp_path / ".museignore").write_text( |
| 89 | '[global]\npatterns = ["*.tmp"]\n' |
| 90 | '[domain.code]\npatterns = ["build/"]\n' |
| 91 | '[force_track]\npaths = ["infra/dev.key"]\n' |
| 92 | ) |
| 93 | config = load_ignore_config(tmp_path) |
| 94 | assert config.get("global", {}).get("patterns") == ["*.tmp"] |
| 95 | assert config.get("domain", {}).get("code", {}).get("patterns") == ["build/"] |
| 96 | assert config.get("force_track", {}).get("paths") == ["infra/dev.key"] |
| 97 | |
| 98 | def test_no_museignore_returns_empty_config(self, tmp_path: pathlib.Path) -> None: |
| 99 | config = load_ignore_config(tmp_path) |
| 100 | assert config == {} |
| 101 | |
| 102 | |
| 103 | # --------------------------------------------------------------------------- |
| 104 | # load_force_track_paths — convenience helper |
| 105 | # --------------------------------------------------------------------------- |
| 106 | |
| 107 | |
| 108 | class TestLoadForceTrackPaths: |
| 109 | def test_no_museignore_returns_empty_frozenset( |
| 110 | self, tmp_path: pathlib.Path |
| 111 | ) -> None: |
| 112 | result = load_force_track_paths(tmp_path) |
| 113 | assert result == frozenset() |
| 114 | |
| 115 | def test_no_force_track_section_returns_empty( |
| 116 | self, tmp_path: pathlib.Path |
| 117 | ) -> None: |
| 118 | (tmp_path / ".museignore").write_text('[global]\npatterns = ["*.tmp"]\n') |
| 119 | result = load_force_track_paths(tmp_path) |
| 120 | assert result == frozenset() |
| 121 | |
| 122 | def test_returns_frozenset_of_paths(self, tmp_path: pathlib.Path) -> None: |
| 123 | (tmp_path / ".museignore").write_text( |
| 124 | '[force_track]\npaths = ["deploy/tls/dev.key", "deploy/tls/dev.crt"]\n' |
| 125 | ) |
| 126 | result = load_force_track_paths(tmp_path) |
| 127 | assert isinstance(result, frozenset) |
| 128 | assert result == frozenset({"deploy/tls/dev.key", "deploy/tls/dev.crt"}) |
| 129 | |
| 130 | def test_empty_paths_returns_empty_frozenset( |
| 131 | self, tmp_path: pathlib.Path |
| 132 | ) -> None: |
| 133 | (tmp_path / ".museignore").write_text("[force_track]\npaths = []\n") |
| 134 | result = load_force_track_paths(tmp_path) |
| 135 | assert result == frozenset() |
| 136 | |
| 137 | def test_paths_use_posix_separators(self, tmp_path: pathlib.Path) -> None: |
| 138 | (tmp_path / ".museignore").write_text( |
| 139 | '[force_track]\npaths = ["deploy/local-tls/localhost.key"]\n' |
| 140 | ) |
| 141 | result = load_force_track_paths(tmp_path) |
| 142 | assert "deploy/local-tls/localhost.key" in result |
| 143 | |
| 144 | |
| 145 | # --------------------------------------------------------------------------- |
| 146 | # Integration: walk_workdir respects [force_track] |
| 147 | # --------------------------------------------------------------------------- |
| 148 | |
| 149 | |
| 150 | def _init_code_repo(root: pathlib.Path) -> None: |
| 151 | """Minimal .muse/repo.json so load_ignore_patterns detects domain=code.""" |
| 152 | muse_dir(root).mkdir(exist_ok=True) |
| 153 | (repo_json_path(root)).write_text('{"repo_id": "x", "domain": "code"}') |
| 154 | |
| 155 | |
| 156 | class TestForceTrackWalkWorkdir: |
| 157 | def test_key_file_excluded_without_force_track( |
| 158 | self, tmp_path: pathlib.Path |
| 159 | ) -> None: |
| 160 | _init_code_repo(tmp_path) |
| 161 | (tmp_path / "deploy").mkdir() |
| 162 | (tmp_path / "deploy" / "server.key").write_bytes(b"private key bytes") |
| 163 | (tmp_path / "app.py").write_text("x = 1\n") |
| 164 | |
| 165 | manifest = walk_workdir(tmp_path) |
| 166 | assert "deploy/server.key" not in manifest |
| 167 | assert "app.py" in manifest |
| 168 | |
| 169 | def test_key_file_included_when_force_tracked( |
| 170 | self, tmp_path: pathlib.Path |
| 171 | ) -> None: |
| 172 | _init_code_repo(tmp_path) |
| 173 | (tmp_path / "deploy").mkdir() |
| 174 | (tmp_path / "deploy" / "server.key").write_bytes(b"private key bytes") |
| 175 | (tmp_path / "app.py").write_text("x = 1\n") |
| 176 | (tmp_path / ".museignore").write_text( |
| 177 | '[force_track]\npaths = ["deploy/server.key"]\n' |
| 178 | ) |
| 179 | |
| 180 | manifest = walk_workdir(tmp_path) |
| 181 | assert "deploy/server.key" in manifest |
| 182 | assert "app.py" in manifest |
| 183 | |
| 184 | def test_pem_file_included_when_force_tracked( |
| 185 | self, tmp_path: pathlib.Path |
| 186 | ) -> None: |
| 187 | _init_code_repo(tmp_path) |
| 188 | (tmp_path / "infra").mkdir() |
| 189 | (tmp_path / "infra" / "ca.pem").write_bytes(b"cert chain") |
| 190 | (tmp_path / ".museignore").write_text( |
| 191 | '[force_track]\npaths = ["infra/ca.pem"]\n' |
| 192 | ) |
| 193 | |
| 194 | manifest = walk_workdir(tmp_path) |
| 195 | assert "infra/ca.pem" in manifest |
| 196 | |
| 197 | def test_env_file_included_when_force_tracked( |
| 198 | self, tmp_path: pathlib.Path |
| 199 | ) -> None: |
| 200 | _init_code_repo(tmp_path) |
| 201 | (tmp_path / ".env").write_text("DEV_ONLY=true\n") |
| 202 | (tmp_path / ".museignore").write_text( |
| 203 | '[force_track]\npaths = [".env"]\n' |
| 204 | ) |
| 205 | |
| 206 | manifest = walk_workdir(tmp_path) |
| 207 | assert ".env" in manifest |
| 208 | |
| 209 | def test_non_force_tracked_secret_still_blocked( |
| 210 | self, tmp_path: pathlib.Path |
| 211 | ) -> None: |
| 212 | _init_code_repo(tmp_path) |
| 213 | (tmp_path / "other.key").write_bytes(b"another key") |
| 214 | (tmp_path / "deploy").mkdir() |
| 215 | (tmp_path / "deploy" / "server.key").write_bytes(b"dev key") |
| 216 | (tmp_path / ".museignore").write_text( |
| 217 | '[force_track]\npaths = ["deploy/server.key"]\n' |
| 218 | ) |
| 219 | |
| 220 | manifest = walk_workdir(tmp_path) |
| 221 | assert "deploy/server.key" in manifest |
| 222 | assert "other.key" not in manifest # not whitelisted |
| 223 | |
| 224 | def test_force_track_overrides_user_ignore_patterns( |
| 225 | self, tmp_path: pathlib.Path |
| 226 | ) -> None: |
| 227 | _init_code_repo(tmp_path) |
| 228 | (tmp_path / "config").mkdir() |
| 229 | (tmp_path / "config" / "local.cfg").write_text("debug=true\n") |
| 230 | (tmp_path / ".museignore").write_text( |
| 231 | '[global]\npatterns = ["config/"]\n' |
| 232 | '[force_track]\npaths = ["config/local.cfg"]\n' |
| 233 | ) |
| 234 | |
| 235 | manifest = walk_workdir(tmp_path) |
| 236 | assert "config/local.cfg" in manifest |
| 237 | |
| 238 | def test_force_track_exact_path_only_no_glob_expansion( |
| 239 | self, tmp_path: pathlib.Path |
| 240 | ) -> None: |
| 241 | _init_code_repo(tmp_path) |
| 242 | (tmp_path / "a.key").write_bytes(b"key a") |
| 243 | (tmp_path / "b.key").write_bytes(b"key b") |
| 244 | # Listing "*.key" in force_track should NOT glob-expand — it's an exact path. |
| 245 | (tmp_path / ".museignore").write_text( |
| 246 | '[force_track]\npaths = ["*.key"]\n' |
| 247 | ) |
| 248 | |
| 249 | manifest = walk_workdir(tmp_path) |
| 250 | # "*.key" is not a real filename so neither file matches the exact path. |
| 251 | assert "a.key" not in manifest |
| 252 | assert "b.key" not in manifest |
| 253 | |
| 254 | def test_force_track_nonexistent_path_no_error( |
| 255 | self, tmp_path: pathlib.Path |
| 256 | ) -> None: |
| 257 | _init_code_repo(tmp_path) |
| 258 | (tmp_path / "app.py").write_text("x = 1\n") |
| 259 | (tmp_path / ".museignore").write_text( |
| 260 | '[force_track]\npaths = ["ghost.key"]\n' |
| 261 | ) |
| 262 | |
| 263 | manifest = walk_workdir(tmp_path) |
| 264 | assert "app.py" in manifest # normal files still tracked |
| 265 | |
| 266 | def test_localhost_tls_use_case(self, tmp_path: pathlib.Path) -> None: |
| 267 | """The canonical use-case: dev TLS cert+key committed to the repo.""" |
| 268 | _init_code_repo(tmp_path) |
| 269 | tls = tmp_path / "deploy" / "local-tls" |
| 270 | tls.mkdir(parents=True) |
| 271 | (tls / "localhost.crt").write_bytes(b"cert bytes") |
| 272 | (tls / "localhost.key").write_bytes(b"private key bytes") |
| 273 | (tmp_path / "app.py").write_text("x = 1\n") |
| 274 | (tmp_path / ".museignore").write_text( |
| 275 | "[force_track]\npaths = [\n" |
| 276 | " \"deploy/local-tls/localhost.crt\",\n" |
| 277 | " \"deploy/local-tls/localhost.key\",\n" |
| 278 | "]\n" |
| 279 | ) |
| 280 | |
| 281 | manifest = walk_workdir(tmp_path) |
| 282 | assert "deploy/local-tls/localhost.crt" in manifest |
| 283 | assert "deploy/local-tls/localhost.key" in manifest |
| 284 | assert "app.py" in manifest |
| 285 | |
| 286 | def test_force_track_does_not_affect_unrelated_secrets( |
| 287 | self, tmp_path: pathlib.Path |
| 288 | ) -> None: |
| 289 | _init_code_repo(tmp_path) |
| 290 | (tmp_path / ".env").write_text("SECRET=hunter2\n") |
| 291 | (tmp_path / "deploy").mkdir() |
| 292 | (tmp_path / "deploy" / "localhost.key").write_bytes(b"key") |
| 293 | # Only localhost.key is whitelisted — .env must still be blocked. |
| 294 | (tmp_path / ".museignore").write_text( |
| 295 | '[force_track]\npaths = ["deploy/localhost.key"]\n' |
| 296 | ) |
| 297 | |
| 298 | manifest = walk_workdir(tmp_path) |
| 299 | assert "deploy/localhost.key" in manifest |
| 300 | assert ".env" not in manifest |
File History
1 commit
sha256:2eaa5d95f9d9383498e76947410a26e5a3ba23d182f339910c424cf88fad412b
fix: try fetch/presign before fetch/mpack to avoid Cloudfla…
Sonnet 4.6
patch
6 days ago