mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
fix(setup): fill the CV and cover-letter template contact blocks; guard CHANGELOG structure (#433)
/setup Step 3 personalised cv/main_example.tex but never the LaTeX contact blocks embedded in 05-cv-templates.md and 06-cover-letter-templates.md, the two files /apply actually compiles from; 06 was not a Step 3 target at all. Step 3.5 now names the 05 contact tokens, a new Step 3.6 covers the 06 contact line and signature, the completion summary lists 06, and /reset restores both blocks instead of listing 06 as framework-only (the existing /reset coverage test forced that half). tests/test_changelog_structure.py checks [Unreleased] on every PR for duplicate headings, unknown headings, orphan entries and conflict markers - the #425 duplicate-heading shape that was fixed by hand at merge time. Claude-Session: https://claude.ai/code/session_013fqqLgQSnwgWkv98twQhHi Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
3bf41149e0
commit
e6f6f4e322
@@ -0,0 +1,124 @@
|
||||
"""Structural guard for CHANGELOG.md's [Unreleased] section.
|
||||
|
||||
Contributors edit one shared file by hand, and every PR inserts its entry near
|
||||
the same line. Two failure shapes have reached master or a merge queue:
|
||||
|
||||
- a second `### Fixed` heading added directly under `## [Unreleased]` because
|
||||
the author did not see the existing one further down (#425, fixed by hand at
|
||||
merge time), and
|
||||
- entries placed above any `###` heading, or under a heading Keep a Changelog
|
||||
does not define.
|
||||
|
||||
`lint_skills.py` does not read the changelog, so nothing caught either. This
|
||||
test does, on every PR. It only inspects [Unreleased]; released sections are
|
||||
history and stay as they are.
|
||||
"""
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
CHANGELOG = REPO / "CHANGELOG.md"
|
||||
|
||||
KNOWN_HEADINGS = {"Added", "Changed", "Deprecated", "Removed", "Fixed", "Security"}
|
||||
CONFLICT_MARKERS = ("<<<<<<< ", "=======", ">>>>>>> ")
|
||||
|
||||
|
||||
def unreleased_block(text: str) -> str:
|
||||
"""The lines between `## [Unreleased]` and the next `## [` heading."""
|
||||
start = text.index("## [Unreleased]")
|
||||
end = text.find("\n## [", start + 1)
|
||||
return text[start:] if end == -1 else text[start:end]
|
||||
|
||||
|
||||
def unreleased_problems(text: str) -> list[str]:
|
||||
"""Return a human-readable problem per structural defect in [Unreleased]."""
|
||||
problems: list[str] = []
|
||||
seen: list[str] = []
|
||||
current: str | None = None
|
||||
for lineno, line in enumerate(unreleased_block(text).splitlines(), 1):
|
||||
if any(line.startswith(marker) for marker in CONFLICT_MARKERS):
|
||||
problems.append(f"conflict marker on [Unreleased] line {lineno}: {line.strip()}")
|
||||
continue
|
||||
if line.startswith("### "):
|
||||
name = line[4:].strip()
|
||||
if name not in KNOWN_HEADINGS:
|
||||
problems.append(
|
||||
f"unknown heading '### {name}' in [Unreleased]; use one of {sorted(KNOWN_HEADINGS)}"
|
||||
)
|
||||
if name in seen:
|
||||
problems.append(
|
||||
f"'### {name}' appears twice in [Unreleased] - fold the entry into the existing section"
|
||||
)
|
||||
seen.append(name)
|
||||
current = name
|
||||
elif line.startswith("- ") and current is None:
|
||||
problems.append(f"entry above any '###' heading in [Unreleased]: {line.strip()[:70]}")
|
||||
return problems
|
||||
|
||||
|
||||
CLEAN = """# Changelog
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
|
||||
- **A new thing** - described.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **A fixed thing** - described.
|
||||
|
||||
## [1.0.0] - 2026-01-01
|
||||
|
||||
### Fixed
|
||||
|
||||
- old entry
|
||||
"""
|
||||
|
||||
|
||||
class UnreleasedProblemsTests(unittest.TestCase):
|
||||
def test_clean_section_reports_nothing(self):
|
||||
self.assertEqual(unreleased_problems(CLEAN), [])
|
||||
|
||||
def test_duplicate_heading_is_reported(self):
|
||||
# The exact #425 shape: a second "### Fixed" inserted directly under
|
||||
# [Unreleased], above "### Added", while "### Fixed" already exists below.
|
||||
text = CLEAN.replace(
|
||||
"## [Unreleased]\n\n### Added",
|
||||
"## [Unreleased]\n\n### Fixed\n\n- **Entry in the wrong place** - described.\n\n### Added",
|
||||
)
|
||||
problems = unreleased_problems(text)
|
||||
self.assertTrue(any("Fixed" in p and "twice" in p for p in problems), problems)
|
||||
|
||||
def test_unknown_heading_is_reported(self):
|
||||
text = CLEAN.replace("### Fixed", "### Fixes")
|
||||
problems = unreleased_problems(text)
|
||||
self.assertTrue(any("Fixes" in p for p in problems), problems)
|
||||
|
||||
def test_entry_above_any_heading_is_reported(self):
|
||||
text = CLEAN.replace(
|
||||
"## [Unreleased]\n\n### Added",
|
||||
"## [Unreleased]\n\n- **Orphan entry** - no heading above it.\n\n### Added",
|
||||
)
|
||||
problems = unreleased_problems(text)
|
||||
self.assertTrue(any("Orphan entry" in p for p in problems), problems)
|
||||
|
||||
def test_conflict_markers_are_reported(self):
|
||||
text = CLEAN.replace("### Fixed", "<<<<<<< HEAD\n### Fixed")
|
||||
problems = unreleased_problems(text)
|
||||
self.assertTrue(any("conflict marker" in p for p in problems), problems)
|
||||
|
||||
def test_released_sections_are_not_inspected(self):
|
||||
# A duplicate heading in an old release is history, not a defect here.
|
||||
text = CLEAN + "\n### Fixed\n\n- another old entry\n"
|
||||
self.assertEqual(unreleased_problems(text), [])
|
||||
|
||||
|
||||
class RealChangelogTests(unittest.TestCase):
|
||||
def test_unreleased_section_is_well_formed(self):
|
||||
text = CHANGELOG.read_text(encoding="utf-8")
|
||||
self.assertEqual(unreleased_problems(text), [])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user