patchloom 0.34.0

Structured file editing library and CLI for AI agents: parser-backed JSON/YAML/TOML edits, AST-aware code operations, multi-file batching, markdown operations, and MCP server
Documentation
#!/usr/bin/env python3
"""Tests for scripts/apply-release-notes.sh."""

from __future__ import annotations

import os
import stat
import subprocess
import tempfile
import unittest
from pathlib import Path

SCRIPT = Path(__file__).resolve().parent / "apply-release-notes.sh"


def run_script(
    env: dict[str, str], cwd: Path | None = None
) -> subprocess.CompletedProcess[str]:
    merged = os.environ.copy()
    merged.pop("GH_TOKEN", None)
    merged.pop("GITHUB_TOKEN", None)
    merged.pop("RELEASE_NOTES", None)
    merged.pop("RELEASE_NOTES_TAG", None)
    merged.update(env)
    return subprocess.run(
        ["bash", str(SCRIPT)],
        capture_output=True,
        text=True,
        env=merged,
        cwd=cwd,
        check=False,
    )


class ApplyReleaseNotesTests(unittest.TestCase):
    def test_rejects_bad_tag(self) -> None:
        r = run_script({"TAG": "canact-v0.1.2", "GH_REPO": "patchloom/patchloom"})
        self.assertNotEqual(r.returncode, 0)
        self.assertIn("vX.Y.Z", r.stderr)

    def test_patchloom_prefixed_tag_dry_run(self) -> None:
        r = run_script(
            {
                "TAG": "patchloom-v0.33.0",
                "GH_REPO": "patchloom/patchloom",
                "DRY_RUN": "1",
            }
        )
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertIn("leaving auto notes", r.stdout)

    def test_missing_source_is_noop(self) -> None:
        r = run_script(
            {"TAG": "v0.1.2", "GH_REPO": "canact/canact", "DRY_RUN": "1"}
        )
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertIn("leaving auto notes", r.stdout)

    def test_notes_file_dry_run(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            notes = Path(td) / "RELEASE_NOTES.md"
            notes.write_text("canact 0.1.2 notes\n", encoding="utf-8")
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "DRY_RUN": "1",
                    "NOTES_FILE": str(notes),
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr)
            self.assertIn("DRY_RUN: would apply file:", r.stdout)
            self.assertIn("BYTES: 19", r.stdout)
            self.assertNotIn("would delete branch", r.stdout)

    def test_empty_file_is_noop(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            notes = Path(td) / "RELEASE_NOTES.md"
            notes.write_text("", encoding="utf-8")
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "DRY_RUN": "1",
                    "NOTES_FILE": str(notes),
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr)
            self.assertIn("leaving auto notes", r.stdout)

    def test_variable_requires_matching_tag(self) -> None:
        r = run_script(
            {
                "TAG": "v0.1.2",
                "GH_REPO": "canact/canact",
                "DRY_RUN": "1",
                "RELEASE_NOTES": "stale notes",
                "RELEASE_NOTES_TAG": "v0.1.1",
            }
        )
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertIn("leaving auto notes", r.stdout)

    def test_variable_applies_when_tag_matches(self) -> None:
        r = run_script(
            {
                "TAG": "v0.1.2",
                "GH_REPO": "canact/canact",
                "DRY_RUN": "1",
                "RELEASE_NOTES": "from var",
                "RELEASE_NOTES_TAG": "0.1.2",
            }
        )
        self.assertEqual(r.returncode, 0, r.stderr)
        self.assertIn("Actions variable", r.stdout)
        self.assertIn("DRY_RUN: would apply variable to v0.1.2", r.stdout)
        self.assertNotIn("would delete branch", r.stdout)

    def test_branch_fetch_dry_run_deletes_branch(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            fake_bin = Path(td) / "bin"
            fake_bin.mkdir()
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                "if [ \"$1\" = api ] && [ \"$2\" != -X ]; then\n"
                "  printf '%s\\n' 'from branch'\n"
                "  exit 0\n"
                "fi\n"
                "exit 1\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "DRY_RUN": "1",
                    "GH_TOKEN": "test",
                    "PATH": env_path,
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
            self.assertIn("release-note-0.1.2", r.stdout)
            self.assertIn("DRY_RUN: would delete branch release-note-0.1.2", r.stdout)

    def test_release_view_fail_skips_edit(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            td_path = Path(td)
            notes = td_path / "RELEASE_NOTES.md"
            notes.write_text("curated notes\n", encoding="utf-8")
            fake_bin = td_path / "bin"
            fake_bin.mkdir()
            log = td_path / "gh.log"
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                f"printf '%s\\n' \"$*\" >> '{log}'\n"
                "if [ \"$1\" = release ] && [ \"$2\" = view ]; then\n"
                "  exit 1\n"
                "fi\n"
                "exit 0\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "NOTES_FILE": str(notes),
                    "GH_TOKEN": "test",
                    "PATH": env_path,
                }
            )
            self.assertEqual(r.returncode, 1, r.stderr + r.stdout)
            self.assertIn("does not exist", r.stderr)
            logged = log.read_text(encoding="utf-8") if log.exists() else ""
            self.assertIn("release view", logged)
            self.assertNotIn("release edit", logged)

    def test_delete_fail_warns_and_exits_zero(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            td_path = Path(td)
            fake_bin = td_path / "bin"
            fake_bin.mkdir()
            log = td_path / "gh.log"
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                f"printf '%s\\n' \"$*\" >> '{log}'\n"
                "if [ \"$1\" = api ] && [ \"$2\" != -X ]; then\n"
                "  printf '%s\\n' 'from branch'\n"
                "  exit 0\n"
                "fi\n"
                "if [ \"$1\" = release ] && [ \"$2\" = view ]; then\n"
                "  exit 0\n"
                "fi\n"
                "if [ \"$1\" = release ] && [ \"$2\" = edit ]; then\n"
                "  exit 0\n"
                "fi\n"
                "if [ \"$1\" = api ] && [ \"$2\" = -X ] && [ \"$3\" = DELETE ]; then\n"
                "  exit 1\n"
                "fi\n"
                "exit 1\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "GH_TOKEN": "test",
                    "PATH": env_path,
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
            combined = f"{r.stderr}\n{r.stdout}"
            self.assertIn("WARN", combined)
            self.assertIn("could not delete", combined)
            logged = log.read_text(encoding="utf-8") if log.exists() else ""
            self.assertIn("release view", logged)
            self.assertIn("release edit", logged)
            self.assertIn("DELETE", logged)

    def test_failed_branch_fetch_applies_matching_variable(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            td_path = Path(td)
            fake_bin = td_path / "bin"
            fake_bin.mkdir()
            log = td_path / "gh.log"
            notes_seen = td_path / "notes_seen.txt"
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                f"printf '%s\\n' \"$*\" >> '{log}'\n"
                "if [ \"$1\" = api ] && [ \"$2\" != -X ]; then\n"
                "  printf '%s\\n' "
                "'{\"message\":\"Not Found\","
                "\"documentation_url\":\"https://docs.github.com\"}'\n"
                "  exit 1\n"
                "fi\n"
                "if [ \"$1\" = release ] && [ \"$2\" = view ]; then\n"
                "  exit 0\n"
                "fi\n"
                "if [ \"$1\" = release ] && [ \"$2\" = edit ]; then\n"
                "  while [ $# -gt 0 ]; do\n"
                "    if [ \"$1\" = --notes-file ]; then\n"
                f"      cat \"$2\" > '{notes_seen}'\n"
                "      break\n"
                "    fi\n"
                "    shift\n"
                "  done\n"
                "  exit 0\n"
                "fi\n"
                "exit 0\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "GH_TOKEN": "test",
                    "RELEASE_NOTES": "from var after failed fetch",
                    "RELEASE_NOTES_TAG": "v0.1.2",
                    "PATH": env_path,
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
            self.assertIn("variable", r.stdout)
            self.assertTrue(notes_seen.is_file(), r.stderr + r.stdout)
            seen = notes_seen.read_text(encoding="utf-8")
            self.assertIn("from var after failed fetch", seen)
            self.assertNotIn("Not Found", seen)
            self.assertNotIn("documentation_url", seen)
            logged = log.read_text(encoding="utf-8") if log.exists() else ""
            self.assertIn("release edit", logged)

    def test_empty_branch_fetch_uses_variable_without_delete(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            fake_bin = Path(td) / "bin"
            fake_bin.mkdir()
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                "if [ \"$1\" = api ] && [ \"$2\" != -X ]; then\n"
                "  exit 0\n"
                "fi\n"
                "exit 1\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "DRY_RUN": "1",
                    "GH_TOKEN": "test",
                    "RELEASE_NOTES": "from var after empty fetch",
                    "RELEASE_NOTES_TAG": "v0.1.2",
                    "PATH": env_path,
                }
            )
            self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
            self.assertIn("empty notes", r.stdout)
            self.assertIn("would apply variable", r.stdout)
            self.assertNotIn("would delete branch", r.stdout)

    def test_empty_branch_fetch_uses_legacy_without_delete(self) -> None:
        with tempfile.TemporaryDirectory() as td:
            td_path = Path(td)
            (td_path / "RELEASE_NOTES.md").write_text("legacy notes\n", encoding="utf-8")
            fake_bin = td_path / "bin"
            fake_bin.mkdir()
            gh = fake_bin / "gh"
            gh.write_text(
                "#!/bin/bash\n"
                "if [ \"$1\" = api ] && [ \"$2\" != -X ]; then\n"
                "  exit 0\n"
                "fi\n"
                "exit 1\n",
                encoding="utf-8",
            )
            gh.chmod(gh.stat().st_mode | stat.S_IEXEC)
            env_path = f"{fake_bin}{os.pathsep}{os.environ.get('PATH', '')}"
            r = run_script(
                {
                    "TAG": "v0.1.2",
                    "GH_REPO": "canact/canact",
                    "DRY_RUN": "1",
                    "GH_TOKEN": "test",
                    "PATH": env_path,
                },
                cwd=td_path,
            )
            self.assertEqual(r.returncode, 0, r.stderr + r.stdout)
            self.assertIn("empty notes", r.stdout)
            self.assertIn("legacy RELEASE_NOTES.md", r.stdout)
            self.assertNotIn("would delete branch", r.stdout)


if __name__ == "__main__":
    unittest.main()