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`:
|
### 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:
|
## Documents reset will delete:
|
||||||
@@ -251,16 +255,27 @@ Leave the rest of the file intact: the portal-CLI and WebSearch-fallback explana
|
|||||||
|
|
||||||
### Documents reset
|
### 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
|
```bash
|
||||||
rm -f documents/cv/*
|
(
|
||||||
rm -f documents/linkedin/*
|
set -e
|
||||||
rm -f documents/diplomas/*
|
for directory in documents documents/{cv,linkedin,diplomas,references,projects,postings,applications}; do
|
||||||
rm -f documents/references/*
|
if [ -L "$directory" ]; then
|
||||||
rm -f documents/projects/*
|
printf 'Reset stopped: %s is a symbolic link.\n' "$directory" >&2
|
||||||
rm -f documents/postings/*
|
exit 1
|
||||||
rm -rf documents/applications/*/
|
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.
|
> 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:**
|
**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:**
|
**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
|
### 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).
|
- **`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`,
|
- **`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.
|
folder or a new /setup target fails this test until /reset covers it.
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import unittest
|
import unittest
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
REPO = Path(__file__).resolve().parent.parent
|
REPO = Path(__file__).resolve().parent.parent
|
||||||
RESET = REPO / ".claude" / "commands" / "reset.md"
|
RESET = REPO / ".claude" / "commands" / "reset.md"
|
||||||
SETUP = REPO / ".claude" / "commands" / "setup.md"
|
SETUP = REPO / ".claude" / "commands" / "setup.md"
|
||||||
|
BASH = shutil.which("bash")
|
||||||
|
|
||||||
|
|
||||||
def tracked_document_subfolders():
|
def tracked_document_subfolders():
|
||||||
@@ -73,16 +76,26 @@ class TestResetCoversEveryDocumentsSubfolder(unittest.TestCase):
|
|||||||
f"so the user confirms a deletion list that omits them: {missing}",
|
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):
|
def test_delete_block_removes_every_subfolder(self):
|
||||||
deleted = set(re.findall(r"rm -r?f documents/(\w+)/", self.text))
|
# Exercise the deletion contract, rather than requiring one particular
|
||||||
missing = sorted(self.folders - deleted)
|
# spelling of rm. Keep deriving the folder set from git so a newly
|
||||||
self.assertEqual(
|
# tracked document root still has to be handled by the real command.
|
||||||
missing,
|
section = self.text.split("### Documents reset", 1)[1]
|
||||||
[],
|
script = re.search(r"```bash\n(.*?)```", section, re.S).group(1)
|
||||||
"reset.md's delete block has no rm line for these documents/ "
|
with TemporaryDirectory() as directory:
|
||||||
'subfolders, yet the command then claims "The `documents/` '
|
root = Path(directory)
|
||||||
f'folder is now empty.": {missing}',
|
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:
|
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