mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(reset): clear document entries while preserving scaffolds (#580)
* fix(reset): clear document entries while preserving scaffolds * fix(reset): preserve scaffolds across Bash versions * docs(reset): keep document inventory before presentation heading
This commit is contained in:
1 parent
3b16f80595
commit
d590aa1b10
4 files changed
+170
-20
No files matched your search
+26
-11
@@ -86,7 +86,11 @@ cv/main_example.tex. This scope covers skill files only.
|
||||
|
||||
### If scope includes `documents`:
|
||||
|
||||
Use Glob to list all files present in `documents/cv/`, `documents/linkedin/`, `documents/diplomas/`, `documents/references/`, `documents/projects/`, `documents/postings/`, and `documents/applications/`. Present as:
|
||||
Use Glob to list all files present in `documents/cv/`, `documents/linkedin/`, `documents/diplomas/`, `documents/references/`, `documents/projects/`, `documents/postings/`, and `documents/applications/`.
|
||||
|
||||
Include nested and hidden user entries in the inventory. Each subfolder's tracked `.gitkeep` is a scaffold placeholder: exclude it from the deletion list and from the empty/non-empty decision, and preserve it.
|
||||
|
||||
Present as:
|
||||
|
||||
```
|
||||
## Documents reset will delete:
|
||||
@@ -251,16 +255,27 @@ Leave the rest of the file intact: the portal-CLI and WebSearch-fallback explana
|
||||
|
||||
### Documents reset
|
||||
|
||||
For each non-empty document subfolder, delete all files within it using Bash `rm`. Do not delete the folder itself, and do not delete `documents/README.md`.
|
||||
For each non-empty document subfolder, delete all user entries, including nested folders and hidden files. Preserve the seven top-level subfolders, their `.gitkeep` placeholders, and `documents/README.md`. Run the following Bash block only after the confirmation above; its glob settings stay inside the subshell. If `documents/` or any of its seven roots is a symbolic link, stop before deleting anything and ask the user to replace that linked root with a local document folder.
|
||||
|
||||
```bash
|
||||
rm -f documents/cv/*
|
||||
rm -f documents/linkedin/*
|
||||
rm -f documents/diplomas/*
|
||||
rm -f documents/references/*
|
||||
rm -f documents/projects/*
|
||||
rm -f documents/postings/*
|
||||
rm -rf documents/applications/*/
|
||||
(
|
||||
set -e
|
||||
for directory in documents documents/{cv,linkedin,diplomas,references,projects,postings,applications}; do
|
||||
if [ -L "$directory" ]; then
|
||||
printf 'Reset stopped: %s is a symbolic link.\n' "$directory" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
unset GLOBIGNORE
|
||||
shopt -s dotglob nullglob
|
||||
for directory in documents/cv documents/linkedin documents/diplomas documents/references documents/projects documents/postings documents/applications; do
|
||||
for entry in "$directory"/*; do
|
||||
if [ "${entry##*/}" != ".gitkeep" ]; then
|
||||
rm -rf -- "$entry"
|
||||
fi
|
||||
done
|
||||
done
|
||||
)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -287,7 +302,7 @@ Then tell the user what to do next based on what was reset:
|
||||
> Note that `CLAUDE.md` and `cv/main_example.tex` are outside the `profile` scope and still hold your personal data. If you are handing this fork over or making it public, clear them by hand.
|
||||
|
||||
**If documents were reset:**
|
||||
> The `documents/` folder is now empty. Add your career documents and run `/setup` to populate your profile. See `documents/README.md` for instructions on what to put where.
|
||||
> Your user documents are now cleared. The folder structure, `.gitkeep` placeholders, and `documents/README.md` are preserved. Add your career documents and run `/setup` to populate your profile.
|
||||
|
||||
**If both were reset:**
|
||||
> Both your profile files and documents folder are now empty. Add documents to `documents/` (or skip and use the CV import / interview path), then run `/setup`.
|
||||
> Your profile data and user documents are now cleared; the document folders, `.gitkeep` placeholders, and `documents/README.md` are preserved. Add documents to `documents/` (or skip and use the CV import / interview path), then run `/setup`.
|
||||
@@ -68,6 +68,8 @@ per-file diff commands.
|
||||
|
||||
### Fixed
|
||||
|
||||
- **`/reset documents` clears nested and hidden user entries while preserving scaffolds** - cleanup retains tracked `.gitkeep` files and the README, with the complete inventory shown before confirmation (#580).
|
||||
|
||||
- **`jobindex-search` examples follow the search CLI contract** - the posted-today example includes a required query, unsupported flags are removed, and regression checks accept LF and CRLF checkouts (#579).
|
||||
|
||||
- **`convert_salary_excel.py` converts the IDA workbook again** (#615, `tools/convert_salary_excel.py`,
|
||||
|
||||
@@ -28,13 +28,16 @@ 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 shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
REPO = Path(__file__).resolve().parent.parent
|
||||
RESET = REPO / ".claude" / "commands" / "reset.md"
|
||||
SETUP = REPO / ".claude" / "commands" / "setup.md"
|
||||
BASH = shutil.which("bash")
|
||||
|
||||
|
||||
def tracked_document_subfolders():
|
||||
@@ -73,16 +76,26 @@ class TestResetCoversEveryDocumentsSubfolder(unittest.TestCase):
|
||||
f"so the user confirms a deletion list that omits them: {missing}",
|
||||
)
|
||||
|
||||
@unittest.skipUnless(BASH, "requires Bash to replay the documented /reset command")
|
||||
def test_delete_block_removes_every_subfolder(self):
|
||||
deleted = set(re.findall(r"rm -r?f documents/(\w+)/", self.text))
|
||||
missing = sorted(self.folders - deleted)
|
||||
self.assertEqual(
|
||||
missing,
|
||||
[],
|
||||
"reset.md's delete block has no rm line for these documents/ "
|
||||
'subfolders, yet the command then claims "The `documents/` '
|
||||
f'folder is now empty.": {missing}',
|
||||
)
|
||||
# Exercise the deletion contract, rather than requiring one particular
|
||||
# spelling of rm. Keep deriving the folder set from git so a newly
|
||||
# tracked document root still has to be handled by the real command.
|
||||
section = self.text.split("### Documents reset", 1)[1]
|
||||
script = re.search(r"```bash\n(.*?)```", section, re.S).group(1)
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
for folder in self.folders:
|
||||
path = root / "documents" / folder
|
||||
path.mkdir(parents=True)
|
||||
(path / ".gitkeep").write_text("Keep scaffold.\n")
|
||||
(path / "user.txt").write_text("Remove document.\n")
|
||||
result = subprocess.run([BASH, "-c", script], cwd=root, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
for folder in self.folders:
|
||||
path = root / "documents" / folder
|
||||
self.assertEqual([entry.name for entry in path.iterdir()], [".gitkeep"])
|
||||
self.assertEqual((path / ".gitkeep").read_text(), "Keep scaffold.\n")
|
||||
|
||||
|
||||
def section(text: str, start: str, end: str) -> str:
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""Replay /reset's actual Bash commands against isolated document files."""
|
||||
import errno
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
COMMAND = Path(__file__).resolve().parent.parent / ".claude/commands/reset.md"
|
||||
FOLDERS = ("cv", "linkedin", "diplomas", "references", "projects", "postings", "applications")
|
||||
BASH = shutil.which("bash")
|
||||
|
||||
|
||||
def symlinks_available():
|
||||
"""Probe only the filesystem capability needed by the scope controls."""
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
target = root / "target"
|
||||
target.touch()
|
||||
try:
|
||||
(root / "link").symlink_to(target)
|
||||
except NotImplementedError:
|
||||
return False
|
||||
except OSError as exc:
|
||||
if exc.errno in (errno.ENOSYS, errno.ENOTSUP, errno.EPERM):
|
||||
return False
|
||||
if os.name == "nt" and getattr(exc, "winerror", None) in (5, 1314):
|
||||
return False
|
||||
raise
|
||||
return True
|
||||
|
||||
|
||||
SYMLINKS = bool(BASH) and symlinks_available()
|
||||
|
||||
|
||||
@unittest.skipUnless(BASH, "requires Bash to replay the documented /reset command")
|
||||
class ResetDocuments(unittest.TestCase):
|
||||
def reset_script(self):
|
||||
section = COMMAND.read_text(encoding="utf-8").split("### Documents reset", 1)[1]
|
||||
return re.search(r"```bash\n(.*?)```", section, re.S).group(1)
|
||||
|
||||
def run_reset(self, populated):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
documents = root / "documents"
|
||||
documents.mkdir()
|
||||
readme = documents / "README.md"
|
||||
readme.write_text("Keep these framework instructions.\n")
|
||||
sibling = root / "outside-documents.md"
|
||||
sibling.write_text("Keep this external file.\n")
|
||||
for name in FOLDERS:
|
||||
folder = documents / name
|
||||
folder.mkdir()
|
||||
(folder / ".gitkeep").write_text("Keep this scaffold.\n")
|
||||
if populated:
|
||||
(folder / "summary.md").write_text("A document\n")
|
||||
(folder / ".source.md").write_text("A hidden document\n")
|
||||
nested = folder / "project-or-application"
|
||||
nested.mkdir()
|
||||
(nested / "README.md").write_text("A nested document\n")
|
||||
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, "")
|
||||
self.assertEqual(readme.read_text(), "Keep these framework instructions.\n")
|
||||
self.assertEqual(sibling.read_text(), "Keep this external file.\n")
|
||||
self.assertEqual({path.name for path in documents.iterdir()}, {*FOLDERS, "README.md"})
|
||||
for name in FOLDERS:
|
||||
self.assertEqual([path.name for path in (documents / name).iterdir()], [".gitkeep"])
|
||||
self.assertEqual((documents / name / ".gitkeep").read_text(), "Keep this scaffold.\n")
|
||||
|
||||
def test_all_document_entries_are_removed_without_removing_the_scaffold(self):
|
||||
self.run_reset(populated=True)
|
||||
|
||||
def test_empty_document_folders_remain_usable(self):
|
||||
self.run_reset(populated=False)
|
||||
|
||||
@unittest.skipUnless(SYMLINKS, "requires filesystem symlinks and Windows symlink privilege")
|
||||
def test_symlinked_roots_stop_before_any_deletion(self):
|
||||
for linked_root in ("documents", *(f"documents/{name}" for name in FOLDERS)):
|
||||
with self.subTest(linked_root=linked_root), TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
outside = root / "outside"
|
||||
outside.mkdir()
|
||||
sentinel = outside / "keep.md"
|
||||
sentinel.write_text("External document.\n")
|
||||
documents = root / "documents"
|
||||
documents.mkdir()
|
||||
for name in FOLDERS:
|
||||
folder = documents / name
|
||||
folder.mkdir()
|
||||
(folder / "local.md").write_text("Local document.\n")
|
||||
target = root / linked_root
|
||||
shutil.rmtree(target)
|
||||
target.symlink_to(outside, target_is_directory=True)
|
||||
locals_before = [path for name in FOLDERS for path in (documents / name).glob("local.md")]
|
||||
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
|
||||
self.assertNotEqual(result.returncode, 0)
|
||||
self.assertIn("symbolic link", result.stderr)
|
||||
self.assertEqual(sentinel.read_text(), "External document.\n")
|
||||
for path in locals_before:
|
||||
self.assertEqual(path.read_text(), "Local document.\n")
|
||||
|
||||
@unittest.skipUnless(SYMLINKS, "requires filesystem symlinks and Windows symlink privilege")
|
||||
def test_internal_symlink_is_removed_without_following_its_target(self):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
outside = root / "outside"
|
||||
outside.mkdir()
|
||||
sentinel = outside / "keep.md"
|
||||
sentinel.write_text("External document.\n")
|
||||
for name in FOLDERS:
|
||||
(root / "documents" / name).mkdir(parents=True)
|
||||
link = root / "documents/projects/external"
|
||||
link.symlink_to(outside, target_is_directory=True)
|
||||
result = subprocess.run([BASH, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertFalse(link.is_symlink())
|
||||
self.assertEqual(sentinel.read_text(), "External document.\n")
|
||||
Reference in new issue
Block a user