8Tests for the changelog generator (Phase 4E — G-E1, G-E2, G-E10).
12- Validate ``collect_changelog_entries`` parsing and filtering.
13- Validate ``format_changelog`` output format (Keep a Changelog).
14- Validate ``changelog_to_json`` structured output.
15- Integration tests for ``oct git changelog`` CLI command.
27- All tests use the ``temp_git_project`` fixture from conftest.
28- CLI tests use subprocess invocation.
34from pathlib
import Path
47 """Tests for collect_changelog_entries()."""
50 """Feat and fix commits produce correct sections."""
53 _git(temp_git_project,
"checkout",
"-b",
"feature/test")
54 commit_file(temp_git_project,
"src/a.py",
"# a\n",
"feat: add feature A")
55 commit_file(temp_git_project,
"src/b.py",
"# b\n",
"fix: resolve crash on startup")
57 entries = collect_changelog_entries(temp_git_project)
58 sections = [e.section
for e
in entries]
59 assert "Added" in sections
60 assert "Fixed" in sections
63 """Non-conventional commit messages are silently skipped."""
66 _git(temp_git_project,
"checkout",
"-b",
"feature/test2")
67 commit_file(temp_git_project,
"src/c.py",
"# c\n",
"just a random message")
68 commit_file(temp_git_project,
"src/d.py",
"# d\n",
"feat: real feature")
70 entries = collect_changelog_entries(temp_git_project)
71 assert len(entries) == 1
72 assert entries[0].section ==
"Added"
75 """--since ref limits entries to commits after ref."""
78 _git(temp_git_project,
"checkout",
"-b",
"feature/test3")
79 commit_file(temp_git_project,
"src/e.py",
"# e\n",
"feat: old feature")
81 _git(temp_git_project,
"tag",
"v0.1.0")
82 commit_file(temp_git_project,
"src/f.py",
"# f\n",
"feat: new feature")
84 entries = collect_changelog_entries(temp_git_project, since_ref=
"v0.1.0")
85 assert len(entries) == 1
86 assert entries[0].subject ==
"new feature"
89 """Fresh repo with no conventional commits returns empty."""
92 repo = tmp_path /
"empty"
94 _git(repo,
"init",
"-b",
"main")
95 _git(repo,
"config",
"user.email",
"t@t.com")
96 _git(repo,
"config",
"user.name",
"T")
97 (repo /
"f.txt").write_text(
"x", encoding=
"utf-8")
98 _git(repo,
"add",
".")
99 _git(repo,
"commit",
"-m",
"initial",
"--quiet")
101 entries = collect_changelog_entries(repo)
105 """Scopes in commit messages are preserved in entries."""
108 _git(temp_git_project,
"checkout",
"-b",
"feature/scope")
109 commit_file(temp_git_project,
"src/g.py",
"# g\n",
"feat(auth): add token validation")
111 entries = collect_changelog_entries(temp_git_project)
112 scoped = [e
for e
in entries
if e.scope ==
"auth"]
113 assert len(scoped) == 1
114 assert scoped[0].subject ==
"add token validation"
117 """Breaking changes are detected from ! marker."""
120 _git(temp_git_project,
"checkout",
"-b",
"feature/breaking")
121 commit_file(temp_git_project,
"src/h.py",
"# h\n",
"feat!: remove deprecated API")
123 entries = collect_changelog_entries(temp_git_project)
124 breaking = [e
for e
in entries
if e.breaking]
125 assert len(breaking) == 1
134 """Tests for format_changelog()."""
137 """Entries are grouped by section in canonical order."""
141 ChangelogEntry(section=
"Fixed", scope=
None, subject=
"fix bug", breaking=
False, sha=
"abc"),
142 ChangelogEntry(section=
"Added", scope=
None, subject=
"add thing", breaking=
False, sha=
"def"),
143 ChangelogEntry(section=
"Security", scope=
None, subject=
"patch vuln", breaking=
False, sha=
"ghi"),
145 text = format_changelog(entries)
147 added_pos = text.index(
"### Added")
148 fixed_pos = text.index(
"### Fixed")
149 security_pos = text.index(
"### Security")
150 assert added_pos < fixed_pos < security_pos
153 """--release produces version header with date."""
157 ChangelogEntry(section=
"Added", scope=
None, subject=
"new", breaking=
False, sha=
"abc"),
159 text = format_changelog(entries, version=
"v1.0.0")
160 assert "## [v1.0.0]" in text
163 """Without version, header is [Unreleased]."""
167 ChangelogEntry(section=
"Added", scope=
None, subject=
"new", breaking=
False, sha=
"abc"),
169 text = format_changelog(entries)
170 assert "## [Unreleased]" in text
173 """Breaking changes get BREAKING prefix."""
177 ChangelogEntry(section=
"Added", scope=
None, subject=
"remove old API", breaking=
True, sha=
"abc"),
179 text = format_changelog(entries)
180 assert "**BREAKING:**" in text
183 """Empty entries produce minimal valid output."""
186 text = format_changelog([])
187 assert "## [Unreleased]" in text
188 assert "No notable changes." in text
197 """Tests for changelog_to_json()."""
200 """JSON output has expected schema."""
204 ChangelogEntry(section=
"Added", scope=
"auth", subject=
"tokens", breaking=
False, sha=
"a1b"),
205 ChangelogEntry(section=
"Fixed", scope=
None, subject=
"crash", breaking=
False, sha=
"c2d"),
207 data = changelog_to_json(entries, version=
"v2.0.0")
208 assert data[
"version"] ==
"v2.0.0"
209 assert data[
"total_entries"] == 2
210 assert "Added" in data[
"sections"]
211 assert "Fixed" in data[
"sections"]
214 """Without version, JSON shows 'Unreleased'."""
217 data = changelog_to_json([], version=
None)
218 assert data[
"version"] ==
"Unreleased"
227 """Run ``oct git changelog`` as a subprocess."""
228 return subprocess.run(
229 [sys.executable,
"-m",
"oct",
"git",
"changelog", *extra_args],
237 """CLI integration tests for oct git changelog."""
240 """--dry-run prints changelog to stdout."""
241 _git(temp_git_project,
"checkout",
"-b",
"feature/cl")
242 commit_file(temp_git_project,
"src/x.py",
"# x\n",
"feat: add X")
245 assert result.returncode == 0
246 assert "Unreleased" in result.stdout
or "Added" in result.stdout
249 """--json produces valid JSON."""
250 _git(temp_git_project,
"checkout",
"-b",
"feature/json")
251 commit_file(temp_git_project,
"src/y.py",
"# y\n",
"fix: patch Y")
254 assert result.returncode == 0
255 data = json.loads(result.stdout)
256 assert "version" in data
257 assert "sections" in data
260 """--release writes CHANGELOG.md."""
261 _git(temp_git_project,
"checkout",
"-b",
"feature/release")
262 commit_file(temp_git_project,
"src/z.py",
"# z\n",
"feat: add Z")
265 assert result.returncode == 0
267 changelog = temp_git_project /
"CHANGELOG.md"
268 assert changelog.is_file()
269 content = changelog.read_text(encoding=
"utf-8")
270 assert "v1.0.0" in content
273 """--since limits scope."""
274 _git(temp_git_project,
"checkout",
"-b",
"feature/since")
275 commit_file(temp_git_project,
"src/s1.py",
"# s1\n",
"feat: first")
276 _git(temp_git_project,
"tag",
"v0.0.1")
277 commit_file(temp_git_project,
"src/s2.py",
"# s2\n",
"feat: second")
279 result =
_run_changelog(temp_git_project,
"--json",
"--since",
"v0.0.1")
280 assert result.returncode == 0
281 data = json.loads(result.stdout)
282 assert data[
"total_entries"] == 1
285 """Changelog command writes audit record."""
286 _git(temp_git_project,
"checkout",
"-b",
"feature/audit")
287 commit_file(temp_git_project,
"src/a.py",
"# a\n",
"feat: audited")
290 assert result.returncode == 0
292 audit_dir = temp_git_project /
".oct-audit"
293 if audit_dir.is_dir():
294 audit_files = list(audit_dir.glob(
"*.jsonl"))
295 assert len(audit_files) >= 1
None test_audit_trail(self, Path temp_git_project)
None test_since_flag(self, Path temp_git_project)
None test_json_output(self, Path temp_git_project)
None test_dry_run(self, Path temp_git_project)
None test_release_writes_file(self, Path temp_git_project)
None test_json_unreleased(self)
None test_json_structure(self)
None test_since_ref_limits_scope(self, Path temp_git_project)
None test_scope_preserved(self, Path temp_git_project)
None test_non_conventional_skipped(self, Path temp_git_project)
None test_empty_repo_returns_empty(self, Path tmp_path)
None test_feat_and_fix_commits(self, Path temp_git_project)
None test_breaking_change_detected(self, Path temp_git_project)
subprocess.CompletedProcess _run_changelog(Path root, *str extra_args)