"""Tests for ``muse format-patch`` — export commits as .patch files. Coverage tiers: - Unit: _make_patch_filename, _format_patch_content, _resolve_commit_range - Integration: single commit → one .patch file; range → multiple files; -N form; --stdout mode; --json manifest; numbered naming (0001-, 0002-); provenance headers present; empty/no-change commits; --output-dir - End-to-end: full CLI via CliRunner - Security: malicious subject sanitized in filename; output-dir traversal rejected - Stress: 10-commit range produces 10 patch files """ from __future__ import annotations import datetime import hashlib import json import os import pathlib import pytest from tests.cli_test_helper import CliRunner from muse.core.object_store import write_object from muse.core.snapshot import compute_commit_id, compute_snapshot_id from muse.core.store import CommitRecord, SnapshotRecord, write_commit, write_snapshot from muse.core._types import Manifest runner = CliRunner() _REPO_ID = "format-patch-test" _counter = 0 # --------------------------------------------------------------------------- # Helpers # --------------------------------------------------------------------------- def _sha(data: bytes) -> str: return hashlib.sha256(data).hexdigest() def _init_repo(path: pathlib.Path) -> pathlib.Path: muse = path / ".muse" for d in ("commits", "snapshots", "objects", "refs/heads", "code"): (muse / d).mkdir(parents=True, exist_ok=True) (muse / "HEAD").write_text("ref: refs/heads/main", encoding="utf-8") (muse / "repo.json").write_text( json.dumps({"repo_id": _REPO_ID, "domain": "code"}), encoding="utf-8" ) return path def _env(repo: pathlib.Path) -> dict[str, str]: return {"MUSE_REPO_ROOT": str(repo)} def _commit_files( root: pathlib.Path, files: dict[str, bytes], branch: str = "main", message: str | None = None, ) -> str: global _counter _counter += 1 manifest: Manifest = {} for rel_path, content in files.items(): obj_id = _sha(content) write_object(root, obj_id, content) manifest[rel_path] = obj_id abs_path = root / rel_path abs_path.parent.mkdir(parents=True, exist_ok=True) abs_path.write_bytes(content) snap_id = compute_snapshot_id(manifest) write_snapshot(root, SnapshotRecord(snapshot_id=snap_id, manifest=manifest)) committed_at = datetime.datetime.now(datetime.timezone.utc) ref_path = root / ".muse" / "refs" / "heads" / branch parent_id = ref_path.read_text(encoding="utf-8").strip() if ref_path.exists() else None parents = [parent_id] if parent_id else [] msg = message or f"commit {_counter}" commit_id = compute_commit_id( parents, snap_id, msg, committed_at.isoformat() ) write_commit( root, CommitRecord( commit_id=commit_id, repo_id=_REPO_ID, branch=branch, snapshot_id=snap_id, message=msg, committed_at=committed_at, parent_commit_id=parent_id, ), ) ref_path.write_text(commit_id, encoding="utf-8") return commit_id def _invoke(repo: pathlib.Path, *args: str): from muse.cli.app import main as cli return runner.invoke(cli, ["format-patch", *args], env=_env(repo)) # --------------------------------------------------------------------------- # Unit — _make_patch_filename # --------------------------------------------------------------------------- def test_make_patch_filename_numbered(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _make_patch_filename name = _make_patch_filename(1, "feat: add login") assert name.startswith("0001-") assert name.endswith(".patch") def test_make_patch_filename_high_number(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _make_patch_filename name = _make_patch_filename(42, "fix: typo") assert name.startswith("0042-") def test_make_patch_filename_sanitizes_subject(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _make_patch_filename # Slashes should be replaced so the filename is safe name = _make_patch_filename(1, "feat: add ../etc/passwd injection") assert "/" not in name assert ".." not in name def test_make_patch_filename_strips_control_chars(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _make_patch_filename name = _make_patch_filename(1, "bad\x00name\x1b[31m") # No control characters in the filename assert all(ord(c) >= 32 for c in name) def test_make_patch_filename_max_length(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _make_patch_filename long_subject = "a" * 200 name = _make_patch_filename(1, long_subject) assert len(name) <= 80 # reasonable max for filesystem compatibility # --------------------------------------------------------------------------- # Unit — _format_patch_content # --------------------------------------------------------------------------- def test_format_patch_content_has_required_headers(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _format_patch_content root = _init_repo(tmp_path) write_object(root, _sha(b"x = 1\n"), b"x = 1\n") write_object(root, _sha(b"x = 2\n"), b"x = 2\n") base = {"a.py": _sha(b"x = 1\n")} target = {"a.py": _sha(b"x = 2\n")} committed_at = datetime.datetime.now(datetime.timezone.utc) content = _format_patch_content( root=root, commit_id="abc" * 21 + "d", subject="feat: change x", committed_at=committed_at, base_manifest=base, target_manifest=target, ) assert "Subject:" in content assert "X-Muse-Commit-ID:" in content assert "diff --muse" in content or "---" in content def test_format_patch_content_includes_diff_lines(tmp_path: pathlib.Path) -> None: from muse.cli.commands.format_patch import _format_patch_content root = _init_repo(tmp_path) write_object(root, _sha(b"x = 1\n"), b"x = 1\n") write_object(root, _sha(b"x = 2\n"), b"x = 2\n") base = {"a.py": _sha(b"x = 1\n")} target = {"a.py": _sha(b"x = 2\n")} committed_at = datetime.datetime.now(datetime.timezone.utc) content = _format_patch_content( root=root, commit_id="abc" * 21 + "d", subject="test", committed_at=committed_at, base_manifest=base, target_manifest=target, ) assert "+x = 2" in content assert "-x = 1" in content # --------------------------------------------------------------------------- # Integration — single commit output # --------------------------------------------------------------------------- def test_format_patch_single_commit_creates_file(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files(root, {"a.py": b"x = 2\n"}, message="feat: bump x") out_dir = tmp_path / "patches" out_dir.mkdir() result = _invoke(root, "HEAD", "--output-dir", str(out_dir)) assert result.exit_code == 0 patch_files = list(out_dir.glob("*.patch")) assert len(patch_files) == 1 def test_format_patch_single_commit_numbered_filename(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files(root, {"a.py": b"x = 2\n"}, message="feat: bump x") out_dir = tmp_path / "patches" out_dir.mkdir() _invoke(root, "HEAD", "--output-dir", str(out_dir)) patch_files = list(out_dir.glob("*.patch")) assert patch_files[0].name.startswith("0001-") def test_format_patch_file_has_muse_commit_id_header(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") commit_id = _commit_files(root, {"a.py": b"x = 2\n"}, message="feat: change") out_dir = tmp_path / "patches" out_dir.mkdir() _invoke(root, "HEAD", "--output-dir", str(out_dir)) patch_file = next(out_dir.glob("*.patch")) content = patch_file.read_text() assert "X-Muse-Commit-ID:" in content assert commit_id[:8] in content or commit_id in content def test_format_patch_file_has_diff_content(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files(root, {"a.py": b"x = 2\n"}, message="change x") out_dir = tmp_path / "patches" out_dir.mkdir() _invoke(root, "HEAD", "--output-dir", str(out_dir)) content = next(out_dir.glob("*.patch")).read_text() assert "+x = 2" in content assert "-x = 1" in content # --------------------------------------------------------------------------- # Integration — --stdout # --------------------------------------------------------------------------- def test_format_patch_stdout_contains_patch_content(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files(root, {"a.py": b"x = 2\n"}, message="feat: change") result = _invoke(root, "HEAD", "--stdout") assert result.exit_code == 0 assert "Subject:" in result.stdout assert "+x = 2" in result.stdout # --------------------------------------------------------------------------- # Integration — --json # --------------------------------------------------------------------------- def test_format_patch_json_schema(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files(root, {"a.py": b"x = 2\n"}, message="feat: change") out_dir = tmp_path / "patches" out_dir.mkdir() result = _invoke(root, "HEAD", "--output-dir", str(out_dir), "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert "patches" in data assert len(data["patches"]) >= 1 patch = data["patches"][0] assert "file" in patch assert "commit_id" in patch assert "subject" in patch # --------------------------------------------------------------------------- # Integration — empty repo error # --------------------------------------------------------------------------- def test_format_patch_empty_repo_exits_nonzero(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) result = _invoke(root, "HEAD") assert result.exit_code != 0 # --------------------------------------------------------------------------- # Integration — initial commit (no parent) # --------------------------------------------------------------------------- def test_format_patch_initial_commit_no_parent(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial commit") out_dir = tmp_path / "patches" out_dir.mkdir() result = _invoke(root, "HEAD", "--output-dir", str(out_dir)) assert result.exit_code == 0 patch_files = list(out_dir.glob("*.patch")) assert len(patch_files) == 1 # Initial commit diff: all files are additions content = patch_files[0].read_text() assert "+x = 1" in content # --------------------------------------------------------------------------- # Security — filename injection # --------------------------------------------------------------------------- def test_format_patch_malicious_subject_safe_filename(tmp_path: pathlib.Path) -> None: root = _init_repo(tmp_path) _commit_files(root, {"a.py": b"x = 1\n"}, message="initial") _commit_files( root, {"a.py": b"x = 2\n"}, message="feat: ../../../etc/malicious\x00\x1b[31m", ) out_dir = tmp_path / "patches" out_dir.mkdir() result = _invoke(root, "HEAD", "--output-dir", str(out_dir)) # Should succeed and produce a safe filename assert result.exit_code == 0 for f in out_dir.glob("*.patch"): assert "/" not in f.name assert ".." not in f.name assert all(ord(c) >= 32 for c in f.name) # --------------------------------------------------------------------------- # Stress — 10 commits # --------------------------------------------------------------------------- def test_format_patch_10_commits_range(tmp_path: pathlib.Path) -> None: """HEAD~9..HEAD produces 9 patch files (last 9 commits, each distinct).""" root = _init_repo(tmp_path) # Create 10 commits for i in range(10): _commit_files(root, {"a.py": f"x = {i}\n".encode()}, message=f"commit {i}") out_dir = tmp_path / "patches" out_dir.mkdir() # Use -N form for last 9 commits result = _invoke(root, "-9", "--output-dir", str(out_dir), "--json") assert result.exit_code == 0 data = json.loads(result.stdout) assert len(data["patches"]) == 9 # Each file should be uniquely numbered filenames = [p["file"] for p in data["patches"]] assert len(set(filenames)) == 9