Files
cimtechniques-service-suite/tests/release/test_commits.py
Andy 63169a7644 feat: add versioning and changelog system
Conventional-Commit-driven version bumps (scripts/bump.py), a commit-msg validation hook, and a /release-notes workflow that produces a human-reviewed CHANGELOG.md. Adds an in-app version badge + 'What's new' dialog on the launcher. The version is single-sourced from pyproject.toml (cim_suite.__version__), and the deterministic bump backbone lives in scripts/release/ with tests in tests/release/. Marks the 1.0.0 baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-06 11:31:22 -04:00

174 lines
5.5 KiB
Python

"""Tests for commit parsing, bump detection, range resolution, and classification."""
from __future__ import annotations
from release.commits import (
ParsedCommit,
build_digest,
commit_range,
determine_bump,
last_version_tag,
parse_commit,
validate_commit_message,
)
# --- parse_commit ----------------------------------------------------------
def test_parse_plain_type():
c = parse_commit("abc", "feat: add launcher")
assert c.type == "feat"
assert c.scope is None
assert c.breaking is False
assert c.description == "add launcher"
def test_parse_with_scope():
c = parse_commit("abc", "fix(repository): stretch Value column")
assert c.type == "fix"
assert c.scope == "repository"
assert c.description == "stretch Value column"
def test_parse_bang_marks_breaking():
c = parse_commit("abc", "feat(api)!: drop wireless sensors")
assert c.type == "feat"
assert c.scope == "api"
assert c.breaking is True
def test_parse_breaking_change_in_body():
c = parse_commit("abc", "refactor: rework transport", body="BREAKING CHANGE: ports moved")
assert c.breaking is True
def test_parse_breaking_change_hyphen_variant():
c = parse_commit("abc", "feat: x", body="BREAKING-CHANGE: y")
assert c.breaking is True
def test_parse_non_conventional_subject():
c = parse_commit("abc", "Merge branch 'feat/foo' into main")
assert c.type is None
assert c.breaking is False
assert c.description == "Merge branch 'feat/foo' into main"
# --- determine_bump --------------------------------------------------------
def test_bump_breaking_wins():
commits = [
parse_commit("1", "feat: a"),
parse_commit("2", "fix: b"),
parse_commit("3", "feat!: c"),
]
assert determine_bump(commits) == "major"
def test_bump_feat_over_fix():
commits = [parse_commit("1", "fix: a"), parse_commit("2", "feat: b")]
assert determine_bump(commits) == "minor"
def test_bump_fix_only():
commits = [parse_commit("1", "fix: a"), parse_commit("2", "docs: b")]
assert determine_bump(commits) == "patch"
def test_bump_none_when_no_releasable_commits():
commits = [parse_commit("1", "docs: a"), parse_commit("2", "chore: b")]
assert determine_bump(commits) is None
def test_bump_none_for_empty():
assert determine_bump([]) is None
# --- last_version_tag ------------------------------------------------------
def test_last_version_tag_picks_highest_semver():
tags = ["v1.0.0", "v1.2.0", "v1.10.0", "v1.9.0"]
assert last_version_tag(tags) == "v1.10.0"
def test_last_version_tag_ignores_non_version_tags():
tags = ["baseline", "v1.0.0", "release-candidate", "not-a-version"]
assert last_version_tag(tags) == "v1.0.0"
def test_last_version_tag_none_when_empty():
assert last_version_tag([]) is None
assert last_version_tag(["nightly", "stable"]) is None
# --- commit_range ----------------------------------------------------------
def test_range_prefers_last_tag():
assert commit_range(last_tag="v1.2.0", baseline="abc123") == "v1.2.0..HEAD"
def test_range_falls_back_to_baseline():
assert commit_range(last_tag=None, baseline="abc123") == "abc123..HEAD"
def test_range_all_commits_when_neither():
assert commit_range(last_tag=None, baseline=None) is None
# --- build_digest ----------------------------------------------------------
def _commits() -> list[ParsedCommit]:
return [
parse_commit("1", "feat: new screen"),
parse_commit("2", "fix: crash on open"),
parse_commit("3", "feat!: remove old mode"),
parse_commit("4", "refactor: tidy"),
parse_commit("5", "test: add coverage"),
parse_commit("6", "Merge branch 'x'"),
]
def test_digest_buckets():
d = build_digest(_commits())
assert [c.sha for c in d.features] == ["1", "3"]
assert [c.sha for c in d.fixes] == ["2"]
assert [c.sha for c in d.breaking] == ["3"]
# refactor, test, and the merge all collapse into internal.
assert [c.sha for c in d.internal] == ["4", "5", "6"]
# --- validate_commit_message ----------------------------------------------
def test_validate_accepts_conventional():
assert validate_commit_message("feat: add thing") is None
assert validate_commit_message("fix(scope): bug") is None
assert validate_commit_message("feat(api)!: breaking") is None
def test_validate_accepts_merge_and_revert():
assert validate_commit_message("Merge branch 'main' into feat/x") is None
assert validate_commit_message('Revert "feat: add thing"') is None
def test_validate_accepts_fixup_and_squash():
# git commit --fixup/--squash generate these; rejecting them breaks autosquash.
assert validate_commit_message("fixup! feat: add thing") is None
assert validate_commit_message("squash! fix: the bug") is None
assert validate_commit_message("amend! feat: add thing") is None
def test_validate_rejects_missing_type():
err = validate_commit_message("add a thing")
assert err is not None
assert "type" in err.lower()
def test_validate_rejects_unknown_type():
err = validate_commit_message("feet: typo in type")
assert err is not None
def test_validate_rejects_empty_description():
err = validate_commit_message("feat: ")
assert err is not None
def test_validate_ignores_comment_lines():
# git passes the whole COMMIT_EDITMSG; comment lines must not be validated.
msg = "feat: real subject\n\n# Please enter the commit message\n# comment"
assert validate_commit_message(msg) is None