personal memory agent
0

Configure Feed

Select the types of activity you want to include in your feed.

solstone / tests / test_release_advisory_policy.py
24 kB 730 lines
1# SPDX-License-Identifier: AGPL-3.0-only 2# Copyright (c) 2026 sol pbc 3 4from __future__ import annotations 5 6import os 7import subprocess 8from collections.abc import Sequence 9from datetime import UTC, datetime 10from pathlib import Path 11 12import pytest 13 14import scripts.check_rust_release_manifest as checker 15import scripts.release_advisory_policy as policy 16 17DB_COMMIT = "a" * 40 18DB_ARCHIVE = "b" * 64 19SNAPSHOT = "advisory-db-1234567890abcdef" 20POLICY_TIME = datetime(2026, 7, 20, 12, 0, tzinfo=UTC) 21FETCH_TIME = datetime(2026, 7, 20, 11, 30, tzinfo=UTC) 22COMMIT_TIME = "2026-07-17T15:52:38Z" 23 24MALFORMED_DB_COMMIT_CASES = ( 25 ("short-39", "a" * 39), 26 ("short-63", "a" * 63), 27 ("long-41", "a" * 41), 28 ("long-65", "a" * 65), 29 ("uppercase", "A" * 40), 30 ("non-hex", "g" * 40), 31 ("empty", ""), 32 ("whitespace", " " + "a" * 40), 33 ("extra-line", "a" * 40 + "\nunexpected\n"), 34) 35MALFORMED_ARCHIVE_DIGEST_CASES = ( 36 ("short", "b" * 63), 37 ("uppercase", "B" * 64), 38 ("non-hex", "g" * 64), 39 ("empty", ""), 40 ("extra-line", "b" * 64 + "\nunexpected"), 41) 42MALFORMED_DB_COMMITS = tuple( 43 pytest.param(value, id=name) for name, value in MALFORMED_DB_COMMIT_CASES 44) 45MALFORMED_ARCHIVE_DIGESTS = tuple( 46 pytest.param(value, id=name) for name, value in MALFORMED_ARCHIVE_DIGEST_CASES 47) 48 49 50class FakeRunner: 51 def __init__( 52 self, 53 snapshot: Path, 54 *, 55 status: str | Sequence[str] = "", 56 fail_check: bool = False, 57 commit_stdout: str = DB_COMMIT + "\n", 58 commit_timestamp: str = COMMIT_TIME + "\n", 59 top_level: Path | None = None, 60 scanned_path: Path | None = None, 61 debug_stderr: str | None = None, 62 ) -> None: 63 self.snapshot = snapshot 64 self.status_outputs = list(status) if not isinstance(status, str) else [status] 65 self.fail_check = fail_check 66 self.commit_stdout = commit_stdout 67 self.commit_timestamp = commit_timestamp 68 self.top_level = top_level 69 self.scanned_path = scanned_path or snapshot 70 self.debug_stderr = debug_stderr 71 self.events: list[str] = [] 72 self.config_bytes: bytes | None = None 73 self.cargo_cwds: list[Path | None] = [] 74 self.cargo_argvs: list[list[str]] = [] 75 76 def __call__(self, argv, **kwargs) -> subprocess.CompletedProcess[str]: 77 command = list(argv) 78 if command[0] == "cargo-deny": 79 if command[-2:] == ["fetch", "db"]: 80 raise AssertionError("cargo-deny fetch subcommand should be gone") 81 if command[-2:] == ["check", "advisories"]: 82 self.events.append("check") 83 self.cargo_cwds.append(kwargs.get("cwd")) 84 self.cargo_argvs.append(command) 85 self.config_bytes = Path( 86 command[command.index("--config") + 1] 87 ).read_bytes() 88 if self.fail_check: 89 return subprocess.CompletedProcess(command, 1, "", "denied") 90 stderr = self.debug_stderr 91 if stderr is None: 92 stderr = ( 93 "2026-07-21 14:40:36 [DEBUG] " 94 f"Opening advisory database at '{self.scanned_path}'\n" 95 ) 96 return subprocess.CompletedProcess(command, 0, "", stderr) 97 if command[:2] == ["git", "-C"]: 98 git_root = Path(command[2]) 99 subcommand = command[3:] 100 if subcommand == ["rev-parse", "--show-toplevel"]: 101 self.events.append("show-toplevel") 102 top_level = self.top_level or git_root 103 return subprocess.CompletedProcess(command, 0, f"{top_level}\n", "") 104 if subcommand[:1] == ["status"]: 105 self.events.append("status") 106 output = self.status_outputs.pop(0) 107 if not self.status_outputs: 108 self.status_outputs.append(output) 109 return subprocess.CompletedProcess(command, 0, output, "") 110 if subcommand[:2] == ["rev-parse", "--verify"]: 111 self.events.append("rev-parse") 112 return subprocess.CompletedProcess(command, 0, self.commit_stdout, "") 113 if subcommand == ["show", "-s", "--format=%cI", "HEAD"]: 114 self.events.append("commit-timestamp") 115 return subprocess.CompletedProcess( 116 command, 0, self.commit_timestamp, "" 117 ) 118 raise AssertionError(f"unexpected command: {command}") 119 120 121def _repo(tmp_path: Path, deny_text: str | None = None) -> Path: 122 root = tmp_path / "repo" 123 (root / "core").mkdir(parents=True, exist_ok=True) 124 (root / "core" / "deny.toml").write_text( 125 deny_text or '[licenses]\nallow = ["MIT"]\n', 126 encoding="utf-8", 127 ) 128 return root 129 130 131def _clock(events: list[str] | None = None, value: datetime = POLICY_TIME): 132 def now() -> datetime: 133 if events is not None: 134 events.append("policy-clock") 135 return value 136 137 return now 138 139 140def _write_snapshot( 141 db_root: Path, 142 *, 143 name: str = SNAPSHOT, 144 advisory_count: int = 1, 145 fetch_time: datetime | None = FETCH_TIME, 146) -> Path: 147 snapshot = db_root / name 148 (snapshot / ".git").mkdir(parents=True) 149 for index in range(advisory_count): 150 advisory = ( 151 snapshot / "crates" / f"probe{index}" / f"RUSTSEC-2026-{index:04d}.md" 152 ) 153 advisory.parent.mkdir(parents=True, exist_ok=True) 154 advisory.write_text( 155 "```toml\n" 156 "[advisory]\n" 157 f'id = "RUSTSEC-2026-{index:04d}"\n' 158 f'package = "probe{index}"\n' 159 'date = "2026-01-01"\n' 160 'url = "https://example.invalid/RUSTSEC-2026-0001"\n' 161 'categories = ["unmaintained"]\n' 162 "keywords = []\n\n" 163 "[versions]\n" 164 "patched = []\n" 165 "```\n", 166 encoding="utf-8", 167 ) 168 if fetch_time is not None: 169 fetch_head = snapshot / ".git" / "FETCH_HEAD" 170 fetch_head.write_text("", encoding="utf-8") 171 timestamp = fetch_time.timestamp() 172 os.utime(fetch_head, (timestamp, timestamp)) 173 return snapshot 174 175 176def _db_root( 177 tmp_path: Path, 178 *, 179 advisory_count: int = 1, 180 fetch_time: datetime | None = FETCH_TIME, 181) -> tuple[Path, Path]: 182 root = tmp_path / "db-root" 183 root.mkdir() 184 (root / "db.lock").write_text("", encoding="utf-8") 185 return root, _write_snapshot( 186 root, 187 advisory_count=advisory_count, 188 fetch_time=fetch_time, 189 ) 190 191 192def _prepare( 193 tmp_path: Path, 194 *, 195 runner: FakeRunner | None = None, 196 db_root: Path | None = None, 197 snapshot: Path | None = None, 198 clock=None, 199 archive: str = DB_ARCHIVE, 200 cleanup_unlink=policy._unlink_path, 201 cleanup_rmdir=policy._remove_dir, 202) -> tuple[policy.PolicyRun, FakeRunner, Path]: 203 repo = _repo(tmp_path) 204 if db_root is None or snapshot is None: 205 db_root, snapshot = _db_root(tmp_path) 206 runner = runner or FakeRunner(snapshot) 207 result = policy.prepare_policy_run( 208 repo, 209 advisory_source_id="internal", 210 db_urls=("ssh://example.test/db.git",), 211 db_root=db_root, 212 runner=runner, 213 temp_path_factory=lambda label: tmp_path / label, 214 clock=clock or _clock(runner.events), 215 archive_hasher=lambda observed: ( 216 runner.events.append(f"archive:{observed.name}") or archive 217 ), 218 cleanup_unlink=cleanup_unlink, 219 cleanup_rmdir=cleanup_rmdir, 220 ) 221 return result, runner, repo 222 223 224def test_materialized_config_and_advisory_check_argv(tmp_path: Path) -> None: 225 result, runner, repo = _prepare(tmp_path) 226 227 assert result.result == "pass" 228 assert result.db_snapshot_basename == SNAPSHOT 229 assert result.advisory_count == 1 230 assert result.advisory_acquired_at == "2026-07-20T11:30:00Z" 231 assert result.db_commit_timestamp == COMMIT_TIME 232 assert runner.config_bytes is not None 233 text = runner.config_bytes.decode("utf-8") 234 assert text.startswith('[licenses]\nallow = ["MIT"]\n\n[advisories]\n') 235 assert 'db-urls = ["ssh://example.test/db.git"]' in text 236 assert "git-fetch-with-cli = true" in text 237 assert 'maximum-db-staleness = "PT24H"' in text 238 assert "24 hours" not in text 239 240 argv = runner.cargo_argvs[0] 241 assert argv == [ 242 "cargo-deny", 243 "--config", 244 argv[2], 245 "--manifest-path", 246 str(repo / "core" / "Cargo.toml"), 247 "-L", 248 "debug", 249 "--locked", 250 "--offline", 251 "check", 252 "advisories", 253 ] 254 assert "fetch" not in runner.events 255 assert runner.events == [ 256 "show-toplevel", 257 "status", 258 "rev-parse", 259 f"archive:{SNAPSHOT}", 260 "commit-timestamp", 261 "check", 262 "status", 263 "policy-clock", 264 ] 265 assert runner.cargo_cwds == [repo] 266 267 268def test_core_deny_toml_advisories_table_fails_loudly(tmp_path: Path) -> None: 269 db_root, _snapshot = _db_root(tmp_path) 270 with pytest.raises(policy.ReleasePolicyError) as exc: 271 policy.prepare_policy_run( 272 _repo(tmp_path, '[advisories]\ndb-path = "x"\n'), 273 advisory_source_id="internal-feed", 274 db_urls=("ssh://example.test/advisory-db.git",), 275 db_root=db_root, 276 runner=FakeRunner(db_root / SNAPSHOT), 277 temp_path_factory=lambda label: tmp_path / label, 278 ) 279 280 assert exc.value.failures[0].error == "core deny.toml already defines advisories" 281 282 283@pytest.mark.parametrize( 284 "source_id,db_urls,error", 285 [ 286 ("", ("ssh://example.test/db.git",), "advisory source id is not a public slug"), 287 ("internal", (), "advisory db source is empty"), 288 ( 289 "internal", 290 ("https://github.com/RustSec/advisory-db",), 291 "advisory db url points at GitHub", 292 ), 293 ( 294 "internal", 295 ("ssh://mirror.github.com/RustSec/db",), 296 "advisory db url points at GitHub", 297 ), 298 ], 299) 300def test_empty_and_github_advisory_sources_are_rejected( 301 tmp_path: Path, 302 source_id: str, 303 db_urls: tuple[str, ...], 304 error: str, 305) -> None: 306 with pytest.raises(policy.ReleasePolicyError) as exc: 307 policy.prepare_policy_run( 308 _repo(tmp_path), 309 advisory_source_id=source_id, 310 db_urls=db_urls, 311 db_root=tmp_path / "db", 312 runner=FakeRunner(tmp_path / "db" / SNAPSHOT), 313 temp_path_factory=lambda label: tmp_path / label, 314 ) 315 316 assert any(failure.error == error for failure in exc.value.failures) 317 318 319def test_snapshot_count_must_be_exactly_one(tmp_path: Path) -> None: 320 db_root = tmp_path / "empty-db" 321 db_root.mkdir() 322 323 with pytest.raises(policy.ReleasePolicyError) as exc: 324 policy.prepare_policy_run( 325 _repo(tmp_path), 326 advisory_source_id="internal", 327 db_urls=("ssh://example.test/db.git",), 328 db_root=db_root, 329 runner=FakeRunner(db_root / SNAPSHOT), 330 temp_path_factory=lambda label: tmp_path / label, 331 ) 332 assert exc.value.failures[0].error == "advisory db snapshot count is invalid" 333 assert exc.value.failures[0].actual == "0" 334 335 db_root = tmp_path / "multi-db" 336 db_root.mkdir() 337 first = _write_snapshot(db_root, name="advisory-db-one") 338 _write_snapshot(db_root, name="advisory-db-two") 339 with pytest.raises(policy.ReleasePolicyError) as exc: 340 policy.prepare_policy_run( 341 _repo(tmp_path), 342 advisory_source_id="internal", 343 db_urls=("ssh://example.test/db.git",), 344 db_root=db_root, 345 runner=FakeRunner(first), 346 temp_path_factory=lambda label: tmp_path / f"multi-{label}", 347 ) 348 assert exc.value.failures[0].error == "advisory db snapshot count is invalid" 349 assert exc.value.failures[0].actual == "2" 350 351 352def test_non_top_level_snapshot_fails_walk_up_check(tmp_path: Path) -> None: 353 db_root, snapshot = _db_root(tmp_path) 354 runner = FakeRunner(snapshot, top_level=tmp_path) 355 356 with pytest.raises(policy.ReleasePolicyError) as exc: 357 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 358 359 assert ( 360 exc.value.failures[0].error 361 == "advisory db snapshot is not an isolated git checkout" 362 ) 363 assert "check" not in runner.events 364 365 366def test_scanned_advisory_db_must_match_measured_snapshot(tmp_path: Path) -> None: 367 db_root, snapshot = _db_root(tmp_path) 368 runner = FakeRunner(snapshot, scanned_path=tmp_path / "other-db") 369 370 with pytest.raises(policy.ReleasePolicyError) as exc: 371 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 372 373 assert ( 374 exc.value.failures[0].error 375 == "cargo-deny scanned a different advisory database" 376 ) 377 378 379def test_missing_debug_line_fails_closed(tmp_path: Path) -> None: 380 db_root, snapshot = _db_root(tmp_path) 381 runner = FakeRunner(snapshot, debug_stderr="debug without database path\n") 382 383 with pytest.raises(policy.ReleasePolicyError) as exc: 384 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 385 386 assert ( 387 exc.value.failures[0].error 388 == "cargo-deny advisory database debug line is missing" 389 ) 390 391 392def test_absent_fetch_head_fails_closed(tmp_path: Path) -> None: 393 db_root, snapshot = _db_root(tmp_path, fetch_time=None) 394 runner = FakeRunner(snapshot) 395 396 with pytest.raises(policy.ReleasePolicyError) as exc: 397 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 398 399 assert exc.value.failures[0].error == "advisory db FETCH_HEAD is missing" 400 401 402def test_stale_fetch_head_fails(tmp_path: Path) -> None: 403 db_root, snapshot = _db_root( 404 tmp_path, 405 fetch_time=datetime(2026, 7, 18, 11, 59, 59, tzinfo=UTC), 406 ) 407 408 with pytest.raises(policy.ReleasePolicyError) as exc: 409 _prepare(tmp_path, db_root=db_root, snapshot=snapshot) 410 411 assert exc.value.failures[0].error == "advisory fetch time is stale" 412 413 414def test_over_age_content_fails(tmp_path: Path) -> None: 415 db_root, snapshot = _db_root(tmp_path) 416 runner = FakeRunner(snapshot, commit_timestamp="2026-07-05T11:59:59Z\n") 417 418 with pytest.raises(policy.ReleasePolicyError) as exc: 419 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 420 421 assert exc.value.failures[0].error == "advisory db content is stale" 422 423 424def test_zero_advisory_count_fails(tmp_path: Path) -> None: 425 db_root, snapshot = _db_root(tmp_path, advisory_count=0) 426 runner = FakeRunner(snapshot) 427 428 with pytest.raises(policy.ReleasePolicyError) as exc: 429 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 430 431 assert exc.value.failures[0].error == "advisory db snapshot contains no advisories" 432 assert "check" not in runner.events 433 434 435def test_post_run_dirty_snapshot_fails(tmp_path: Path) -> None: 436 db_root, snapshot = _db_root(tmp_path) 437 runner = FakeRunner(snapshot, status=("", "?? late-file\n")) 438 439 with pytest.raises(policy.ReleasePolicyError) as exc: 440 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 441 442 assert ( 443 exc.value.failures[0].error 444 == "advisory db snapshot has uncommitted or ignored material" 445 ) 446 assert runner.events.count("status") == 2 447 448 449def test_fresh_fetch_and_four_day_content_pass(tmp_path: Path) -> None: 450 db_root, snapshot = _db_root(tmp_path) 451 runner = FakeRunner(snapshot, commit_timestamp="2026-07-16T12:00:00Z\n") 452 453 result, _runner, _repo_path = _prepare( 454 tmp_path, 455 db_root=db_root, 456 snapshot=snapshot, 457 runner=runner, 458 ) 459 460 assert result.advisory_acquired_at == "2026-07-20T11:30:00Z" 461 assert result.db_commit_timestamp == "2026-07-16T12:00:00Z" 462 463 464def test_prepare_policy_run_accepts_sha256_db_commit(tmp_path: Path) -> None: 465 db_root, snapshot = _db_root(tmp_path) 466 result, _runner, _repo_path = _prepare( 467 tmp_path, 468 db_root=db_root, 469 snapshot=snapshot, 470 runner=FakeRunner(snapshot, commit_stdout="a" * 64 + "\n"), 471 ) 472 473 assert result.db_commit == "a" * 64 474 475 476@pytest.mark.parametrize("commit_stdout", MALFORMED_DB_COMMITS) 477def test_prepare_policy_run_rejects_malformed_db_commit_observation( 478 tmp_path: Path, 479 commit_stdout: str, 480) -> None: 481 db_root, snapshot = _db_root(tmp_path) 482 runner = FakeRunner(snapshot, commit_stdout=commit_stdout) 483 484 with pytest.raises(policy.ReleasePolicyError) as exc: 485 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 486 487 assert exc.value.failures[0].error == "advisory_snapshot.db_commit is invalid" 488 assert exc.value.failures[0].expected == "exactly 40 or 64 lowercase hex characters" 489 assert "check" not in runner.events 490 if commit_stdout.startswith("A"): 491 assert exc.value.failures[0].actual == commit_stdout.removesuffix("\n") 492 493 494@pytest.mark.parametrize("digest", MALFORMED_ARCHIVE_DIGESTS) 495def test_prepare_policy_run_rejects_malformed_archive_digest_observation( 496 tmp_path: Path, 497 digest: str, 498) -> None: 499 db_root, snapshot = _db_root(tmp_path) 500 runner = FakeRunner(snapshot) 501 502 with pytest.raises(policy.ReleasePolicyError) as exc: 503 _prepare( 504 tmp_path, 505 db_root=db_root, 506 snapshot=snapshot, 507 runner=runner, 508 archive=digest, 509 ) 510 511 assert ( 512 exc.value.failures[0].error == "advisory_snapshot.db_archive_sha256 is invalid" 513 ) 514 assert exc.value.failures[0].expected == "exactly 64 lowercase hex characters" 515 assert "check" not in runner.events 516 if digest.startswith("B"): 517 assert exc.value.failures[0].actual == digest 518 519 520def test_policy_run_constructor_accepts_sha256_db_commit() -> None: 521 result = policy.PolicyRun( 522 advisory_source_id="internal", 523 db_snapshot_basename=SNAPSHOT, 524 db_commit="a" * 64, 525 db_archive_sha256=DB_ARCHIVE, 526 advisory_count=1, 527 advisory_acquired_at="2026-07-20T11:30:00Z", 528 db_commit_timestamp=COMMIT_TIME, 529 policy_checked_at="2026-07-20T12:00:00Z", 530 result="pass", 531 ) 532 533 assert result.db_commit == "a" * 64 534 535 536@pytest.mark.parametrize( 537 ("field", "value", "error"), 538 [ 539 *( 540 pytest.param( 541 "db_commit", 542 value, 543 "policy_run.db_commit is invalid", 544 id=f"commit-{name}", 545 ) 546 for name, value in MALFORMED_DB_COMMIT_CASES 547 ), 548 *( 549 pytest.param( 550 "db_archive_sha256", 551 value, 552 "policy_run.db_archive_sha256 is invalid", 553 id=f"archive-{name}", 554 ) 555 for name, value in MALFORMED_ARCHIVE_DIGEST_CASES 556 ), 557 pytest.param( 558 "db_snapshot_basename", 559 "../db", 560 "policy_run.db_snapshot_basename is invalid", 561 id="snapshot-path", 562 ), 563 pytest.param( 564 "db_snapshot_basename", 565 "", 566 "policy_run.db_snapshot_basename is invalid", 567 id="snapshot-empty", 568 ), 569 pytest.param( 570 "advisory_count", 571 0, 572 "policy_run.advisory_count is invalid", 573 id="count-zero", 574 ), 575 pytest.param( 576 "advisory_count", 577 True, 578 "policy_run.advisory_count is invalid", 579 id="count-bool", 580 ), 581 pytest.param( 582 "db_commit_timestamp", 583 "2026-07-19T12:00:00-06:00", 584 "policy_run.db_commit_timestamp is invalid", 585 id="commit-time-not-normalized", 586 ), 587 ], 588) 589def test_policy_run_constructor_rejects_malformed_receipt_identity( 590 field: str, 591 value: object, 592 error: str, 593) -> None: 594 kwargs = { 595 "advisory_source_id": "internal", 596 "db_snapshot_basename": SNAPSHOT, 597 "db_commit": DB_COMMIT, 598 "db_archive_sha256": DB_ARCHIVE, 599 "advisory_count": 1, 600 "advisory_acquired_at": "2026-07-20T11:30:00Z", 601 "db_commit_timestamp": COMMIT_TIME, 602 "policy_checked_at": "2026-07-20T12:00:00Z", 603 "result": "pass", 604 } 605 kwargs[field] = value 606 607 with pytest.raises(policy.ReleasePolicyError) as exc: 608 policy.PolicyRun(**kwargs) 609 610 assert exc.value.failures[0].error == error 611 if isinstance(value, str) and value and value[0].isupper(): 612 assert exc.value.failures[0].actual == value 613 614 615def _cleanup_failure(_path: Path) -> None: 616 raise OSError(5, "cleanup failed", "/private/tmp/release-advisory-secret") 617 618 619@pytest.mark.parametrize("primary_failure", (False, True), ids=("success", "primary")) 620@pytest.mark.parametrize( 621 ("cleanup_name", "cleanup_kwargs"), 622 [ 623 ("unlink", {"cleanup_unlink": _cleanup_failure}), 624 ("rmdir", {"cleanup_rmdir": _cleanup_failure}), 625 ], 626) 627def test_cleanup_failures_surface_without_masking_primary_errors( 628 tmp_path: Path, 629 cleanup_name: str, 630 cleanup_kwargs: dict, 631 primary_failure: bool, 632) -> None: 633 db_root, snapshot = _db_root(tmp_path) 634 runner = FakeRunner(snapshot, status="?? dirty\n" if primary_failure else "") 635 636 with pytest.raises(policy.ReleasePolicyError) as exc: 637 _prepare( 638 tmp_path, 639 db_root=db_root, 640 snapshot=snapshot, 641 runner=runner, 642 **cleanup_kwargs, 643 ) 644 645 errors = [failure.error for failure in exc.value.failures] 646 if primary_failure: 647 assert "advisory db snapshot has uncommitted or ignored material" in errors 648 else: 649 assert errors == [ 650 "release advisory cleanup failed during materialized config removal" 651 ] 652 assert any(error.startswith("release advisory cleanup failed") for error in errors) 653 assert ( 654 checker.validate_public_evidence_text( 655 "release advisory cleanup", str(exc.value) 656 ) 657 == [] 658 ) 659 660 661def _caller_db_inventory(root: Path) -> list[tuple[str, str, bytes]]: 662 items: list[tuple[str, str, bytes]] = [] 663 for path in root.rglob("*"): 664 relative = path.relative_to(root).as_posix() 665 if path.is_dir(): 666 items.append((relative, "dir", b"")) 667 elif path.is_file(): 668 items.append((relative, "file", path.read_bytes())) 669 return sorted(items) 670 671 672@pytest.mark.parametrize( 673 ("primary_failure", "cleanup_kwargs"), 674 [ 675 (False, {}), 676 (True, {}), 677 (False, {"cleanup_unlink": _cleanup_failure}), 678 ], 679 ids=("success", "primary-failure", "cleanup-failure"), 680) 681def test_caller_owned_db_root_is_preserved( 682 tmp_path: Path, 683 primary_failure: bool, 684 cleanup_kwargs: dict, 685) -> None: 686 db_root, snapshot = _db_root(tmp_path) 687 before = _caller_db_inventory(db_root) 688 runner = FakeRunner(snapshot, status="?? dirty\n" if primary_failure else "") 689 690 if primary_failure or cleanup_kwargs: 691 with pytest.raises(policy.ReleasePolicyError): 692 _prepare( 693 tmp_path, 694 db_root=db_root, 695 snapshot=snapshot, 696 runner=runner, 697 **cleanup_kwargs, 698 ) 699 else: 700 _prepare(tmp_path, db_root=db_root, snapshot=snapshot, runner=runner) 701 702 assert _caller_db_inventory(db_root) == before 703 704 705def test_policy_temps_are_cleaned_without_removing_caller_db(tmp_path: Path) -> None: 706 db_root, snapshot = _db_root(tmp_path) 707 temp_root = tmp_path / "policy-temp" 708 _prepare( 709 tmp_path, 710 db_root=db_root, 711 snapshot=snapshot, 712 runner=FakeRunner(snapshot), 713 clock=_clock([]), 714 ) 715 assert db_root.is_dir() 716 assert not (tmp_path / "advisory-policy").exists() 717 718 with pytest.raises(policy.ReleasePolicyError): 719 policy.prepare_policy_run( 720 _repo(tmp_path), 721 advisory_source_id="internal", 722 db_urls=("ssh://example.test/db.git",), 723 db_root=db_root, 724 runner=FakeRunner(snapshot, status="?? dirty\n"), 725 temp_path_factory=lambda _label: temp_root, 726 clock=_clock([]), 727 archive_hasher=lambda _db: DB_ARCHIVE, 728 ) 729 assert db_root.is_dir() 730 assert not temp_root.exists()