tests.test_changelog module¶
Tests for repomatic.changelog: parsing, updating and linting the changelog.
- tests.test_changelog.SAMPLE_CHANGELOG = '# Changelog\n\n## [`1.2.3` (unreleased)](https://github.com/user/repo/compare/v1.2.2...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n\n- Add new feature.\n- Fix bug.\n\n## [`1.2.2` (2024-01-15)](https://github.com/user/repo/compare/v1.2.1...v1.2.2)\n\n- Previous release.\n'¶
Reusable changelog fixture for freeze tests.
- tests.test_changelog.test_freeze_idempotent()[source]¶
Test that freezing an already-frozen changelog is a no-op.
- tests.test_changelog.test_freeze_file(tmp_path)[source]¶
Test that freeze_file freezes a changelog on disk.
- tests.test_changelog.test_freeze_file_already_released(tmp_path)[source]¶
Test that freeze_file is a no-op for released changelogs.
- tests.test_changelog.test_freeze_file_missing(tmp_path)[source]¶
Test that freeze_file handles missing files gracefully.
- tests.test_changelog.DECOMPOSE_CHANGELOG = '# Changelog\n\n## [`3.0.0` (unreleased)](https://github.com/user/repo/compare/v2.0.0...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n\n- New feature.\n\n## [`2.0.0` (2026-02-01)](https://github.com/user/repo/compare/v1.0.0...v2.0.0)\n\n> [!NOTE]\n> `2.0.0` is available on [🐍 PyPI](https://pypi.org/project/pkg/2.0.0/).\n\n> [!CAUTION]\n> `2.0.0` has been [yanked from PyPI](https://pypi.org/project/pkg/2.0.0/).\n\n- Breaking change.\n- New API.\n\n## [`1.0.0` (2025-12-01)](https://github.com/user/repo/compare/v0.9.0...v1.0.0)\n\n- Initial release.\n'¶
Changelog with all element types for decompose tests.
- tests.test_changelog.test_decompose_version(version, expected_date, expected_url, contains, equals)[source]¶
Decompose versions with varying element combinations.
- tests.test_changelog.test_decompose_version_extracts_editorial_admonitions()[source]¶
Editorial admonitions (e.g. [!TIP]) land in editorial_admonition.
- tests.test_changelog.test_decompose_version_multiple_editorial_admonitions()[source]¶
Multiple editorial admonitions are joined in editorial_admonition.
- tests.test_changelog.test_decompose_version_editorial_with_auto_generated()[source]¶
Editorial and auto-generated admonitions are correctly separated.
- tests.test_changelog.test_decompose_version_empty()[source]¶
Version not found returns empty elements.
- tests.test_changelog.test_replace_section(version, new_section, expected, present, absent)[source]¶
Test replacing an entire section (heading + body).
- tests.test_changelog.MULTI_RELEASE_CHANGELOG = '# Changelog\n\n## [2.0.0 (unreleased)](https://github.com/user/repo/compare/v1.1.0...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n\n## [1.1.0 (2026-02-10)](https://github.com/user/repo/compare/v1.0.0...v1.1.0)\n\n- Second release.\n\n## [1.0.0 (2025-12-01)](https://github.com/user/repo/compare/v0.9.0...v1.0.0)\n\n- Initial release.\n'¶
Changelog with multiple released versions and one unreleased.
- tests.test_changelog.changelog_file(tmp_path)[source]¶
A changelog on disk carrying
MULTI_RELEASE_CHANGELOG.A test needing other content writes over the returned path.
- tests.test_changelog.test_extract_all_releases(content, expected)[source]¶
Test extraction of released versions from varying changelogs.
- tests.test_changelog.test_lint_changelog_dates_pypi_all_match(changelog_file, monkeypatch)[source]¶
Test that lint returns 0 when all PyPI dates match.
- tests.test_changelog.test_lint_changelog_dates_pypi_mismatch(changelog_file, monkeypatch)[source]¶
Test that lint returns 1 when a PyPI date differs.
- tests.test_changelog.test_lint_changelog_dates_fallback_to_tags(changelog_file, monkeypatch)[source]¶
Test that lint falls back to git tags when not on PyPI.
- tests.test_changelog.test_lint_changelog_dates_fallback_no_package(changelog_file, monkeypatch)[source]¶
Test that lint falls back to git tags when no package name is detected.
- tests.test_changelog.test_lint_changelog_dates_warns_missing_pypi(changelog_file, monkeypatch, caplog)[source]¶
Test that versions not on PyPI get a warning if they postdate the first PyPI release, and an info log if they predate it.
- tests.test_changelog.test_lint_changelog_dates_skips_pre_pypi(changelog_file, monkeypatch, caplog)[source]¶
Test that versions older than the first PyPI release are skipped.
- tests.test_changelog.test_lint_changelog_dates_skips_abandoned(changelog_file, monkeypatch, caplog)[source]¶
Test that explicitly-abandoned versions are skipped without warning.
- tests.test_changelog.test_lint_changelog_dates_archive_suppresses_orphans(tmp_path, monkeypatch)[source]¶
Versions documented in the archive are neither flagged nor re-inserted as orphans.
- tests.test_changelog.test_lint_changelog_dates_ignores_prerelease_orphans(tmp_path, monkeypatch, prerelease)[source]¶
A published pre-release is never treated as a missing changelog entry.
The changelog documents only final releases. A pre-release (dev/alpha/beta/rc) present as a git tag, PyPI upload, or GitHub release must not be flagged as an orphan. The former bug inserted a spurious
## X.Y.Z.dev0section (placed below its final release, since a pre-release sorts lower) and rewrote the final release’s comparison-URL base to point at it (v1.0.0...v2.0.0becamev2.0.0.dev0...v2.0.0).
- tests.test_changelog.test_split_changelog_bullets_count(changes, expected_count)[source]¶
Each top-level
-starts one entry; wraps and sub-bullets fold in.
- tests.test_changelog.test_split_changelog_bullets_folds_continuation_and_sub_bullets()[source]¶
A wrapped line and indented sub-bullets stay with their parent entry.
- tests.test_changelog.test_count_bullet_words(bullet, expected_words)[source]¶
List markers are stripped before counting; everything else counts.
- tests.test_changelog.test_warn_on_long_bullets_flags_unreleased(caplog)[source]¶
An over-long unreleased bullet emits a non-fatal warning.
- tests.test_changelog.test_warn_on_long_bullets_short_entry_is_silent(caplog)[source]¶
A short unreleased bullet stays under the ceiling and warns nothing.
- tests.test_changelog.test_warn_on_long_bullets_ignores_released_sections(caplog)[source]¶
Immutable released sections are never flagged, only the unreleased one.
- tests.test_changelog.test_warn_on_long_bullets_threshold_zero_disables(caplog)[source]¶
A threshold of 0 disables the check, even for a very long bullet.
- tests.test_changelog.test_warn_on_empty_sections_flags_released(caplog)[source]¶
A released section with no entry emits a non-fatal warning.
- tests.test_changelog.test_warn_on_empty_sections_ignores_the_unreleased_one(caplog)[source]¶
The empty unreleased section the post-release bump creates is expected.
- tests.test_changelog.test_warn_on_empty_sections_counts_an_admonition_as_empty(caplog)[source]¶
An availability caveat is not a statement of what changed.
- tests.test_changelog.test_annotation_reads_true_downstream(warn, content, capsys)[source]¶
A lint annotation cites a section the published documentation declares.
The annotation shows in the checks of a downstream repository, which holds no copy of this project’s
claude.md.
- tests.test_changelog.test_lint_changelog_dates_warns_long_unreleased_bullet(tmp_path, caplog)[source]¶
lint-changelog surfaces the bullet-length warning without failing.
An unreleased-only changelog has no released versions, so the date check returns 0 early; the length warning still fires beforehand.
- tests.test_changelog.test_lint_fix_corrects_date(changelog_file, monkeypatch)[source]¶
Test that –fix corrects mismatched dates in the changelog.
- tests.test_changelog.test_lint_fix_adds_release_admonition(changelog_file, monkeypatch)[source]¶
Test that –fix adds conditional release admonitions.
- tests.test_changelog.test_lint_fix_github_only(changelog_file, monkeypatch)[source]¶
Test that –fix adds GitHub-only admonition when not on PyPI.
- tests.test_changelog.THREE_RELEASE_CHANGELOG = '# Changelog\n\n## [3.0.0 (unreleased)](https://github.com/user/repo/compare/v2.0.0...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n\n## [2.0.0 (2026-02-10)](https://github.com/user/repo/compare/v1.0.0...v2.0.0)\n\n- Second release.\n\n## [1.0.0 (2025-12-01)](https://github.com/user/repo/compare/v0.9.0...v1.0.0)\n\n- First release.\n\n## [0.5.0 (2025-06-01)](https://github.com/user/repo/compare/v0.4.0...v0.5.0)\n\n- Pre-PyPI release.\n'¶
Changelog with three released versions for first-version testing.
- tests.test_changelog.test_lint_fix_first_version_admonition(tmp_path, monkeypatch)[source]¶
Test that –fix uses ‘first version’ wording for inaugural releases.
- tests.test_changelog.test_lint_fix_pypi_only(changelog_file, monkeypatch)[source]¶
Test that –fix adds PyPI NOTE and GitHub WARNING when only on PyPI.
- tests.test_changelog.test_lint_fix_no_warning_predates_github(changelog_file, monkeypatch)[source]¶
Test that –fix skips GitHub WARNING for versions predating the first GitHub release.
- tests.test_changelog.test_lint_fix_adds_yanked_admonition(changelog_file, monkeypatch)[source]¶
Test that –fix adds a CAUTION admonition for yanked releases.
- tests.test_changelog.test_lint_fix_yanked_admonition_carries_reason(changelog_file, monkeypatch)[source]¶
PyPI’s yank reason lands in the CAUTION admonition.
- tests.test_changelog.test_lint_fix_no_admonition_when_nowhere(changelog_file, monkeypatch)[source]¶
Test that –fix adds WARNINGs for both platforms when version is on neither PyPI nor GitHub.
- tests.test_changelog.test_lint_fix_idempotent(changelog_file, monkeypatch)[source]¶
Test that running –fix twice produces the same result.
Test that –fix removes stale ‘is not available on’ warnings when the version becomes available.
- tests.test_changelog.test_lint_keeps_availability_when_only_the_cache_is_stale(tmp_path, monkeypatch, caplog, fix)[source]¶
A cached lookup predating a release must not retract its availability.
Both lookups are TTL-cached for a day, so between publishing
1.1.0and the cache expiring they still answer “1.0.0 only”. Acting on that would stamp a false “not available” warning onto a correct section under--fix, and report the published version as missing in either mode.
- tests.test_changelog.test_lint_finds_a_release_newer_than_the_cache(tmp_path, monkeypatch, caplog, fix)[source]¶
A release the cache predates is re-confirmed live, not reported missing.
Its section records no availability yet, as
--fixhas not run since the release. The cached lookups then have no claim to retract, and their answer is still stale.
- tests.test_changelog.test_lint_leaves_a_recorded_gap_to_the_cache(tmp_path, monkeypatch)[source]¶
A gap the section already records costs no live request.
A version that never reached a platform keeps its “not available” warning from one run to the next: re-fetching it on every run would defeat the cache.
- tests.test_changelog.test_lint_fix_retracts_availability_confirmed_missing(tmp_path, monkeypatch)[source]¶
A live-confirmed absence still retracts, so the gate is not a veto.
The counterpart to
test_lint_keeps_availability_when_only_the_cache_is_stale(): when the forced refresh agrees the release is gone (a deleted GitHub release, a removed PyPI file), rewriting the section is the correct repair.
- tests.test_changelog.test_lint_fix_refuses_when_confirming_github_lookup_fails(tmp_path, monkeypatch)[source]¶
An unreachable API during confirmation must not license a retraction.
- tests.test_changelog.test_lint_reports_an_unconfirmed_gap_when_github_lookup_fails(tmp_path, monkeypatch, caplog)[source]¶
A failed confirming lookup is reported as such, never as a confirmation.
- tests.test_changelog.test_lint_confirms_a_pypi_only_gap_when_github_lookup_fails(tmp_path, monkeypatch, caplog)[source]¶
A gap on PyPI alone is settled by the fresh PyPI answer.
The GitHub half stays cached, but no GitHub answer is needed to tell whether PyPI carries the release.
- tests.test_changelog.test_extract_all_version_headings()[source]¶
Test that all versions (released and unreleased) are extracted.
- tests.test_changelog.test_extract_all_version_headings_empty()[source]¶
Test extraction from an empty changelog.
- tests.test_changelog.test_insert_version_section()[source]¶
Test inserting a placeholder section for a missing version.
- tests.test_changelog.test_insert_version_section_idempotent()[source]¶
Test that inserting an already-present version is a no-op.
- tests.test_changelog.test_insert_version_section_at_end()[source]¶
Test inserting a version older than all existing ones.
- tests.test_changelog.test_update_comparison_base(version, new_base, expected, present, absent)[source]¶
Test replacing the comparison base in a version heading.
- tests.test_changelog.test_lint_orphan_detection_returns_1(changelog_file, monkeypatch, caplog)[source]¶
Test that an orphaned version causes lint to return 1.
- tests.test_changelog.test_lint_orphan_fix_inserts_placeholder(changelog_file, monkeypatch)[source]¶
Test that –fix inserts placeholder sections for orphaned versions.
- tests.test_changelog.test_lint_orphan_fix_idempotent(changelog_file, monkeypatch)[source]¶
Test that running –fix with orphans twice produces the same result.
- tests.test_changelog.test_lint_orphan_tag_only(changelog_file, monkeypatch, caplog)[source]¶
Test orphan detected from git tag only (not on PyPI or GitHub).
- tests.test_changelog.test_lint_orphan_uses_pypi_date(changelog_file, monkeypatch)[source]¶
Test that orphan fix prefers PyPI date over GitHub and tag dates.
- tests.test_changelog.test_lint_unreleased_not_flagged_as_orphan(changelog_file, monkeypatch)[source]¶
Test that the unreleased dev version is not flagged as orphan.
- tests.test_changelog.RENAME_CHANGELOG = '# Changelog\n\n## [2.0.0 (unreleased)](https://github.com/user/repo/compare/v1.1.0...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n\n## [1.1.0 (2026-02-10)](https://github.com/user/repo/compare/v1.0.0...v1.1.0)\n\n- New release under current name.\n\n## [1.0.0 (2025-12-01)](https://github.com/user/repo/compare/v0.9.0...v1.0.0)\n\n- Release under old name.\n'¶
Changelog fixture for package rename tests.
- tests.test_changelog.test_lint_fix_pypi_package_history(tmp_path, monkeypatch)[source]¶
Versions from former package names get correct PyPI URLs.
- tests.test_changelog.test_lint_pypi_history_current_wins(tmp_path, monkeypatch)[source]¶
Current package name wins when a version exists under both names.
- tests.test_changelog.CHANGELOG_WITH_ADMONITIONS = '# Changelog\n\n## [`1.3.0` (2026-03-01)](https://github.com/user/repo/compare/v1.2.0...v1.3.0)\n\n> [!NOTE]\n> `1.3.0` is available on [🐍 PyPI](https://pypi.org/project/my-package/1.3.0/) and [🐙 GitHub](https://github.com/user/repo/releases/tag/v1.3.0).\n\n- Third release.\n\n## [`1.2.0` (2026-02-01)](https://github.com/user/repo/compare/v1.1.0...v1.2.0)\n\n> [!NOTE]\n> `1.2.0` is available on [🐍 PyPI](https://pypi.org/project/my-package/1.2.0/) and [🐙 GitHub](https://github.com/user/repo/releases/tag/v1.2.0).\n\n- Second release.\n\n## [`1.1.0` (2026-01-01)](https://github.com/user/repo/compare/v1.0.0...v1.1.0)\n\n> [!NOTE]\n> `1.1.0` is available on [🐍 PyPI](https://pypi.org/project/my-package/1.1.0/) and [🐙 GitHub](https://github.com/user/repo/releases/tag/v1.1.0).\n\n- Minor release.\n\n## [`1.0.0` (2025-12-01)](https://github.com/user/repo/compare/v0.9.0...v1.0.0)\n\n> [!NOTE]\n> `1.0.0` is the *first version* available on [🐍 PyPI](https://pypi.org/project/my-package/1.0.0/) and [🐙 GitHub](https://github.com/user/repo/releases/tag/v1.0.0).\n\n- Initial release.\n'¶
Changelog with four releases, each carrying both PyPI and GitHub links.
Used by sanity-gate tests to assert that a destructive rewrite is refused when an upstream lookup fails or returns no data.
- tests.test_changelog.test_lint_fix_refuses_rewrite_when_github_lookup_raises(tmp_path, monkeypatch)[source]¶
The sanity gate refuses to strip every GitHub link when the API errors.
This replays the kdeldycke/click-extra#1702 scenario: GitHub returns a transient error, the legacy code treated the empty dict as “no GitHub releases,” and the rewrite stripped every
🐙 GitHublink from the file. The gate exits non-zero before any write.
- tests.test_changelog.test_lint_fix_refuses_rewrite_when_pypi_empty_with_coverage(tmp_path, monkeypatch)[source]¶
The sanity gate refuses when PyPI returns nothing but coverage exists.
PyPI’s client conflates
404with all transient failures, so an empty result above the coverage threshold is treated as a failure fingerprint rather than a genuine “package no longer published” transition.
- tests.test_changelog.test_lint_fix_proceeds_when_github_fails_but_no_existing_links(changelog_file, monkeypatch)[source]¶
No existing GitHub links → GitHub failure is not destructive, so proceed.
This is the legitimate “new repo, GitHub API flaky” case: there’s nothing in the changelog to lose, so the rewrite is a no-op on the GitHub side and can safely run.
- tests.test_changelog.test_lint_fix_proceeds_when_pypi_empty_below_threshold(tmp_path, monkeypatch)[source]¶
Empty PyPI fetch with < threshold existing PyPI links → proceed.
A project that’s never been on PyPI (or just migrated off) has at most a couple of PyPI links in its history. The threshold lets that legitimate case through while still catching the transient-failure fingerprint.
- tests.test_changelog.DEV_ONLY_CHANGELOG = '# Changelog\n\n## [`1.0.0.dev0` (unreleased)](https://github.com/user/repo/compare/v0.9.0...main)\n\n> [!WARNING]\n> This version is **not released yet** and is under active development.\n'¶
Changelog mid-development cycle, carrying no final-release heading.
- tests.test_changelog.FROZEN_CHANGELOG = '# Changelog\n\n## [`1.2.3` (2026-03-01)](https://github.com/user/repo/compare/v1.2.2...v1.2.3)\n\n- Add papaya sorting.\n'¶
Changelog whose newest section is already frozen, ready for a post-release bump.
- tests.test_changelog.test_insert_version_section_orders_against_dev_headings()[source]¶
A
.devNheading takes part in the insertion-point scan.The published
1.0.0outranks the1.0.0.dev0section still sitting in the changelog, so it belongs above it. A scan blind to development headings finds no lower version, falls through to the append-at-end branch, and files the newest release at the bottom.
- tests.test_changelog.test_update_retargets_comparison_url_to_default_branch(kwargs, expected)[source]¶
The new unreleased entry points at the branch the caller names.
freezeretargets this same URL back to a tag, so a hard-codedmainhere leaves any repository on another default branch unable to round-trip.
- tests.test_changelog.test_freeze_warns_when_section_is_missing(caplog)[source]¶
A version with no changelog section warns instead of quietly no-opping.
- tests.test_changelog.test_freeze_already_frozen_stays_silent(caplog)[source]¶
The idempotent no-op path must not warn, or every re-run cries wolf.
- tests.test_changelog.test_lint_fix_fails_when_orphans_cannot_be_inserted(tmp_path, monkeypatch)[source]¶
A date fix must not mask an orphan the run was unable to insert.
Orphan insertion needs a repository URL to build comparison links from. Without one the orphan stays missing, so reporting success on the strength of an unrelated date correction would publish the gap.
- tests.test_changelog.test_lint_fix_skips_orphan_without_a_release_date(changelog_file, monkeypatch, caplog)[source]¶
An orphan no source can date is reported, not stamped with a placeholder.
A placeholder date satisfies the released-heading pattern, so it would be re-checked against the reference source on every later run and mismatch forever.
- tests.test_changelog.test_rendered_sections_are_an_mdformat_fixed_point(tmp_path, monkeypatch)[source]¶
Every section this module renders must survive
format-markdownintact.fix-changelogwriteschangelog.mdandformat-markdownreformats the same file on the same push. When the two disagree on the canonical layout they ping-pong: one job rewrites the section, the next reformats it, each opening its own PR, and neither converges.Both renderings are exercised, because they fail differently. The inserted placeholder leaves every optional admonition slot of the
release-notestemplate empty, which is where stray blank-line runs collect; the regenerated section fills the availability slot with a GFM alert, which onlymdformat_gfm_alertsknows how to reflow.Verified through the write path rather than
mdformat --check, whose verdict is unreliable for a tool carrying apost_processfixup (seeverify_via_write_path()).Marked
once: it resolves mdformat and its fifteen plugins through uvx, so one runner suffices.