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
@@ -41,7 +41,8 @@ Read the current state of these files and report whether each has content or is
|
|||||||
- `.claude/skills/job-application-assistant/01-candidate-profile.md`
|
- `.claude/skills/job-application-assistant/01-candidate-profile.md`
|
||||||
- `.claude/skills/job-application-assistant/02-behavioral-profile.md`
|
- `.claude/skills/job-application-assistant/02-behavioral-profile.md`
|
||||||
- `.claude/skills/job-application-assistant/04-job-evaluation.md` *(personalized match areas, career goals, and life-situation constraints only — the scoring framework is preserved)*
|
- `.claude/skills/job-application-assistant/04-job-evaluation.md` *(personalized match areas, career goals, and life-situation constraints only — the scoring framework is preserved)*
|
||||||
- `.claude/skills/job-application-assistant/05-cv-templates.md` *(profile statements section only — framework structure is preserved)*
|
- `.claude/skills/job-application-assistant/05-cv-templates.md` *(profile statements section and the contact block inside the LaTeX template only — framework structure is preserved)*
|
||||||
|
- `.claude/skills/job-application-assistant/06-cover-letter-templates.md` *(contact line and signature inside the LaTeX template only — framework structure is preserved)*
|
||||||
- `.claude/skills/job-application-assistant/07-interview-prep.md` *(STAR examples and STAR candidates sections only — framework structure is preserved)*
|
- `.claude/skills/job-application-assistant/07-interview-prep.md` *(STAR examples and STAR candidates sections only — framework structure is preserved)*
|
||||||
- `.claude/skills/job-scraper/search-queries.md` *(role titles, domain keywords, and location terms only — query structure is preserved)*
|
- `.claude/skills/job-scraper/search-queries.md` *(role titles, domain keywords, and location terms only — query structure is preserved)*
|
||||||
|
|
||||||
@@ -63,8 +64,11 @@ Present as:
|
|||||||
constraints will be restored to placeholders. The scoring framework (dimensions,
|
constraints will be restored to placeholders. The scoring framework (dimensions,
|
||||||
score bands, weights, Language Gate, Company Research Checklist) is preserved.
|
score bands, weights, Language Gate, Company Research Checklist) is preserved.
|
||||||
|
|
||||||
- 05-cv-templates.md — [has profile statements / already blank]
|
- 05-cv-templates.md — [has profile statements or contact details / already blank]
|
||||||
Profile statement templates will be cleared. LaTeX structure and tailoring guidelines are preserved.
|
Profile statement templates will be cleared and the contact block in the LaTeX template restored to placeholders. LaTeX structure and tailoring guidelines are preserved.
|
||||||
|
|
||||||
|
- 06-cover-letter-templates.md — [has contact details / already blank]
|
||||||
|
The contact line and signature in the LaTeX template will be restored to placeholders. Letter structure, opening patterns, and closing formulations are preserved.
|
||||||
|
|
||||||
- 07-interview-prep.md — [has STAR examples / already blank]
|
- 07-interview-prep.md — [has STAR examples / already blank]
|
||||||
STAR examples and any STAR candidate stubs will be cleared. Framework, tough questions, and roleplay guidelines are preserved.
|
STAR examples and any STAR candidate stubs will be cleared. Framework, tough questions, and roleplay guidelines are preserved.
|
||||||
@@ -75,7 +79,6 @@ Present as:
|
|||||||
|
|
||||||
The following files are NOT touched (they contain framework rules, not candidate data):
|
The following files are NOT touched (they contain framework rules, not candidate data):
|
||||||
- 03-writing-style.md
|
- 03-writing-style.md
|
||||||
- 06-cover-letter-templates.md
|
|
||||||
|
|
||||||
Outside the profile scope, still holding your personal data: CLAUDE.md and
|
Outside the profile scope, still holding your personal data: CLAUDE.md and
|
||||||
cv/main_example.tex. This scope covers skill files only.
|
cv/main_example.tex. This scope covers skill files only.
|
||||||
@@ -207,7 +210,9 @@ Leave the rest of `04-job-evaluation.md` intact: the five scoring dimensions and
|
|||||||
<!-- Run /setup to populate role-specific profile statements -->
|
<!-- Run /setup to populate role-specific profile statements -->
|
||||||
```
|
```
|
||||||
|
|
||||||
Leave all other content in `05-cv-templates.md` intact.
|
Then restore the contact block inside the file's LaTeX template to its placeholder tokens: `\name{[FIRST_NAME]}{[LAST_NAME]}`, `\address{[YOUR_ADDRESS]}{}{}`, `\phone[mobile]{[YOUR_PHONE]}`, `\email{[YOUR_EMAIL]}`, the `\extrainfo{...}` line's `[YOUR_LINKEDIN_URL]` and `[YOUR_GITHUB_URL]`, and `[YOUR_NAME]` in the `pdftitle`. Leave all other content in `05-cv-templates.md` intact.
|
||||||
|
|
||||||
|
**For `06-cover-letter-templates.md`**, restore the contact line and the signature inside the file's LaTeX template to their placeholder tokens: the `\namesection{}` line becomes `\namesection{}{\Huge{[YOUR_NAME]}}{ \href{mailto:[YOUR_EMAIL]}{[YOUR_EMAIL]} | [YOUR_PHONE] | \urlstyle{same}\href{[YOUR_LINKEDIN_URL]}{LinkedIn}` and `\signature{...}` becomes `\signature{[YOUR_NAME]}`. Leave all other content in `06-cover-letter-templates.md` intact - the letter structure, opening patterns, and closing formulations are framework, not candidate data. If `/setup` Step 3.6 ever personalizes anything beyond these two lines, add it here too.
|
||||||
|
|
||||||
**For `07-interview-prep.md`**, locate and remove:
|
**For `07-interview-prep.md`**, locate and remove:
|
||||||
- The entire `## Ready-Made STAR Examples` section and all numbered STAR examples under it
|
- The entire `## Ready-Made STAR Examples` section and all numbered STAR examples under it
|
||||||
@@ -223,7 +228,7 @@ Replace with:
|
|||||||
|
|
||||||
Leave all other content in `07-interview-prep.md` intact (STAR format explanation, tough questions, questions to ask interviewers, phone/video tips, follow-up etiquette, roleplay guidelines).
|
Leave all other content in `07-interview-prep.md` intact (STAR format explanation, tough questions, questions to ask interviewers, phone/video tips, follow-up etiquette, roleplay guidelines).
|
||||||
|
|
||||||
**For `.claude/skills/job-scraper/search-queries.md`**, restore the values `/setup` Step 3.8 personalized back to their placeholder tokens:
|
**For `.claude/skills/job-scraper/search-queries.md`**, restore the values `/setup` Step 3.9 personalized back to their placeholder tokens:
|
||||||
|
|
||||||
- **Search Sites**: the board names back to `[YOUR_JOB_BOARD]`, `[YOUR_INDUSTRY_JOB_BOARD]`, `[YOUR_ADDITIONAL_JOB_BOARD]`, and the LinkedIn filter back to `[YOUR_COUNTRY]` / `[YOUR_CITY]`.
|
- **Search Sites**: the board names back to `[YOUR_JOB_BOARD]`, `[YOUR_INDUSTRY_JOB_BOARD]`, `[YOUR_ADDITIONAL_JOB_BOARD]`, and the LinkedIn filter back to `[YOUR_COUNTRY]` / `[YOUR_CITY]`.
|
||||||
- **Query Categories**: the four priority headings back to `[YOUR_PRIMARY_ROLE_TYPE]`, `[YOUR_DOMAIN_EXPERTISE]`, `[YOUR_ADJACENT_ROLE_TYPE]`, and `Broader Technical / Consulting`; inside the query blocks, the titles, skills, and domain terms back to `[YOUR_PRIMARY_JOB_TITLE_1]`, `[YOUR_PRIMARY_JOB_TITLE_2]`, `[YOUR_ADJACENT_TITLE_1]`, `[YOUR_ADJACENT_TITLE_2]`, `[YOUR_KEY_SKILL]`, `[YOUR_DOMAIN_KEYWORD_1]`, `[YOUR_DOMAIN_KEYWORD_2]`, `[YOUR_DOMAIN]`, and the location terms back to `[YOUR_CITY]`, `[YOUR_COUNTRY]`, `[YOUR_REGION]`.
|
- **Query Categories**: the four priority headings back to `[YOUR_PRIMARY_ROLE_TYPE]`, `[YOUR_DOMAIN_EXPERTISE]`, `[YOUR_ADJACENT_ROLE_TYPE]`, and `Broader Technical / Consulting`; inside the query blocks, the titles, skills, and domain terms back to `[YOUR_PRIMARY_JOB_TITLE_1]`, `[YOUR_PRIMARY_JOB_TITLE_2]`, `[YOUR_ADJACENT_TITLE_1]`, `[YOUR_ADJACENT_TITLE_2]`, `[YOUR_KEY_SKILL]`, `[YOUR_DOMAIN_KEYWORD_1]`, `[YOUR_DOMAIN_KEYWORD_2]`, `[YOUR_DOMAIN]`, and the location terms back to `[YOUR_CITY]`, `[YOUR_COUNTRY]`, `[YOUR_REGION]`.
|
||||||
|
|||||||
@@ -367,15 +367,18 @@ Replace skill match areas with the user's actual skills:
|
|||||||
Update career goals and motivation filters with their actual preferences.
|
Update career goals and motivation filters with their actual preferences.
|
||||||
|
|
||||||
### 5. Update `05-cv-templates.md` *(Path B and C; skip if Path A populated it)*
|
### 5. Update `05-cv-templates.md` *(Path B and C; skip if Path A populated it)*
|
||||||
Add role-specific profile statement templates based on their background.
|
Add role-specific profile statement templates based on their background, and personalise the contact block inside the file's LaTeX template: replace `[FIRST_NAME]`, `[LAST_NAME]`, `[YOUR_ADDRESS]`, `[YOUR_PHONE]`, `[YOUR_EMAIL]`, `[YOUR_LINKEDIN_URL]` and `[YOUR_GITHUB_URL]` (and `[YOUR_NAME]` in the PDF title) with their actual details. Check this block whichever path ran - Path A extracts profile statements from documents, not the contact block. `/apply` builds every tailored CV from this template, so a placeholder left here reaches a compiled document.
|
||||||
|
|
||||||
### 6. Update `07-interview-prep.md` *(Path B and C; skip if Path A populated it)*
|
### 6. Update `06-cover-letter-templates.md` *(all paths - Path A does not fill this block)*
|
||||||
|
Personalise the contact line and the signature inside the file's LaTeX template: replace `[YOUR_NAME]`, `[YOUR_EMAIL]`, `[YOUR_PHONE]` and `[YOUR_LINKEDIN_URL]` in the `\namesection{}` line, and `[YOUR_NAME]` in `\signature{}`. Path A merges only structural patterns (openings, bullets, closings) into this file, never the contact block. `/apply` compiles every cover letter from this template.
|
||||||
|
|
||||||
|
### 7. Update `07-interview-prep.md` *(Path B and C; skip if Path A populated it)*
|
||||||
Create STAR examples from their actual experience (at least 3-4 examples). Path A leaves STAR stubs under "## STAR Candidates (Complete Manually)" rather than full examples; if any stubs are present, mention them in Step 4 so the user knows to flesh them out.
|
Create STAR examples from their actual experience (at least 3-4 examples). Path A leaves STAR stubs under "## STAR Candidates (Complete Manually)" rather than full examples; if any stubs are present, mention them in Step 4 so the user knows to flesh them out.
|
||||||
|
|
||||||
### 7. Update `cv/main_example.tex`
|
### 8. Update `cv/main_example.tex`
|
||||||
Replace placeholder personal data with their actual name, contact info, and add their education and most recent experience entries.
|
Replace placeholder personal data with their actual name, contact info, and add their education and most recent experience entries.
|
||||||
|
|
||||||
### 8. Generate `.claude/skills/job-scraper/search-queries.md`
|
### 9. Generate `.claude/skills/job-scraper/search-queries.md`
|
||||||
Replace all placeholder tokens in the search queries file with the user's actual information from Section 9 (or the equivalent follow-up questions in Path A's Step A7):
|
Replace all placeholder tokens in the search queries file with the user's actual information from Section 9 (or the equivalent follow-up questions in Path A's Step A7):
|
||||||
- Replace `[YOUR_PRIMARY_ROLE_TYPE]`, `[YOUR_PRIMARY_JOB_TITLE]`, etc. with actual role titles
|
- Replace `[YOUR_PRIMARY_ROLE_TYPE]`, `[YOUR_PRIMARY_JOB_TITLE]`, etc. with actual role titles
|
||||||
- Replace `[YOUR_KEY_SKILL]`, `[YOUR_DOMAIN_KEYWORD_1]`, etc. with actual skills and domain terms
|
- Replace `[YOUR_KEY_SKILL]`, `[YOUR_DOMAIN_KEYWORD_1]`, etc. with actual skills and domain terms
|
||||||
@@ -399,7 +402,8 @@ Present a summary:
|
|||||||
> - `.claude/skills/job-application-assistant/01-candidate-profile.md` - Structured profile
|
> - `.claude/skills/job-application-assistant/01-candidate-profile.md` - Structured profile
|
||||||
> - `.claude/skills/job-application-assistant/02-behavioral-profile.md` - Behavioral assessment
|
> - `.claude/skills/job-application-assistant/02-behavioral-profile.md` - Behavioral assessment
|
||||||
> - `.claude/skills/job-application-assistant/04-job-evaluation.md` - Personalized evaluation framework
|
> - `.claude/skills/job-application-assistant/04-job-evaluation.md` - Personalized evaluation framework
|
||||||
> - `.claude/skills/job-application-assistant/05-cv-templates.md` - CV templates with your profile statements
|
> - `.claude/skills/job-application-assistant/05-cv-templates.md` - CV templates with your profile statements and contact block
|
||||||
|
> - `.claude/skills/job-application-assistant/06-cover-letter-templates.md` - Cover letter templates with your contact line and signature
|
||||||
> - `.claude/skills/job-application-assistant/07-interview-prep.md` - STAR examples from your experience
|
> - `.claude/skills/job-application-assistant/07-interview-prep.md` - STAR examples from your experience
|
||||||
> - `cv/main_example.tex` - Your LaTeX CV template
|
> - `cv/main_example.tex` - Your LaTeX CV template
|
||||||
> - `.claude/skills/job-scraper/search-queries.md` - Job search queries for `/scrape`
|
> - `.claude/skills/job-scraper/search-queries.md` - Job search queries for `/scrape`
|
||||||
|
|||||||
@@ -15,6 +15,13 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- **CHANGELOG structure guard** (`tests/test_changelog_structure.py`) - every PR edits this one
|
||||||
|
shared file by hand near the same line, and nothing checked the result: a second `### Fixed`
|
||||||
|
heading landed directly under `[Unreleased]`, above `### Added`, on #425 and was fixed by hand
|
||||||
|
at merge time. The `[Unreleased]` section is now checked on every PR for duplicate headings,
|
||||||
|
headings outside the Keep a Changelog set, entries above any heading, and leftover conflict
|
||||||
|
markers. Released sections are history and are not inspected.
|
||||||
|
|
||||||
- **`/rank` now consumes the `posted_date` #391 persists** (#390, the deferred second
|
- **`/rank` now consumes the `posted_date` #391 persists** (#390, the deferred second
|
||||||
half) - Step 3 gains a staleness flag: a posting whose stored `posted_date` is more
|
half) - Step 3 gains a staleness flag: a posting whose stored `posted_date` is more
|
||||||
than 30 days old at rank time carries a visible ⚠ marker with its age spelled out
|
than 30 days old at rank time carries a visible ⚠ marker with its age spelled out
|
||||||
@@ -41,6 +48,18 @@ per-file diff commands.
|
|||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
||||||
|
- **`/setup` now fills the contact blocks inside `05-cv-templates.md` and
|
||||||
|
`06-cover-letter-templates.md`, and `/reset` restores them** - Step 3 personalised
|
||||||
|
`cv/main_example.tex` but never the LaTeX contact blocks embedded in the two template files
|
||||||
|
`/apply` actually compiles from, so a full Path B or C run left `[YOUR_NAME]`, `[YOUR_EMAIL]`
|
||||||
|
and `[YOUR_PHONE]` in both, and whether they reached a document depended on the drafter
|
||||||
|
noticing (a real user ran `/setup` and then hand-edited both files, #420).
|
||||||
|
`06-cover-letter-templates.md` 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 (Path A never fills
|
||||||
|
it, so it runs for every path), the completion summary lists `06`, and `/reset` clears both
|
||||||
|
blocks instead of listing `06` as framework-only. Pinned by `tests/test_setup_command.py`; the
|
||||||
|
existing `/reset` coverage test is what forced the `reset.md` half.
|
||||||
|
|
||||||
- **`/rank` no longer reads or rewrites the whole of `seen_jobs.json` on every run** (#395) -
|
- **`/rank` no longer reads or rewrites the whole of `seen_jobs.json` on every run** (#395) -
|
||||||
Step 1 used to read the entire state file into the conversation to select candidates by
|
Step 1 used to read the entire state file into the conversation to select candidates by
|
||||||
eye, and Step 4 emitted it back to record scores: a cost paid on every run regardless of
|
eye, and Step 4 emitted it back to record scores: a cost paid on every run regardless of
|
||||||
|
|||||||
@@ -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()
|
||||||
@@ -32,7 +32,7 @@ PROFILE_SENTINEL = "[YOUR_EMAIL]"
|
|||||||
|
|
||||||
|
|
||||||
def personalize_cv(text: str) -> str:
|
def personalize_cv(text: str) -> str:
|
||||||
"""Apply /setup Step 3.7's documented edit: replace placeholder personal
|
"""Apply /setup Step 3.8's documented edit: replace placeholder personal
|
||||||
data with a real name and contact info. Header comments and hyperref
|
data with a real name and contact info. Header comments and hyperref
|
||||||
metadata are not personal data, so they are deliberately left alone -
|
metadata are not personal data, so they are deliberately left alone -
|
||||||
that is exactly why a comment-located sentinel guards nothing."""
|
that is exactly why a comment-located sentinel guards nothing."""
|
||||||
|
|||||||
@@ -0,0 +1,87 @@
|
|||||||
|
"""Guards for the /setup command spec.
|
||||||
|
|
||||||
|
The command is a markdown spec (the spec IS the implementation). These tests pin
|
||||||
|
one invariant that broke silently: Step 3 must personalise every contact block
|
||||||
|
that `/apply` later compiles into a document. `cv/main_example.tex` was covered;
|
||||||
|
the LaTeX blocks embedded in `05-cv-templates.md` and `06-cover-letter-templates.md`
|
||||||
|
were not, so a full Path B/C run left `[YOUR_NAME]`, `[YOUR_EMAIL]` and
|
||||||
|
`[YOUR_PHONE]` in both, and whether they reached a compiled cover letter depended
|
||||||
|
on the drafter noticing. A real user (#420) ran `/setup` and then hand-edited both
|
||||||
|
files to close the gap.
|
||||||
|
"""
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
|
COMMAND = REPO / ".claude" / "commands" / "setup.md"
|
||||||
|
SKILL_DIR = REPO / ".claude" / "skills" / "job-application-assistant"
|
||||||
|
CV_TEMPLATES = SKILL_DIR / "05-cv-templates.md"
|
||||||
|
COVER_TEMPLATES = SKILL_DIR / "06-cover-letter-templates.md"
|
||||||
|
|
||||||
|
|
||||||
|
def _sections(text: str) -> dict[str, str]:
|
||||||
|
"""Split a command spec into {heading: body} by '## ' headers."""
|
||||||
|
parts = text.split("\n## ")
|
||||||
|
result = {}
|
||||||
|
for part in parts[1:]:
|
||||||
|
heading, _, body = part.partition("\n")
|
||||||
|
result[heading.strip()] = body
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def _substeps(step_body: str) -> dict[str, str]:
|
||||||
|
"""Split a step body into {'### N. ...' heading: body}."""
|
||||||
|
parts = step_body.split("\n### ")
|
||||||
|
result = {}
|
||||||
|
for part in parts[1:]:
|
||||||
|
heading, _, body = part.partition("\n")
|
||||||
|
result[heading.strip()] = body
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
class SetupStep3ContactBlocks(unittest.TestCase):
|
||||||
|
def setUp(self):
|
||||||
|
self.step3 = _sections(COMMAND.read_text(encoding="utf-8"))["Step 3: Generate Profile Files"]
|
||||||
|
self.substeps = _substeps(self.step3)
|
||||||
|
|
||||||
|
def _substep_for(self, filename: str) -> str:
|
||||||
|
matches = [body for heading, body in self.substeps.items() if filename in heading]
|
||||||
|
self.assertEqual(len(matches), 1, f"expected exactly one Step 3 substep for {filename}, got {len(matches)}")
|
||||||
|
return matches[0]
|
||||||
|
|
||||||
|
def test_cv_templates_substep_fills_the_contact_block(self):
|
||||||
|
body = self._substep_for("05-cv-templates.md")
|
||||||
|
self.assertIn("contact", body.lower())
|
||||||
|
for token in ("[FIRST_NAME]", "[YOUR_EMAIL]", "[YOUR_PHONE]"):
|
||||||
|
self.assertIn(token, body, f"the 05 substep must name {token} as something to replace")
|
||||||
|
|
||||||
|
def test_cover_letter_templates_get_their_own_substep(self):
|
||||||
|
body = self._substep_for("06-cover-letter-templates.md")
|
||||||
|
self.assertIn("signature", body.lower())
|
||||||
|
for token in ("[YOUR_NAME]", "[YOUR_EMAIL]", "[YOUR_PHONE]", "[YOUR_LINKEDIN_URL]"):
|
||||||
|
self.assertIn(token, body, f"the 06 substep must name {token} as something to replace")
|
||||||
|
|
||||||
|
def test_completion_summary_lists_the_cover_letter_templates(self):
|
||||||
|
step4 = _sections(COMMAND.read_text(encoding="utf-8"))["Step 4: Confirm & Next Steps"]
|
||||||
|
summary = step4.split("**Privacy note:**")[0]
|
||||||
|
self.assertIn("06-cover-letter-templates.md", summary)
|
||||||
|
|
||||||
|
|
||||||
|
class TemplatesStillCarryThePlaceholders(unittest.TestCase):
|
||||||
|
"""The instructions above target real tokens; if a template renames them,
|
||||||
|
the instruction and this test must move together."""
|
||||||
|
|
||||||
|
def test_cv_templates_contact_block_tokens(self):
|
||||||
|
text = CV_TEMPLATES.read_text(encoding="utf-8")
|
||||||
|
for token in ("[FIRST_NAME]", "[LAST_NAME]", "[YOUR_EMAIL]", "[YOUR_PHONE]"):
|
||||||
|
self.assertIn(token, text)
|
||||||
|
|
||||||
|
def test_cover_letter_templates_contact_and_signature_tokens(self):
|
||||||
|
text = COVER_TEMPLATES.read_text(encoding="utf-8")
|
||||||
|
for token in ("[YOUR_NAME]", "[YOUR_EMAIL]", "[YOUR_PHONE]", "[YOUR_LINKEDIN_URL]"):
|
||||||
|
self.assertIn(token, text)
|
||||||
|
self.assertIn("\\signature{[YOUR_NAME]}", text)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user