CtrlK
BlogDocsLog inGet started
Tessl Logo

jbaruch/coding-policy

General-purpose coding policy for Baruch's AI agents

73

Quality

91%

Does it follow best practices?

Run evals on this skill

Adds up to 20 points to the overall score

View guide

SecuritybySnyk

Low

Low-risk findings worth noting

Overview
Quality
Evals
Security
Files

test_check_changelog_placement.pyskills/release/tests/

#!/usr/bin/env python3
"""A CHANGELOG entry the publish step cannot stamp is refused on the PR.

The hazard is a branch cut before an intervening publish: its `### ` block
merges BELOW a version heading added since, the stamp step correctly finds
nothing un-headed, and the work ships filed under an already-published version
(#452). These cases pin the check that catches it while a rebase is still the
fix.
"""

import os as _os
import sys as _sys

_ROOT = _os.path.dirname(_os.path.dirname(_os.path.abspath(__file__)))
if _ROOT not in _sys.path:
    _sys.path.insert(0, _ROOT)

import importlib.util
import json
import subprocess
import tempfile
import unittest
from pathlib import Path

_SCRIPT = _os.path.join(_ROOT, "check-changelog-placement.py")
_SPEC = importlib.util.spec_from_file_location("check_changelog_placement", _SCRIPT)
# Narrowed for the type checker the same way its sibling suite narrows, since
# `spec_from_file_location` is Optional at the type level.
assert _SPEC and _SPEC.loader, f"cannot load check-changelog-placement.py at {_SCRIPT}"
check = importlib.util.module_from_spec(_SPEC)
_SPEC.loader.exec_module(check)

HEADED = """# Changelog

## 0.3.9 — 2026-01-02

### Added

- **A published entry.** Already stamped.
"""

STAMPABLE = """# Changelog

### Added

- **A new entry.** Not yet stamped.

## 0.3.9 — 2026-01-02

### Added

- **A published entry.** Already stamped.
"""

MISFILED = """# Changelog

## 0.3.9 — 2026-01-02

### Added

- **A new entry.** Not yet stamped.

### Added

- **A published entry.** Already stamped.
"""


VERDICT_EXIT = {"pass": 0, "nothing_to_measure": 0, "misfiled": 1, "error": 2}


def run_script(cmd, **kwargs):
    """Run the checker and hold it to its stdout contract on every outcome.

    Exactly one JSON object on stdout, whose verdict agrees with the exit
    code; human diagnostics stay on stderr.
    """
    proc = subprocess.run(cmd, **kwargs)
    payload = json.loads(proc.stdout)
    assert set(payload) == {"verdict", "base", "new_items", "reason"}, payload
    assert VERDICT_EXIT[payload["verdict"]] == proc.returncode, (payload, proc.returncode)
    proc.payload = payload
    return proc


TWO_VERSIONS = """# Changelog

## 0.3.9 — 2026-01-02

### Added

- **A second entry.** Published with it.

## 0.3.8 — 2026-01-01

### Added

- **An older entry.** Published first.
"""

# The #581 shape: two releases' items under one heading.
SHARED_HEADING = """# Changelog

## 0.3.9 — 2026-01-02

### Fixed

- **The later release's entry.** Published as 0.3.10.

- **This release's entry.** Published as 0.3.9.
"""

REPAIRED = """# Changelog

## 0.3.10 — 2026-01-02

### Fixed

- **The later release's entry.** Published as 0.3.10.

## 0.3.9 — 2026-01-02

### Fixed

- **This release's entry.** Published as 0.3.9.
"""


class ParkedIdentityTest(unittest.TestCase):
    def test_an_entry_above_the_first_heading_is_not_parked(self):
        # The published entry below the heading is parked, as it should be;
        # the new one above it is not, which is what makes it stampable.
        parked = check.parked(STAMPABLE)
        self.assertEqual(len(parked), 1)
        self.assertIn("A published entry.", parked[0])
        self.assertNotIn("A new entry.", "\n".join(parked))

    def test_an_entry_under_a_version_heading_is_parked(self):
        self.assertEqual(len(check.parked(MISFILED)), 2)

    def test_a_changelog_with_no_version_heading_yet_parks_nothing(self):
        self.assertEqual(check.parked("# Changelog\n\n### Added\n"), [])

    def test_a_moved_block_is_known_to_the_base(self):
        self.assertEqual(check.newly_parked(HEADED, HEADED), [])

    def test_a_new_block_parked_under_a_heading_is_flagged(self):
        flagged = check.newly_parked(HEADED, MISFILED)
        self.assertEqual(len(flagged), 1)
        self.assertIn("A new entry.", flagged[0])

    def test_a_swap_across_headings_that_keeps_the_count_is_still_flagged(self):
        # The count rule this replaced was satisfied by adding one misfiled
        # item while dropping another parked one. Dropped under one heading
        # and added under another is no in-place edit: nothing under the new
        # item's heading went away for it to pair with.
        swapped = TWO_VERSIONS.replace(
            "- **A second entry.** Published with it.\n",
            "- **A second entry.** Published with it.\n\n"
            "- **A new entry.** Not yet stamped.\n").replace(
            "\n- **An older entry.** Published first.\n", "")
        self.assertEqual(len(check.parked(swapped)), len(check.parked(TWO_VERSIONS)))
        self.assertEqual(len(check.newly_parked(TWO_VERSIONS, swapped)), 1)

    def test_a_heading_only_repair_parks_nothing_new(self):
        # #581: 0.3.291 published with no heading, its entry filed under
        # 0.3.290. The repair adds `## 0.3.291` and a `### Fixed` above
        # content the base already carries; only structure changes.
        self.assertEqual(check.newly_parked(SHARED_HEADING, REPAIRED), [])

    def test_a_declared_in_place_edit_is_not_new(self):
        # #503: rewording a published entry replaces the item's text under
        # the same heading, and a `Changelog-Edit:` trailer declares it.
        edited = HEADED.replace("Already stamped.", "Reworded in place.")
        self.assertEqual(check.newly_parked(HEADED, edited, {"0.3.9"}), [])

    def test_an_undeclared_same_heading_replacement_is_flagged(self):
        # Deleting a published item and adding unrelated work under the same
        # heading reads exactly like a reword. Without the declaration it is
        # new content parked where the stamp cannot reach.
        replaced = HEADED.replace(
            "- **A published entry.** Already stamped.\n",
            "- **A new entry.** Not yet stamped.\n")
        flagged = check.newly_parked(HEADED, replaced)
        self.assertEqual(len(flagged), 1)
        self.assertIn("A new entry.", flagged[0])

    def test_a_declaration_for_another_version_does_not_cover_an_edit(self):
        edited = HEADED.replace("Already stamped.", "Reworded in place.")
        self.assertEqual(len(check.newly_parked(HEADED, edited, {"0.3.8"})), 1)

    def test_an_edit_answers_one_removed_item_only(self):
        # One rewritten item pairs with one removed item; a second new item
        # under the same heading has nothing left to pair with.
        edited = HEADED.replace(
            "- **A published entry.** Already stamped.\n",
            "- **A published entry.** Reworded in place.\n\n"
            "- **A new entry.** Not yet stamped.\n")
        flagged = check.newly_parked(HEADED, edited, {"0.3.9"})
        self.assertEqual(len(flagged), 1)

    def test_an_edit_moved_to_another_heading_is_flagged(self):
        # An edit pairs only under the heading the base item sat under.
        moved = TWO_VERSIONS.replace(
            "- **A second entry.** Published with it.\n\n", "").replace(
            "- **An older entry.** Published first.\n",
            "- **An older entry.** Published first.\n\n"
            "- **A second entry.** Reworded and moved.\n")
        self.assertEqual(
            len(check.newly_parked(TWO_VERSIONS, moved, {"0.3.8", "0.3.9"})), 1)

    def test_a_column_zero_paragraph_is_its_own_item(self):
        # Older entries are prose paragraphs, not bullets.
        text = ("# Changelog\n\n## 0.3.9 — 2026-01-02\n\n### Added\n\n"
                "First paragraph.\n\nSecond paragraph.\n")
        self.assertEqual(check.parked(text), ["First paragraph.", "Second paragraph."])


class _RepoCase(unittest.TestCase):
    """A real repository whose `main` carries HEADED."""

    def setUp(self):
        temporary = tempfile.TemporaryDirectory()
        self.addCleanup(temporary.cleanup)
        self.root = Path(temporary.name)
        self.changelog = self.root / "CHANGELOG.md"
        self.git("init", "-q", "-b", "main")
        self.git("config", "user.email", "test@example.com")
        self.git("config", "user.name", "Test")
        self.changelog.write_text(HEADED, encoding="utf-8")
        self.git("add", "CHANGELOG.md")
        self.git("commit", "-q", "-m", "base")

    def git(self, *args):
        subprocess.run(["git", *args], cwd=self.root, check=True,
                       capture_output=True, text=True)

    def run_check(self):
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--base", "main"],
            cwd=self.root, capture_output=True, text=True)
        return proc.returncode, proc.stderr

    def rebase_on(self, text):
        """Replace the base commit's changelog with `text`."""
        self.changelog.write_text(text, encoding="utf-8")
        self.git("commit", "-q", "-am", "base changelog")

    def commit_changelog(self, text, message="entry"):
        self.git("checkout", "-q", "-b", "work")
        self.changelog.write_text(text, encoding="utf-8")
        self.git("add", "CHANGELOG.md")
        self.git("commit", "-q", "-m", message)


class RepositoryTest(_RepoCase):
    """End to end over a real repository, so the diff half is exercised too."""

    def test_a_stampable_entry_passes(self):
        self.commit_changelog(STAMPABLE)
        code, err = self.run_check()
        self.assertEqual(code, 0, err)

    def test_an_entry_merged_under_a_heading_is_refused(self):
        self.commit_changelog(MISFILED)
        code, err = self.run_check()
        self.assertEqual(code, 1, err)
        self.assertIn("are new since", err)
        self.assertIn("Rebase onto main", err)

    def test_a_swap_that_keeps_the_count_is_refused(self):
        # Adds a misfiled item under one heading and drops a parked one from
        # another: the net count is unchanged, and identity still catches it.
        self.rebase_on(TWO_VERSIONS)
        swapped = TWO_VERSIONS.replace(
            "- **A second entry.** Published with it.\n",
            "- **A second entry.** Published with it.\n\n"
            "- **A new entry.** Not yet stamped.\n").replace(
            "\n- **An older entry.** Published first.\n", "")
        self.commit_changelog(swapped)
        code, err = self.run_check()
        self.assertEqual(code, 1, err)
        self.assertIn("are new since", err)

    def test_a_heading_only_repair_is_allowed(self):
        # #581: the repair inserts `## 0.3.10` and `### Fixed` above an item
        # the base files under 0.3.9. The old block-identity check read the
        # two split blocks as new content and refused it.
        self.rebase_on(SHARED_HEADING)
        self.commit_changelog(REPAIRED)
        code, err = self.run_check()
        self.assertEqual(code, 0, err)

    def test_a_declared_in_place_edit_of_a_published_entry_is_allowed(self):
        # #503: rewording a published entry under its own heading, declared
        # by a trailer on a commit in the range.
        self.commit_changelog(
            HEADED.replace("Already stamped.", "Reworded in place."),
            "Reword the entry\n\nChangelog-Edit: 0.3.9\n")
        code, err = self.run_check()
        self.assertEqual(code, 0, err)

    def test_an_undeclared_edit_is_refused_and_names_the_trailer(self):
        self.commit_changelog(HEADED.replace("Already stamped.", "Reworded in place."))
        code, err = self.run_check()
        self.assertEqual(code, 1, err)
        self.assertIn("Changelog-Edit: <version>", err)

    def test_an_edit_does_not_carry_a_new_item_through(self):
        edited = HEADED.replace(
            "- **A published entry.** Already stamped.\n",
            "- **A published entry.** Reworded in place.\n\n"
            "- **A new entry.** Not yet stamped.\n")
        self.commit_changelog(edited, "Reword\n\nChangelog-Edit: 0.3.9\n")
        code, err = self.run_check()
        self.assertEqual(code, 1, err)
        self.assertIn("A new entry.", err)

    def test_moving_an_entry_between_headings_is_allowed(self):
        # An archive repair -- filing a past entry under the version that
        # published it -- appears in the diff as an addition below a heading,
        # exactly like the hazard. Block-content identity tells them apart: the
        # moved block is text the base already carries (#452).
        moved = HEADED.replace(
            "## 0.3.9 — 2026-01-02\n\n### Added\n\n- **A published entry.** Already stamped.\n",
            "## 0.3.10 — 2026-01-03\n\n### Added\n\n- **A published entry.** Already stamped.\n")
        self.commit_changelog(moved)
        code, err = self.run_check()
        self.assertEqual(code, 0, err)

    def test_a_second_copy_of_a_parked_entry_is_a_new_parked_block(self):
        # coding-policy#457: `set()` dropped multiplicity, so both copies were
        # members of the one-element set and a genuinely added duplicate went
        # through. Occurrences are consumed one for one.
        duplicated = HEADED.replace(
            "### Added\n\n- **A published entry.** Already stamped.\n",
            "### Added\n\n- **A published entry.** Already stamped.\n"
            "\n### Added\n\n- **A published entry.** Already stamped.\n")
        self.commit_changelog(duplicated)
        code, err = self.run_check()
        self.assertEqual(code, 1, err)
        self.assertIn("are new since", err)

    def test_a_branch_touching_no_entries_passes(self):
        self.git("checkout", "-q", "-b", "work")
        (self.root / "other.txt").write_text("x", encoding="utf-8")
        self.git("add", "other.txt")
        self.git("commit", "-q", "-m", "unrelated")
        code, err = self.run_check()
        self.assertEqual(code, 0, err)

    def test_absent_git_is_a_tool_error_not_a_verdict(self):
        # `subprocess.run` raises FileNotFoundError, and an uncaught exception
        # exits 1 — the misfiling verdict. A missing tool is the absence of an
        # answer, so it must exit 2.
        self.commit_changelog(MISFILED)
        env = dict(_os.environ, PATH=str(self.root / "no-tools"))
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--base", "main"],
            cwd=self.root, capture_output=True, text=True, env=env)
        self.assertEqual(proc.returncode, 2, proc.stderr)
        self.assertIn("cannot run `git`", proc.stderr)

    def test_an_undecodable_changelog_is_a_tool_error_not_a_verdict(self):
        # UnicodeDecodeError is not an OSError; letting it escape exits 1, the
        # misfiling verdict, for a file the tool simply cannot read.
        self.commit_changelog(MISFILED)
        self.changelog.write_bytes(b"# Changelog\n\n\xff\xfe not utf-8\n")
        code, err = self.run_check()
        self.assertEqual(code, 2, err)
        self.assertIn("cannot decode", err)

    def commit_undecodable_path(self, author="Test"):
        """Commit a file whose path bytes are not UTF-8; return its sha.

        Staged straight into the index so no filesystem has to accept the
        name. `diff-tree -z` then emits those bytes raw.
        """
        blob = subprocess.run(["git", "hash-object", "-w", "--stdin"],
                              cwd=self.root, input=b"x", check=True,
                              capture_output=True).stdout.strip()
        subprocess.run(["git", "update-index", "--add", "--cacheinfo",
                        b"100644," + blob + b",bad-\xff-name"],
                       cwd=self.root, check=True, capture_output=True)
        self.git("-c", "user.name=" + author, "commit", "-q", "-m", "odd path")
        return subprocess.run(["git", "rev-parse", "HEAD"], cwd=self.root,
                              check=True, capture_output=True,
                              text=True).stdout.strip()

    def test_checked_turns_undecodable_output_into_a_tool_error(self):
        sha = self.commit_undecodable_path()
        cwd = _os.getcwd()
        _os.chdir(self.root)
        self.addCleanup(_os.chdir, cwd)
        with self.assertRaises(RuntimeError) as caught:
            check.checked("diff-tree", "-z", "--no-commit-id", "--name-only",
                          "-r", "--root", sha)
        self.assertIn("not UTF-8", str(caught.exception))

    def test_undecodable_git_output_is_a_tool_error_not_a_verdict(self):
        # A bot commit whose path is not UTF-8 reaches last_publish() through
        # checked(). An uncaught UnicodeDecodeError exits 1, the misfiling
        # verdict, for output the tool cannot read.
        self.commit_undecodable_path(author="github-actions[bot]")
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 2, proc.stderr)
        self.assertIn("not UTF-8", proc.stderr)
        self.assertEqual(proc.payload["verdict"], "error")

    def test_an_unknown_base_is_a_tool_error_not_a_verdict(self):
        self.commit_changelog(STAMPABLE)
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--base", "no-such-ref"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 2)
        self.assertIn("failed", proc.stderr)


class PublishModeTest(_RepoCase):
    """`--since-last-publish`: the stamp step's check before it publishes.

    #581's merge published 0.3.291 while its entry sat under 0.3.290 and
    nothing sat above the first `## ` heading, so the stamp found nothing to
    head and the version shipped unheaded. The baseline is the last publish
    bookkeeping commit, so a stopped publish stays stopped.
    """

    def setUp(self):
        super().setUp()
        self.manifest = self.root / ".tessl-plugin" / "plugin.json"
        self.manifest.parent.mkdir()
        self.manifest.write_text('{"version": "0.3.8"}\n', encoding="utf-8")
        self.git("add", ".tessl-plugin/plugin.json")
        self.git("commit", "-q", "-m", "manifest")
        self.bump("0.3.9")

    def bump(self, version):
        """The publish pipeline's version-bump commit, authored by the bot."""
        self.manifest.write_text('{"version": "%s"}\n' % version, encoding="utf-8")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-am",
                 "Bump to " + version)

    def merge(self, text, message="merge"):
        self.changelog.write_text(text, encoding="utf-8")
        self.git("commit", "-q", "-am", message)

    def run_publish(self):
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish"],
            cwd=self.root, capture_output=True, text=True)
        self.last = proc.payload
        return proc.returncode, proc.stderr

    def test_a_merge_parking_its_entry_under_a_published_heading_stops_the_publish(self):
        # The #581 shape: the merged item lands inside the block the previous
        # stamp headed, so nothing sits above the first `## ` heading.
        self.merge(HEADED.replace(
            "### Added\n\n",
            "### Added\n\n- **A new entry.** Not yet stamped.\n\n"))
        code, err = self.run_publish()
        self.assertEqual(code, 1, err)
        self.assertIn("The publish is stopped", err)
        self.assertEqual(self.last["new_items"],
                         ["- **A new entry.** Not yet stamped."])

    def test_a_stampable_merge_publishes(self):
        self.merge(STAMPABLE)
        code, err = self.run_publish()
        self.assertEqual(code, 0, err)

    def test_a_later_push_cannot_carry_a_stopped_item_through(self):
        # The stopped publish wrote no bookkeeping commit, so the next push
        # (or a manual dispatch) is measured from the last real publish.
        self.merge(MISFILED)
        (self.root / "other.txt").write_text("x", encoding="utf-8")
        self.git("add", "other.txt")
        self.git("commit", "-q", "-m", "unrelated")
        code, err = self.run_publish()
        self.assertEqual(code, 1, err)
        self.assertIn("A new entry.", err)

    def test_a_publish_after_the_last_bump_has_nothing_new(self):
        # A manual dispatch right after a publish: HEAD is the bump commit.
        code, err = self.run_publish()
        self.assertEqual(code, 0, err)

    def test_a_bot_commit_touching_other_files_is_not_a_baseline(self):
        self.merge(MISFILED)
        (self.root / "generated.txt").write_text("x", encoding="utf-8")
        self.git("add", "generated.txt")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-m", "regen")
        code, err = self.run_publish()
        self.assertEqual(code, 1, err)

    def test_a_declared_edit_publishes(self):
        self.merge(HEADED.replace("Already stamped.", "Reworded in place."),
                   "Reword\n\nChangelog-Edit: 0.3.9\n")
        code, err = self.run_publish()
        self.assertEqual(code, 0, err)

    def test_a_changelog_path_with_spaces_is_still_measured(self):
        # `diff-tree --name-only` split on whitespace broke this path into
        # pieces, no bookkeeping commit matched, and the gate passed having
        # measured nothing.
        nested = self.root / "docs" / "My CHANGELOG.md"
        nested.parent.mkdir()
        nested.write_text(HEADED, encoding="utf-8")
        self.git("add", "docs/My CHANGELOG.md")
        self.git("commit", "-q", "-m", "move changelog")
        nested.write_text(HEADED + "\n", encoding="utf-8")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-am",
                 "Stamp")
        nested.write_text(MISFILED, encoding="utf-8")
        self.git("commit", "-q", "-am", "merge")
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish", "--changelog", "docs/My CHANGELOG.md"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 1, proc.stderr)
        self.assertEqual(proc.payload["verdict"], "misfiled")

    def test_a_non_canonical_changelog_spelling_is_still_measured(self):
        # #618: `diff-tree` reports `CHANGELOG.md`, which never equalled
        # `./CHANGELOG.md`, so the bot's stamp commit was no baseline and the
        # entry it stamped read as new against the older bump.
        stamped = TWO_VERSIONS.replace("0.3.9", "0.3.10").replace("0.3.8", "0.3.9")
        self.changelog.write_text(stamped, encoding="utf-8")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-am",
                 "Stamp")
        self.merge(stamped.replace(
            "## 0.3.10", "### Added\n\n- **A new entry.** Not yet stamped.\n\n## 0.3.10"))
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish", "--changelog", "./CHANGELOG.md"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 0, proc.stderr)
        self.assertEqual(proc.payload["verdict"], "pass")

    def test_a_bot_bump_of_a_custom_manifest_is_a_baseline(self):
        # #618: the stamp action's `manifest:` input names the file the
        # bump commit touches; without it that commit was no baseline, so
        # the check measured from an older publish.
        custom = self.root / "meta" / "plugin.json"
        custom.parent.mkdir()
        custom.write_text('{"version": "0.3.9"}\n', encoding="utf-8")
        self.git("add", "meta/plugin.json")
        self.git("commit", "-q", "-m", "custom manifest")
        custom.write_text('{"version": "0.3.10"}\n', encoding="utf-8")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-am",
                 "Bump to 0.3.10")
        bump = subprocess.run(["git", "rev-parse", "HEAD"], cwd=self.root,
                              check=True, capture_output=True,
                              text=True).stdout.strip()
        self.merge(MISFILED)
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish", "--manifest", "meta/plugin.json"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 1, proc.stderr)
        self.assertEqual(proc.payload["base"], bump)

    def run_as_action(self, manifest=""):
        """The argv `.github/actions/stamp-changelog/action.yml` passes.

        The action forwards its `changelog` and `manifest` inputs verbatim,
        an unset `manifest:` as the empty string.
        """
        return run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish", "--changelog", "CHANGELOG.md",
             "--manifest", manifest],
            cwd=self.root, capture_output=True, text=True)

    def test_the_action_default_inputs_measure_from_the_last_bump(self):
        self.merge(MISFILED)
        proc = self.run_as_action()
        self.assertEqual(proc.payload["verdict"], "misfiled", proc.stderr)

    def test_the_action_forwards_a_custom_manifest_as_a_baseline(self):
        # The bot's bump of a custom manifest also stamps an entry; measured
        # from the older bump instead, that stamped entry would read as new.
        custom = self.root / "meta" / "plugin.json"
        custom.parent.mkdir()
        custom.write_text('{"version": "0.3.9"}\n', encoding="utf-8")
        self.git("add", "meta/plugin.json")
        self.git("commit", "-q", "-m", "custom manifest")
        stamped = TWO_VERSIONS.replace("0.3.9", "0.3.10").replace("0.3.8", "0.3.9")
        self.changelog.write_text(stamped, encoding="utf-8")
        custom.write_text('{"version": "0.3.10"}\n', encoding="utf-8")
        self.git("-c", "user.name=github-actions[bot]", "commit", "-q", "-am",
                 "Bump to 0.3.10")
        proc = self.run_as_action(manifest="./meta/plugin.json")
        self.assertEqual(proc.payload["verdict"], "pass", proc.stderr)

    def test_no_publish_in_history_has_nothing_to_measure(self):
        fresh = tempfile.TemporaryDirectory()
        self.addCleanup(fresh.cleanup)
        root = Path(fresh.name)
        for args in (("init", "-q", "-b", "main"),
                     ("config", "user.email", "t@example.com"),
                     ("config", "user.name", "Test")):
            subprocess.run(["git", *args], cwd=root, check=True, capture_output=True)
        (root / "CHANGELOG.md").write_text(MISFILED, encoding="utf-8")
        subprocess.run(["git", "add", "CHANGELOG.md"], cwd=root, check=True, capture_output=True)
        subprocess.run(["git", "commit", "-q", "-m", "first"], cwd=root, check=True, capture_output=True)
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish"],
            cwd=root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 0, proc.stderr)
        self.assertIn("nothing to measure", proc.stderr)

    def test_a_shallow_checkout_is_a_tool_error(self):
        self.merge(MISFILED)
        clone = tempfile.TemporaryDirectory()
        self.addCleanup(clone.cleanup)
        target = Path(clone.name) / "shallow"
        subprocess.run(["git", "clone", "-q", "--depth", "1",
                        "file://" + str(self.root), str(target)],
                       check=True, capture_output=True)
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--since-last-publish"],
            cwd=target, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 2, proc.stderr)
        self.assertIn("fetch-depth: 0", proc.stderr)

    def test_a_changelog_absent_at_the_last_publish_has_nothing_to_measure(self):
        self.git("rm", "-q", "CHANGELOG.md")
        self.git("commit", "-q", "-m", "drop")
        self.bump("0.3.10")
        self.changelog.write_text(MISFILED, encoding="utf-8")
        self.git("add", "CHANGELOG.md")
        self.git("commit", "-q", "-m", "first changelog")
        code, err = self.run_publish()
        self.assertEqual(code, 0, err)
        self.assertIn("does not exist at the last publish", err)

    def test_the_two_modes_are_exclusive(self):
        proc = run_script(
            [_sys.executable, _os.path.join(_ROOT, "check-changelog-placement.py"),
             "--base", "main", "--since-last-publish"],
            cwd=self.root, capture_output=True, text=True)
        self.assertEqual(proc.returncode, 2, proc.stderr)

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

skills

README.md

tile.json