From e9a38a02680277ce83a67d6cb8e34ae8e2430f33 Mon Sep 17 00:00:00 2001 From: Bastien Chanot Date: Tue, 14 Jul 2026 16:19:29 +0200 Subject: [PATCH] fix(memory): test covers CLAUDE.global.md guard entry; doctor asserts exact global symlink target --- CHANGELOG.md | 1 + doctor.sh | 11 ++++++++ lib/tests/curated-config-guard.test.sh | 39 ++++++++++++++------------ 3 files changed, 33 insertions(+), 18 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e60e71..b537bd1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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. diff --git a/doctor.sh b/doctor.sh index 6cd0d7e..74da787 100644 --- a/doctor.sh +++ b/doctor.sh @@ -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" diff --git a/lib/tests/curated-config-guard.test.sh b/lib/tests/curated-config-guard.test.sh index 60e888c..72245e2 100644 --- a/lib/tests/curated-config-guard.test.sh +++ b/lib/tests/curated-config-guard.test.sh @@ -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 ]