From 6e9df3c64092b533a1b7130b9b89c422b241a2e9 Mon Sep 17 00:00:00 2001 From: Jason Woltje <2+jason.woltje@noreply.git.mosaicstack.dev> Date: Tue, 18 Aug 2026 07:57:37 +0000 Subject: [PATCH] fix(doctor): greenfield brain lock-in note (fred's #1301 follow-up, #1288) (#1307) Co-authored-by: Jason Woltje <2+jason.woltje@noreply.git.mosaicstack.dev> --- .../framework/tools/_scripts/mosaic-doctor | 14 +++++++++ .../tools/_scripts/test-brain-home-check.sh | 29 +++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/packages/mosaic/framework/tools/_scripts/mosaic-doctor b/packages/mosaic/framework/tools/_scripts/mosaic-doctor index b281e056..a1db3938 100755 --- a/packages/mosaic/framework/tools/_scripts/mosaic-doctor +++ b/packages/mosaic/framework/tools/_scripts/mosaic-doctor @@ -51,6 +51,8 @@ fix() { fix_count=$((fix_count + 1)); echo "[FIX] $*"; } warn_count=0 warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; } +note() { echo "[NOTE] $*"; return 0; } + pass() { if [[ $VERBOSE -eq 1 ]]; then echo "[OK] $*" @@ -288,6 +290,18 @@ check_brain_home() { brain="$(resolve_brain_home)" if [[ "$brain" == "$MOSAIC_HOME" ]]; then + # Implicit-path greenfield case (#1288 comment 23133, fred's trace): nothing + # in product code creates ~/.mosaic/fleet/agents — the first fleet write + # resolves legacy (generated-env-boundary resolves before creating) and + # then manufactures the evidence that keeps the host legacy. On a host with + # ~/.mosaic but no fleet/agents, the three operator checks all agree and all + # point the wrong way; this doctor is the only one that can disagree, so it + # must say it — as a note, not a warn: nothing is broken yet. + if [[ "$(cd "$MOSAIC_HOME" 2>/dev/null && pwd -P)" == "$HOME/.config/mosaic" \ + && -d "$HOME/.mosaic" && ! -d "$HOME/.mosaic/fleet/agents" ]]; then + note "Fleet state home: $MOSAIC_HOME (legacy). NOTE: ~/.mosaic exists but carries no fleet/agents — the first 'mosaic fleet regen' on this host locks in the legacy tree. Create ~/.mosaic/fleet/agents first to adopt the brain." + return + fi pass "Fleet state home: $MOSAIC_HOME (legacy single-tree; no brain adopted)" return fi diff --git a/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh b/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh index 20119800..8de66a2e 100755 --- a/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh +++ b/packages/mosaic/framework/tools/_scripts/test-brain-home-check.sh @@ -39,22 +39,26 @@ eval "$(extract_function check_brain_home)" ROOT=$(mktemp -d) trap 'rm -rf "$ROOT"' EXIT +# note output is neither [OK] nor [WARN] — assert it directly in the case below. run_case() { - # label, expect (ok|warn), then env assignments as arguments. + # label, expect (ok|warn|note), then env assignments as arguments. # The check runs under `env` in a subshell, so its warn() also prints a # sentinel the parent counts — a subshell counter would never be visible. local label="$1" expect="$2" shift 2 - local out warns - out=$(env "$@" bash -c "warn() { echo \"[WARN] \$*\"; }; pass() { echo \"[OK] \$*\"; return 0; }; $(extract_function resolve_brain_home); $(extract_function check_brain_home); check_brain_home" 2>&1) + local out warns notes + out=$(env "$@" bash -c "warn() { echo \"[WARN] \$*\"; }; note() { echo \"[NOTE] \$*\"; return 0; }; pass() { echo \"[OK] \$*\"; return 0; }; $(extract_function resolve_brain_home); $(extract_function check_brain_home); check_brain_home" 2>&1) warns=$(printf '%s\n' "$out" | grep -c '^\[WARN\]' || true) - if [[ "$expect" == ok && "$warns" -eq 0 ]]; then + notes=$(printf '%s\n' "$out" | grep -c '^\[NOTE\]' || true) + if [[ "$expect" == ok && "$warns" -eq 0 && "$notes" -eq 0 ]]; then echo "ok - $label" elif [[ "$expect" == warn && "$warns" -gt 0 ]]; then echo "ok - $label (warned)" + elif [[ "$expect" == note && "$notes" -gt 0 ]]; then + echo "ok - $label (noted)" else echo "output: $out" >&2 - fail "$label: expected $expect (warns=$warns)" + fail "$label: expected $expect (warns=$warns notes=$notes)" fi } @@ -105,4 +109,19 @@ chmod 700 "$ROOT/brain-clean/fleet/agents" "$ROOT/config6/fleet/agents" run_case "empty config-home agents dir alongside brain passes" ok \ MOSAIC_HOME="$ROOT/config6" HOME="$ROOT" MOSAIC_BRAIN_HOME="$ROOT/brain-clean" +# ── greenfield brain-without-agents at the default home → note (#1288) ───── +mkdir -p "$ROOT/gf-home/.config/mosaic/fleet" "$ROOT/gf-home/.mosaic" +run_case "~/.mosaic without fleet/agents at default home notes the lock-in" note \ + MOSAIC_HOME="$ROOT/gf-home/.config/mosaic" HOME="$ROOT/gf-home" + +# ── no ~/.mosaic at all at the default home → clean pass ───────────────── +mkdir -p "$ROOT/plain-home/.config/mosaic/fleet" +run_case "no ~/.mosaic at default home passes silently" ok \ + MOSAIC_HOME="$ROOT/plain-home/.config/mosaic" HOME="$ROOT/plain-home" + +# ── custom (non-default) home with a stray ~/.mosaic → still silent ────── +mkdir -p "$ROOT/custom-home/fleet/agents" "$ROOT/custom-home/.mosaic" +run_case "custom home with stray ~/.mosaic stays silent" ok \ + MOSAIC_HOME="$ROOT/custom-home" HOME="$ROOT/custom-home" + echo "ok - mosaic-doctor brain-home check"