mirror of
https://github.com/MadsLorentzen/ai-job-search.git
synced 2026-11-02 13:12:50 +00:00
fix(reset): make the documents reset block POSIX so it runs under zsh (#699)
* fix(reset): make the documents reset block POSIX so it runs under zsh The block merged in #580 used the bash-only builtin "shopt -s dotglob nullglob". Claude Code's Bash tool runs the user's shell, which is zsh on macOS, where the line fails with "command not found: shopt", and under set -e the subshell exits 127 before its deletion loop: the reset reported success and deleted nothing. The replay tests ran the block under bash only, so they stayed green. Rewrite the block as POSIX shell: explicit directory names instead of brace expansion, and "find <dir> -mindepth 1 -maxdepth 1 ! -name .gitkeep -exec rm -rf -- {} +", which lists hidden entries and nested folders without any glob setting. The replay tests now run the block under every shell present (bash, zsh, sh, dash) and guard against bashisms returning. The zsh and dash subtests fail on master. * changelog: place the #699 entry under [Unreleased] Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> --------- Co-authored-by: Mads Lorentzen <madslorentzen_17@hotmail.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
3 files changed
+59
-10
No files matched your search
@@ -255,25 +255,20 @@ Leave the rest of the file intact: the portal-CLI and WebSearch-fallback explana
|
||||
|
||||
### Documents reset
|
||||
|
||||
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.
|
||||
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 block only after the confirmation above. It is POSIX shell on purpose: the Bash tool runs whatever shell the user has, which is zsh on macOS, and a bash-only builtin such as `shopt` fails there with `command not found` before anything is deleted. `find` lists hidden entries and nested folders without any glob setting. 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
|
||||
(
|
||||
set -e
|
||||
for directory in documents documents/{cv,linkedin,diplomas,references,projects,postings,applications}; do
|
||||
for directory in documents documents/cv documents/linkedin documents/diplomas documents/references documents/projects documents/postings documents/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
|
||||
[ -d "$directory" ] || continue
|
||||
find "$directory" -mindepth 1 -maxdepth 1 ! -name .gitkeep -exec rm -rf -- {} +
|
||||
done
|
||||
)
|
||||
```
|
||||
|
||||
@@ -341,6 +341,19 @@ per-file diff commands.
|
||||
canonical identity link, which 404s in a browser, and the employer page is `applyUrl` (search) or
|
||||
the `Apply:` line of `detail`.
|
||||
|
||||
- **`/reset documents` deletes the documents again under zsh** (#699, `.claude/commands/reset.md`
|
||||
Step 3, `tests/test_reset_documents.py`) - the block merged in #580 used the bash-only builtin
|
||||
`shopt -s dotglob nullglob`. Claude Code's Bash tool runs the user's shell, which is zsh on
|
||||
macOS, where the line fails with `command not found: shopt`, and under `set -e` the subshell
|
||||
exits 127 before its deletion loop: the reset reported success and deleted nothing. The
|
||||
replay tests ran the block under `bash` only, so they stayed green. The block is now POSIX
|
||||
shell - explicit directory names instead of brace expansion, and
|
||||
`find <dir> -mindepth 1 -maxdepth 1 ! -name .gitkeep -exec rm -rf -- {} +`, which lists
|
||||
hidden entries and nested folders without any glob setting, skipping a folder that is
|
||||
missing instead of stopping the loop there - and the replay tests run it under every shell
|
||||
present on the machine (bash, zsh, sh, dash), with a missing-folder case and a guard that no
|
||||
bashism returns. The zsh subtests fail on master.
|
||||
|
||||
### Security
|
||||
|
||||
- **CI no longer leaves the job token in `.git/config` or expands a PR-controlled name into a
|
||||
|
||||
@@ -16,6 +16,11 @@ except ImportError: # unittest discover -s tests imports modules top-level
|
||||
COMMAND = Path(__file__).resolve().parent.parent / ".claude/commands/reset.md"
|
||||
FOLDERS = ("cv", "linkedin", "diplomas", "references", "projects", "postings", "applications")
|
||||
BASH = find_bash()
|
||||
# The block is POSIX shell by design: Claude Code's Bash tool runs the user's
|
||||
# shell, which is zsh on macOS, and the bash-only `shopt` it used to carry
|
||||
# exited 127 there before deleting anything. Replay it under every shell on
|
||||
# this machine, not only bash.
|
||||
SHELLS = [path for path in (BASH, shutil.which("zsh"), shutil.which("sh"), shutil.which("dash")) if path]
|
||||
|
||||
|
||||
def symlinks_available():
|
||||
@@ -47,6 +52,11 @@ class ResetDocuments(unittest.TestCase):
|
||||
return re.search(r"```bash\n(.*?)```", section, re.S).group(1)
|
||||
|
||||
def run_reset(self, populated):
|
||||
for shell in SHELLS:
|
||||
with self.subTest(shell=shell):
|
||||
self.run_reset_with(shell, populated)
|
||||
|
||||
def run_reset_with(self, shell, populated):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
documents = root / "documents"
|
||||
@@ -65,7 +75,7 @@ class ResetDocuments(unittest.TestCase):
|
||||
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)
|
||||
result = subprocess.run([shell, "-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")
|
||||
@@ -81,6 +91,37 @@ class ResetDocuments(unittest.TestCase):
|
||||
def test_empty_document_folders_remain_usable(self):
|
||||
self.run_reset(populated=False)
|
||||
|
||||
def test_a_missing_folder_is_skipped_and_the_rest_are_still_cleared(self):
|
||||
# `find` on a missing folder exits 1, and under `set -e` that stopped
|
||||
# the loop there, leaving every later folder uncleared; master under
|
||||
# bash skipped the folder. The block must skip it too.
|
||||
for shell in SHELLS:
|
||||
with self.subTest(shell=shell):
|
||||
with TemporaryDirectory() as directory:
|
||||
root = Path(directory)
|
||||
documents = root / "documents"
|
||||
documents.mkdir()
|
||||
(documents / "README.md").write_text("Keep.\n")
|
||||
present = [name for name in FOLDERS if name != "diplomas"]
|
||||
for name in present:
|
||||
folder = documents / name
|
||||
folder.mkdir()
|
||||
(folder / ".gitkeep").write_text("Keep.\n")
|
||||
(folder / "summary.md").write_text("A document\n")
|
||||
result = subprocess.run([shell, "-c", self.reset_script()], cwd=root, capture_output=True, text=True)
|
||||
self.assertEqual(result.returncode, 0, result.stderr)
|
||||
self.assertEqual(result.stderr, "")
|
||||
for name in present:
|
||||
self.assertEqual([path.name for path in (documents / name).iterdir()], [".gitkeep"], name)
|
||||
self.assertFalse((documents / "diplomas").exists())
|
||||
|
||||
def test_block_uses_no_bash_only_builtins(self):
|
||||
# `shopt`, `declare`, `[[` and brace expansion are what break under
|
||||
# zsh/sh; a POSIX block needs none of them.
|
||||
script = self.reset_script()
|
||||
for bashism in ("shopt", "declare ", "[[", "{cv,"):
|
||||
self.assertNotIn(bashism, script, f"the documents reset block must stay POSIX: found {bashism!r}")
|
||||
|
||||
@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)):
|
||||
|
||||
Reference in new issue
Block a user