fix(wake): #913 installer adoption gaps — _lib dep-check fail-loud + mosaic-wake.service systemd-search-path link+validate
Some checks failed
ci/woodpecker/pr/ci Pipeline was canceled

Two non-blocking wake-canon adoption gaps surfaced by the dragon-lin pilot in
the A10 component installer (wake-install.sh). Both fixes are ADDITIVE per #869:
framework-manifest.txt ownership, the install-ordering-guard, and the bash/TS
manifest parity contract are all UNCHANGED.

(a) DEP-CHECK — wake-install.sh sourced the shared framework-manifest reader
(_lib/manifest.sh) unconditionally, so an older host seed that predates that
helper aborted with a bare, obscure `source: No such file or directory`. It now
checks the library FIRST and FAILS LOUD naming the missing file + the remedy
(re-seed the framework, then retry --component wake). The dependency is genuinely
required (Gate A cannot be skipped on an enforcement path), so it fails loud
rather than degrading.

(b) SYSTEMD SEARCH PATH — wi_install copies mosaic-wake.service under mosaic
home (systemd/user/, framework-owned) but `systemctl --user` searches
~/.config/systemd/user/, so the unit was invisible and could not be
enabled/started. install now LINKS the unit into the user systemd search path
(symlink -> the mosaic-home SSOT copy, so upgrades propagate) and post-install
VALIDATES it resolves (search-path entry exists, dereferences to a readable,
well-formed unit; an opportunistic `systemctl --user cat` probe runs only behind
a guard, since the installer may run where no user manager is live). Both steps
are idempotent (a re-install neither duplicates nor breaks the link). #869
ADDITIVE: the link target lives OUTSIDE mosaic home, so it is not a
framework-manifest path — ownership of the SSOT unit stays systemd/** in the
single framework-manifest.txt authority; NO new owned path, NO second authority.

Red-first: test-wake-install.sh gains I9 (a: missing _lib/manifest.sh fails loud
naming dep + remedy, not a bare source error; positive control installs) and I10
(b: install links the unit into the search path + validate resolves; negative
control catches a not-in-path unit; idempotent re-install). Both go RED against
the prior code and GREEN after. Wake component manifest.txt bumped 0.6.6 -> 0.6.7
(VERSION metadata only).

Closes #913
Part of #892

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
This commit is contained in:
mosaic-coder
2026-07-26 07:15:43 -05:00
parent 347c1d57c1
commit 26a2de41f6
3 changed files with 231 additions and 5 deletions

View File

@@ -43,6 +43,12 @@ command -v jq >/dev/null 2>&1 || { echo "SKIP: jq not available" >&2; exit 0; }
TMP_ROOT="$(mktemp -d)"
trap 'rm -rf "$TMP_ROOT"' EXIT
# Isolation default: no group may ever touch the operator's REAL user systemd dir
# (~/.config/systemd/user). The (b/#913) search-path link step defaults there, so
# pin an isolated per-run dir here; groups that need their own re-export it.
export WAKE_SYSTEMD_USER_DIR="$TMP_ROOT/systemd-user-default"
mkdir -p "$WAKE_SYSTEMD_USER_DIR"
FAILFILE="$TMP_ROOT/failures"
: >"$FAILFILE"
pass=0
@@ -248,6 +254,67 @@ else
) && ok
fi
# ── (a #913) missing _lib/manifest.sh dependency — FAIL LOUD, not a bare source error ──
echo "== I9: dep-check — a host seed missing _lib/manifest.sh FAILS LOUD (names dep + remedy), not a bare source error =="
(
# Simulate an OLDER host seed that predates the shared manifest reader: copy the
# framework tree the installer needs, but OMIT tools/_lib/manifest.sh. Running the
# copied wake-install.sh reproduces the adoption gap on a not-yet-updated host.
FAKE="$(fresh i9-fw)"
mkdir -p "$FAKE/tools/wake" "$FAKE/tools/_lib" "$FAKE/systemd/user"
cp "$FRAMEWORK_ROOT/tools/wake/"* "$FAKE/tools/wake/" 2>/dev/null || true
# Copy _lib EXCEPT manifest.sh — the exact file older seeds lack.
for f in "$FRAMEWORK_ROOT/tools/_lib/"*; do
[ -e "$f" ] || continue
case "$(basename "$f")" in manifest.sh) continue ;; esac
cp "$f" "$FAKE/tools/_lib/" 2>/dev/null || true
done
cp "$FRAMEWORK_ROOT/systemd/user/mosaic-wake.service" "$FAKE/systemd/user/" 2>/dev/null || true
cp "$FRAMEWORK_ROOT/framework-manifest.txt" "$FAKE/" 2>/dev/null || true
[ ! -e "$FAKE/tools/_lib/manifest.sh" ] || fail_msg "I9: fixture setup — manifest.sh must be absent to simulate an older seed"
WI_FAKE="$FAKE/tools/wake/wake-install.sh"
TGT="$(fresh i9-target)"
out="$(WAKE_INSTALL_SOURCE="$FAKE" WAKE_INSTALL_TARGET="$TGT" bash "$WI_FAKE" install 2>&1)"; rc=$?
[ "$rc" -ne 0 ] || fail_msg "I9: install MUST fail when _lib/manifest.sh is missing (rc=$rc)"
echo "$out" | grep -qi 'manifest.sh' || fail_msg "I9: the failure must NAME the missing library (manifest.sh) [$out]"
echo "$out" | grep -qiE 'REMEDY|mosaic update|re-seed|framework install' \
|| fail_msg "I9: the failure must give an actionable REMEDY, not just an error [$out]"
# It must be a CLEAR fail-loud, NOT the obscure bare `source: No such file or directory`.
echo "$out" | grep -qi 'No such file or directory' \
&& fail_msg "I9: must not fail with a bare source error (obscure) [$out]"
# Positive control: with the helper present (real framework root) install succeeds.
TGT_OK="$(fresh i9-target-ok)"
WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT" WAKE_INSTALL_TARGET="$TGT_OK" \
bash "$WI" install >/dev/null 2>&1 || fail_msg "I9: with _lib/manifest.sh present the install must succeed"
) && ok
# ── (b #913) mosaic-wake.service lands IN the user systemd search path + validates ──
echo "== I10: systemd search-path — install links mosaic-wake.service into the search path; validate catches a miss; idempotent =="
(
TGT="$(fresh i10-target)"
UD="$(fresh i10-systemd)" # isolated stand-in for ~/.config/systemd/user
export WAKE_INSTALL_SOURCE="$FRAMEWORK_ROOT"
export WAKE_INSTALL_TARGET="$TGT"
export WAKE_SYSTEMD_USER_DIR="$UD"
bash "$WI" install >/dev/null 2>&1 || fail_msg "I10: install must succeed"
# (a) the unit is resolvable in the user systemd search path.
[ -e "$UD/mosaic-wake.service" ] || fail_msg "I10: mosaic-wake.service must be linked into the user systemd search path ($UD)"
out="$(bash "$WI" validate-systemd-path 2>&1)"; rc=$?
[ "$rc" -eq 0 ] || fail_msg "I10: post-install validate must confirm the unit resolves (rc=$rc) [$out]"
# (b) NEGATIVE CONTROL: not-in-search-path is CAUGHT (fail loud, names the miss).
rm -f "$UD/mosaic-wake.service"
out2="$(bash "$WI" validate-systemd-path 2>&1)"; rc2=$?
[ "$rc2" -ne 0 ] || fail_msg "I10: validate must FAIL when the unit is not in the search path (rc=$rc2) [$out2]"
echo "$out2" | grep -qi 'search path' || fail_msg "I10: validate failure must name the search-path miss [$out2]"
# (c) idempotent re-install: exactly one search-path entry, still resolvable.
bash "$WI" install >/dev/null 2>&1 || fail_msg "I10: re-run install must succeed"
n="$(find "$UD" -maxdepth 1 -name 'mosaic-wake.service' | grep -c .)"
[ "$n" -eq 1 ] || fail_msg "I10: re-install must not duplicate the search-path entry (found $n)"
bash "$WI" validate-systemd-path >/dev/null 2>&1 || fail_msg "I10: re-install must keep the unit resolvable"
) && ok
echo
if [ -s "$FAILFILE" ]; then
echo "wake install harness: FAILED ($(grep -c . "$FAILFILE") assertion(s))" >&2