fix(memory): test covers CLAUDE.global.md guard entry; doctor asserts exact global symlink target

This commit is contained in:
Bastien Chanot
2026-07-14 16:19:29 +02:00
parent 0f23f10767
commit e9a38a0268
3 changed files with 33 additions and 18 deletions
+1
View File
@@ -7,6 +7,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/).
## [Unreleased]
### Changed
- BREAKING(layout): repo-root global memory renamed CLAUDE.md → CLAUDE.global.md; run `bash link.sh` once after pulling (doctor.sh now checks the exact target)
- graphify skill dist refreshed 0.8.45 → 0.9.6 (out-of-band `make plugin`; SKILL.md + query/extraction references updated by the generator).
- `/deploy` checklist reshaped on first-real-run feedback, in two passes: runbook steps are **one command per line, interactive-session style** (an early step opens the ssh session; later lines run on the box; local steps say "from your machine") instead of folded `ssh host "cd … && …"` one-liners — step = comment header + command lines up to the next blank line, a `@delta:` directive governs the whole block; and the checklist is now **display-only** — `NEXT.sh` is no longer written at all (throwaway artifact; `PENDING.json` + the live runbook regenerate it in any session) and every hand-back **ends the turn with the full checklist as the final text, no tool call after it** (a checklist printed above a blocking question tool was observed never reaching the user). Template `templates/deploy/PROCEDURE.md` restyled to match.
- `settings.json`: `inputNeededNotifEnabled: true` adopted (harness notification toggle); committed layout otherwise unchanged.
+11
View File
@@ -63,6 +63,17 @@ check_symlink() {
}
check_symlink "CLAUDE.md"
# check_symlink only asserts the canonical path lands inside $REPO — after a
# `git pull` without `link.sh`, ~/.claude/CLAUDE.md can still resolve inside
# $REPO but at the wrong file (the 29-line project CLAUDE.md instead of
# CLAUDE.global.md), passing green while the global doctrine is silently gone.
_claude_md_target=$(readlink "$HOME/.claude/CLAUDE.md" 2>/dev/null || true)
if [ "$_claude_md_target" != "$REPO/CLAUDE.global.md" ]; then
# shellcheck disable=SC2088 # literal label, not a tilde-expansion attempt
warn "~/.claude/CLAUDE.md points to $_claude_md_target — expected \
$REPO/CLAUDE.global.md; run: bash link.sh"
fi
unset _claude_md_target
check_symlink "settings.json"
check_symlink "agents"
check_symlink "skills"
+21 -18
View File
@@ -6,7 +6,7 @@
# single-occurrence + column-0 closing brace) so drift in install-plugins.sh
# propagates into this test instead of testing a stale copy. GUARDED_CONFIGS,
# CFG_SNAPSHOT, REPO and an info() stub are defined here — the array literal
# at install-plugins.sh:41 is outside the extracted range.
# at install-plugins.sh:43-44 is outside the extracted range.
set -u
INSTALL_SH="$(cd "$(dirname "$0")/../.." && pwd)/install-plugins.sh"
pass=0; fail=0
@@ -19,13 +19,14 @@ awk '/^restore_curated_configs\(\) \{/,/^\}/' "$INSTALL_SH" > "$SUT"
REPO="$(mktemp -d)"
CFG_SNAPSHOT="$(mktemp -d)"
EXPECT="$(mktemp -d)" # our own reference copy — independent of CFG_SNAPSHOT (SUT rm -rf's it)
GUARDED_CONFIGS=("CLAUDE.md" ".claude/settings.json" "settings.json")
GUARDED_CONFIGS=("CLAUDE.md" "CLAUDE.global.md" ".claude/settings.json" "settings.json")
info() { :; } # stub — extracted body calls info(), irrelevant to the assertions
mkdir -p "$REPO/.claude"
printf 'CLAUDE original\n' > "$REPO/CLAUDE.md"
printf '{"a":1}\n' > "$REPO/.claude/settings.json"
printf '{"b":2}\n' > "$REPO/settings.json"
printf 'CLAUDE original\n' > "$REPO/CLAUDE.md"
printf 'CLAUDE.global original\n' > "$REPO/CLAUDE.global.md"
printf '{"a":1}\n' > "$REPO/.claude/settings.json"
printf '{"b":2}\n' > "$REPO/settings.json"
for f in "${GUARDED_CONFIGS[@]}"; do
mkdir -p "$CFG_SNAPSHOT/$(dirname "$f")" "$EXPECT/$(dirname "$f")"
@@ -33,7 +34,7 @@ for f in "${GUARDED_CONFIGS[@]}"; do
cp "$REPO/$f" "$EXPECT/$f"
done
# simulate installer drift: mutate ONE guarded file, leave the other two alone
# simulate installer drift: mutate ONE guarded file, leave the other three alone
printf 'CLAUDE CLOBBERED BY INSTALLER\n' > "$REPO/CLAUDE.md"
# shellcheck source=/dev/null
@@ -42,20 +43,22 @@ restore_curated_configs
cmp -s "$REPO/CLAUDE.md" "$EXPECT/CLAUDE.md"
check T1-mutated-file-restored "$?" 0
cmp -s "$REPO/CLAUDE.global.md" "$EXPECT/CLAUDE.global.md"
check T2-untouched-global-md-unchanged "$?" 0
cmp -s "$REPO/.claude/settings.json" "$EXPECT/.claude/settings.json"
check T2-untouched-local-settings-unchanged "$?" 0
check T3-untouched-local-settings-unchanged "$?" 0
cmp -s "$REPO/settings.json" "$EXPECT/settings.json"
check T3-untouched-settings-unchanged "$?" 0
if [ -d "$CFG_SNAPSHOT" ]; then r4=present; else r4=gone; fi
check T4-snapshot-dir-removed "$r4" gone
check T4-untouched-settings-unchanged "$?" 0
if [ -d "$CFG_SNAPSHOT" ]; then r5=present; else r5=gone; fi
check T5-snapshot-dir-removed "$r5" gone
# --- T5: mktemp failure -> fail-closed (install-plugins.sh, the header block
# --- T6: mktemp failure -> fail-closed (install-plugins.sh, the header block
# that builds CFG_SNAPSHOT) — refuses to run unguarded instead of warning and
# continuing. Extracted with a WIDER range than the SUT above: this logic
# lives in the top-level if/else, outside restore_curated_configs().
SUT2="$(mktemp)"
awk '/^GUARDED_CONFIGS=/,/^fi$/' "$INSTALL_SH" > "$SUT2"
ERR5="$(mktemp)"
ERR6="$(mktemp)"
(
# shellcheck disable=SC2329 # invoked indirectly by the sourced snippet below
mktemp() { return 1; } # force the header's CFG_SNAPSHOT creation to fail
@@ -68,11 +71,11 @@ ERR5="$(mktemp)"
REPO="$(command mktemp -d)"
# shellcheck source=/dev/null
source "$SUT2"
) >/dev/null 2>"$ERR5"
rc5=$?
check T5-mktemp-failure-aborts "$rc5" 1
if grep -qi 'mktemp failed' "$ERR5"; then r5msg=yes; else r5msg=no; fi
check T5-mktemp-failure-loud "$r5msg" yes
rm -f "$ERR5" "$SUT2"
) >/dev/null 2>"$ERR6"
rc6=$?
check T6-mktemp-failure-aborts "$rc6" 1
if grep -qi 'mktemp failed' "$ERR6"; then r6msg=yes; else r6msg=no; fi
check T6-mktemp-failure-loud "$r6msg" yes
rm -f "$ERR6" "$SUT2"
printf 'PASS=%s FAIL=%s\n' "$pass" "$fail"; [ "$fail" -eq 0 ]