mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-09-17 00:26:26 +00:00
/setup Step 3 populates six skill files; /reset profile cleared four. 04-job-evaluation.md was listed by name under "files NOT touched (they contain framework rules, not candidate data)" while Step 3.4 writes the user's match areas, career goals, energizing/draining tasks, financial situation and schedule constraints into it - and ci.yml's placeholder-integrity job already guards that file under "personal data may have been committed". job-scraper/search-queries.md, which Step 3.8 fills with their job boards, role titles, domain keywords, city and commute tiers, appeared nowhere in reset.md at all. Both are tracked and unignored, so Step 1 asked the user to confirm a wipe list that omitted them and Step 4 then reported a blank profile while /rank kept scoring against the old skills and career goals and /scrape kept running the old city and queries. Re-running /setup does not necessarily clean them either: Path A skips files whose content is "no longer placeholder text", and Step 3.8 is phrased as token replacement, with no tokens left to replace. Both files are now previewed and cleared, restoring their /setup placeholders while preserving the scoring framework and the query structure. 04-job-evaluation.md leaves the preserved list, which keeps 03-writing-style.md and 06-cover-letter-templates.md - the latter correctly, since its [YOUR_NAME] tokens are LaTeX scaffolding Step 3 never writes to. CLAUDE.md and cv/main_example.tex stay outside the profile scope, which reset.md:13 defines as skill files only; the preview and Step 4 now say they still hold personal data instead of implying a full wipe. tests/test_reset_command.py gains a profile-scope guard beside its documents-scope one, deriving the file list from /setup Step 3's own headings rather than hardcoding it, so a future /setup target that /reset forgets fails in CI. Against master the three cases fail on exactly the defect: preview missing search-queries.md, execution missing both, and the preserved list mislabelling 04-job-evaluation.md as framework-only - the last of which a filename search alone would have missed. Closes #364 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
co-authored by
Cursor
parent
8d2786118b
commit
d82df2fe51
+106
-10
@@ -1,15 +1,31 @@
|
||||
"""Guards for /reset's documents scope.
|
||||
"""Guards for /reset's two scopes: documents and profile.
|
||||
|
||||
/reset ends its documents pass by telling the user "The `documents/`
|
||||
folder is now empty." That statement is only true if every personal-data
|
||||
drop folder is actually covered by both the Step 1 preview and the
|
||||
Step 3 delete block. `documents/postings/` was missing from both while
|
||||
being documented in documents/README.md and protected as personal data
|
||||
by tools/security_guards.py (review finding F26, 2026-08-19), so a reset
|
||||
silently kept the user's hand-pasted job postings.
|
||||
Both scopes have the same failure mode - /reset promises a clean slate it
|
||||
does not deliver, because something that writes personal data is missing
|
||||
from the Step 1 preview the user confirms and from the Step 3 execution.
|
||||
|
||||
The folder list is derived from the repository tree, so adding a new
|
||||
drop folder under documents/ fails this test until /reset covers it.
|
||||
Documents scope: /reset ends its documents pass by telling the user "The
|
||||
`documents/` folder is now empty." That statement is only true if every
|
||||
personal-data drop folder is actually covered by both the Step 1 preview
|
||||
and the Step 3 delete block. `documents/postings/` was missing from both
|
||||
while being documented in documents/README.md and protected as personal
|
||||
data by tools/security_guards.py (review finding F26, 2026-08-19), so a
|
||||
reset silently kept the user's hand-pasted job postings.
|
||||
|
||||
Profile scope: the same class of gap, one scope over. /setup Step 3
|
||||
populates six skill files, and /reset profile cleared four of them -
|
||||
`04-job-evaluation.md` (the user's match areas, career goals, financial
|
||||
situation and schedule constraints) was listed by name as containing
|
||||
"framework rules, not candidate data", and `job-scraper/search-queries.md`
|
||||
(their role titles, city and commute tiers) appeared nowhere in reset.md.
|
||||
Both are tracked and unignored, and CI's placeholder-integrity job guards
|
||||
04-job-evaluation.md under "personal data may have been committed", so a
|
||||
"blank" profile left /rank scoring against the old skills and /scrape
|
||||
running the old city.
|
||||
|
||||
Both file lists are derived - the documents folders from the repository
|
||||
tree, the profile files from /setup Step 3's own headings - so a new drop
|
||||
folder or a new /setup target fails this test until /reset covers it.
|
||||
"""
|
||||
import re
|
||||
import subprocess
|
||||
@@ -18,6 +34,7 @@ from pathlib import Path
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
RESET = REPO / ".claude" / "commands" / "reset.md"
|
||||
SETUP = REPO / ".claude" / "commands" / "setup.md"
|
||||
|
||||
|
||||
def tracked_document_subfolders():
|
||||
@@ -68,5 +85,84 @@ class TestResetCoversEveryDocumentsSubfolder(unittest.TestCase):
|
||||
)
|
||||
|
||||
|
||||
def section(text: str, start: str, end: str) -> str:
|
||||
"""The slice of text from the start marker up to the end marker."""
|
||||
begin = text.index(start)
|
||||
return text[begin : text.index(end, begin)]
|
||||
|
||||
|
||||
def setup_step3_skill_files():
|
||||
"""Skill files /setup Step 3 populates, derived from its own headings.
|
||||
|
||||
Step 3's targets are written as '### <n>. <verb> `<target>`', where the
|
||||
target is either a bare filename resolved against .claude/skills/ or a
|
||||
repo-relative path. Non-skill targets (CLAUDE.md, cv/main_example.tex)
|
||||
are dropped: /reset profile's scope is skill files only.
|
||||
"""
|
||||
step3 = section(SETUP.read_text(encoding="utf-8"), "## Step 3:", "## Step 4:")
|
||||
files = set()
|
||||
for target in re.findall(r"^###\s+\d+\.\s+\w+\s+`([^`]+)`", step3, re.MULTILINE):
|
||||
if (REPO / target).exists():
|
||||
if target.startswith(".claude/skills/"):
|
||||
files.add(Path(target).name)
|
||||
continue
|
||||
matches = list((REPO / ".claude" / "skills").glob(f"*/{target}"))
|
||||
if matches:
|
||||
files.add(Path(target).name)
|
||||
return files
|
||||
|
||||
|
||||
class TestResetCoversEveryPersonalizedSkillFile(unittest.TestCase):
|
||||
def setUp(self):
|
||||
self.text = RESET.read_text(encoding="utf-8")
|
||||
self.files = setup_step3_skill_files()
|
||||
# /setup must actually still name these targets, or every assertion
|
||||
# below would pass vacuously against an empty set.
|
||||
self.assertGreaterEqual(len(self.files), 6, self.files)
|
||||
self.assertIn("04-job-evaluation.md", self.files)
|
||||
self.assertIn("search-queries.md", self.files)
|
||||
|
||||
def test_preview_lists_every_personalized_skill_file(self):
|
||||
preview = section(
|
||||
self.text, "### If scope includes `profile`:", "### If scope includes `documents`:"
|
||||
)
|
||||
missing = sorted(f for f in self.files if f not in preview)
|
||||
self.assertEqual(
|
||||
missing,
|
||||
[],
|
||||
"reset.md's profile preview never mentions these files that /setup "
|
||||
"Step 3 writes candidate data into, so the user types RESET against "
|
||||
f"a list that omits them: {missing}",
|
||||
)
|
||||
|
||||
def test_execution_clears_every_personalized_skill_file(self):
|
||||
execution = section(self.text, "### Profile reset", "### Documents reset")
|
||||
missing = sorted(f for f in self.files if f not in execution)
|
||||
self.assertEqual(
|
||||
missing,
|
||||
[],
|
||||
"reset.md's Step 3 profile pass has no instruction for these files, "
|
||||
'yet the command then reports the skill files are "now blank": '
|
||||
f"{missing}",
|
||||
)
|
||||
|
||||
def test_preserved_list_claims_no_personalized_file_is_framework_only(self):
|
||||
"""A file /setup personalizes must never be listed as framework-only.
|
||||
|
||||
This is the specific regression: 04-job-evaluation.md was named in the
|
||||
"NOT touched (they contain framework rules, not candidate data)" list,
|
||||
so merely searching reset.md for the filename would have found it.
|
||||
"""
|
||||
preserved = section(self.text, "The following files are NOT touched", "```")
|
||||
mislabeled = sorted(f for f in self.files if f in preserved)
|
||||
self.assertEqual(
|
||||
mislabeled,
|
||||
[],
|
||||
"reset.md tells the user these files contain 'framework rules, not "
|
||||
"candidate data', but /setup Step 3 writes candidate data into them: "
|
||||
f"{mislabeled}",
|
||||
)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user