diff --git a/scripts/test/run.sh b/scripts/test/run.sh index e4f52ec..82c846a 100755 --- a/scripts/test/run.sh +++ b/scripts/test/run.sh @@ -189,8 +189,19 @@ if [ -z "${MVP_TEMPLATE_LOCAL:-}" ] && [ -z "${MVP_TEMPLATE_REPO:-}" ]; then echo "Template source: $REPO_ROOT_DEFAULT working tree (git-visible files)" echo " (override with MVP_TEMPLATE_LOCAL= or MVP_TEMPLATE_REPO=)" else - echo "WARN: could not export the working tree for smoke tests;" >&2 - echo " falling back to the network clone of the published template." >&2 + # Fail LOUD, do not fall back to the network clone: observed in a + # derived project's CI, a transient tar failure here degraded into + # cloning the PUBLISHED template — the run then tested the wrong + # tree and drowned the real (one-line) error under 30 misleading + # naming-assertion fails. An export failure means the environment + # is broken; nothing this run could report would be trustworthy. + echo "ERROR: could not export the working tree for smoke tests" >&2 + echo " (git ls-files | tar pipeline failed; see tar errors above)." >&2 + echo " Refusing to fall back to a network clone of the published" >&2 + echo " template: that would test upstream main, not this tree." >&2 + echo " To test a different source explicitly, set MVP_TEMPLATE_LOCAL" >&2 + echo " or MVP_TEMPLATE_REPO." >&2 + exit 2 fi fi fi diff --git a/scripts/test/tests/integration/adopted-host-sync/_fixtures/legacy-update-from-template.sh b/scripts/test/tests/integration/adopted-host-sync/_fixtures/legacy-update-from-template.sh new file mode 100644 index 0000000..e8e4423 --- /dev/null +++ b/scripts/test/tests/integration/adopted-host-sync/_fixtures/legacy-update-from-template.sh @@ -0,0 +1,225 @@ +#!/usr/bin/env bash +# shellcheck source-path=SCRIPTDIR +# +# update-from-template.sh — pull updates from the llm-wiki template repo into +# an existing project, without touching project-specific content. +# +# Usage: +# ./scripts/update-from-template.sh [--dry-run] [--template-url=] +# +# Flags: +# --dry-run Print what would change without writing anything. +# --template-url Override the template remote URL. Defaults to the +# crcresearch/llm-wiki-memory-template repo. Pass this only if +# your org maintains a fork. +# +# What gets updated: +# The active file set is assembled by lw_manifest_assemble_active_files +# from scripts/lib/template-manifest.sh, which is the single source of +# truth: TEMPLATE_SHARED_INFRA always syncs; TEMPLATE_OVERLAY_CLAUDE and +# TEMPLATE_OVERLAY_CURSOR activate based on .claude/ and .cursor/ presence +# on disk. {{REPO_NAME}} substitution targets are listed in +# TEMPLATE_SUBSTITUTE_FILES. To add or remove a synced file, edit the +# manifest and nothing else. +# +# What does NOT get touched (project-specific): +# CLAUDE.md, .gitignore, .claude/settings.json (TEMPLATE_HOST_OWNED: the +# template defines the operation type but the host owns the content) +# README.md, .cursorrules (project's own) +# .claude/settings.local.json (per-user, gitignored) +# .claude/hooks/ (per-machine, opt-in) +# wiki/.wiki/ (separate git sub-repo) +# Any file under your project's source tree +# +# .gitignore note: earlier versions of this script synced .gitignore as part +# of the former always-synced list. It is now host-owned. The advisory in the post-sync +# report flags a divergence if the host's .gitignore differs from the +# template's; back-port manually if you want the new ignore rule. +# +# After each successful run, an entry is appended to .llm-wiki-template-log.md +# at the repo root, recording the template commit SHA and the files changed. +# Push: this script does NOT push. It stages changes locally (unless --dry-run). +# + +set -euo pipefail + +DRY_RUN=false +TEMPLATE_URL="git@github.com:crcresearch/llm-wiki-memory-template.git" + +while [[ $# -gt 0 ]]; do + case $1 in + --dry-run) DRY_RUN=true; shift ;; + --template-url=*) TEMPLATE_URL="${1#*=}"; shift ;; + -h|--help) + sed -n '2,/^$/p' "$0" | sed 's/^# \?//' + exit 0 + ;; + *) echo "Unknown option: $1" >&2; exit 1 ;; + esac +done + +# --- Load shared library + template manifest --- +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# shellcheck source=lib/common.sh +source "$HERE/lib/common.sh" +# shellcheck source=lib/template-manifest.sh +source "$HERE/lib/template-manifest.sh" + +REPO_ROOT=$(lw_repo_root) +# Post-clone: the authoritative name is the on-disk wiki, not the clone-dir +# basename (F1/F2). A fork or renamed clone makes the basename wrong; the +# committed wiki/.wiki is the only correct source. Fails loud if the wiki +# is absent, which is correct here: this script is for already-instantiated +# projects. +REPO_NAME=$(lw_discover_wiki_name "$REPO_ROOT") +cd "$REPO_ROOT" + +# --- Ensure 'template' remote exists and points at the expected repo --- +# lw_ensure_remote adds it when absent and, when present, fails loud if it +# points at a different repo rather than silently fetching from the wrong +# place (F6). Pass --template-url to point at an org fork. +lw_ensure_remote template "$TEMPLATE_URL" + +# Detect the template's default branch instead of assuming 'main' (F5). +TEMPLATE_BRANCH=$(lw_default_branch template) || TEMPLATE_BRANCH=main +echo "Fetching template@${TEMPLATE_BRANCH} ..." +git fetch --quiet template "$TEMPLATE_BRANCH" + +TEMPLATE_REF="template/$TEMPLATE_BRANCH" +TEMPLATE_SHA=$(git rev-parse --short "$TEMPLATE_REF") +echo "Template HEAD: $TEMPLATE_SHA" + +# --- Build the file list --- +# All path enumeration lives in scripts/lib/template-manifest.sh. The +# accessor returns SHARED_INFRA always plus overlay arrays based on the +# host's .claude/ and .cursor/ presence (detection mode: empty agent arg, +# repo_root populated). Bash 3.2 portable: while read instead of mapfile. +FILES=() +while IFS= read -r _path; do + FILES+=("$_path") +done < <(lw_manifest_assemble_active_files "$REPO_ROOT" "") + +# --- Diff and apply --- +CHANGED=() +SKIPPED=() +MISSING_IN_TEMPLATE=() + +for f in "${FILES[@]}"; do + # Does the file exist in the template's default branch? + if ! git cat-file -e "$TEMPLATE_REF:$f" 2>/dev/null; then + MISSING_IN_TEMPLATE+=("$f") + continue + fi + + # Materialize the template version to a temp file so the byte-exact + # comparison preserves trailing newlines. Using a bash variable + sha256sum + # via pipe would strip the trailing \n and produce false "changed" reports. + TEMPLATE_TMP=$(mktemp) + git show "$TEMPLATE_REF:$f" > "$TEMPLATE_TMP" + if lw_manifest_needs_substitution "$f"; then + # GNU sed: -i without backup. macOS sed needs -i ''. Use a portable + # form via the .bak suffix and clean up after. + sed -i.bak "s|{{REPO_NAME}}|$REPO_NAME|g" "$TEMPLATE_TMP" + rm -f "${TEMPLATE_TMP}.bak" + fi + + TEMPLATE_HASH=$(lw_sha256 "$TEMPLATE_TMP") + if [[ -f "$f" ]]; then + LOCAL_HASH=$(lw_sha256 "$f") + else + LOCAL_HASH="" + fi + + if [[ "$LOCAL_HASH" == "$TEMPLATE_HASH" ]]; then + SKIPPED+=("$f") + rm -f "$TEMPLATE_TMP" + continue + fi + + CHANGED+=("$f") + if ! $DRY_RUN; then + mkdir -p "$(dirname "$f")" + mv "$TEMPLATE_TMP" "$f" + # Preserve executable bit for shell scripts that should be executable. + case "$f" in + *.sh) chmod +x "$f" ;; + esac + else + rm -f "$TEMPLATE_TMP" + fi +done + +# --- Report --- +echo "" +echo "================ update-from-template ================" +echo "Repo: $REPO_ROOT" +echo "Template: $TEMPLATE_URL @ $TEMPLATE_SHA" +echo "Mode: $($DRY_RUN && echo 'DRY RUN' || echo 'APPLIED')" +echo "------------------------------------------------------" +echo "Changed (${#CHANGED[@]}):" +if [[ ${#CHANGED[@]} -gt 0 ]]; then + for f in "${CHANGED[@]}"; do echo " + $f"; done +fi +echo "" +echo "Skipped, identical to template (${#SKIPPED[@]}):" +if [[ ${#SKIPPED[@]} -gt 0 ]]; then + for f in "${SKIPPED[@]}"; do echo " = $f"; done +fi +if [[ ${#MISSING_IN_TEMPLATE[@]} -gt 0 ]]; then + echo "" + echo "Not present in ${TEMPLATE_REF} (${#MISSING_IN_TEMPLATE[@]}):" + for f in "${MISSING_IN_TEMPLATE[@]}"; do echo " ? $f"; done +fi + +# .gitignore advisory: the file is host-owned (TEMPLATE_HOST_OWNED). Earlier +# versions of this script synced it as part of the former always-synced list; +# the host now owns the content. If the template's .gitignore has rules the +# host lacks, back-port them manually. Probe TEMPLATE_REF directly because +# .gitignore is absent from the assembled FILES list. +if git cat-file -e "$TEMPLATE_REF:.gitignore" 2>/dev/null && [[ -f "$REPO_ROOT/.gitignore" ]]; then + TEMPLATE_GI=$(mktemp) + git show "$TEMPLATE_REF:.gitignore" > "$TEMPLATE_GI" + if ! cmp -s "$REPO_ROOT/.gitignore" "$TEMPLATE_GI"; then + echo "" + echo "Advisory: .gitignore is host-owned (was synced in earlier versions)." + echo " The template's .gitignore differs from yours. This script" + echo " will not overwrite it. Review with:" + echo " git diff $TEMPLATE_REF -- .gitignore" + echo " Adopt installs an append-only wiki sub-repo rule via the" + echo " .gitignore grant; see scripts/adopt.sh." + fi + rm -f "$TEMPLATE_GI" +fi +echo "======================================================" + +if $DRY_RUN; then + echo "" + echo "Dry run only. Re-run without --dry-run to apply." + exit 0 +fi + +if [[ ${#CHANGED[@]} -eq 0 ]]; then + echo "" + echo "Nothing to apply. Repo is in sync with ${TEMPLATE_REF} @ $TEMPLATE_SHA." + exit 0 +fi + +# --- Append log entry --- +LOG_FILE="$REPO_ROOT/.llm-wiki-template-log.md" +TODAY=$(date +%Y-%m-%d) +# shellcheck disable=SC2094 # the -f tests only probe existence; the block appends +{ + [[ -f "$LOG_FILE" ]] || echo "# llm-wiki template sync log" + [[ -f "$LOG_FILE" ]] || echo "" + echo "## [$TODAY] pulled template @${TEMPLATE_SHA} - ${#CHANGED[@]} file(s) updated" + for f in "${CHANGED[@]}"; do echo "- $f"; done + echo "" +} >> "$LOG_FILE" + +echo "" +echo "Logged in .llm-wiki-template-log.md." +echo "" +echo "Next steps:" +echo " Review the changes: git diff" +echo " Stage and commit: git add -A && git commit -m \"chore: pull llm-wiki template @${TEMPLATE_SHA}\"" +echo " (Do NOT push unless the team has reviewed.)" diff --git a/scripts/test/tests/integration/adopted-host-sync/assertions.sh b/scripts/test/tests/integration/adopted-host-sync/assertions.sh new file mode 100644 index 0000000..b1e6b9c --- /dev/null +++ b/scripts/test/tests/integration/adopted-host-sync/assertions.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Assertions: an adopted host's OWN sync tooling must work (#74) and its +# wiki must carry the stamped Edge-Types page (#75). +# +# #74: update-from-template.sh and check-template-version.sh source +# scripts/lib/template-manifest.sh from the HOST tree. If adopt does not +# ship the manifest itself, both die at their source line — the exact +# failure this fixture pins. The template remote for the host-side runs +# is the sandbox-staged template repo (hermetic; no network). +# +# #75: init-wiki stamps wiki/*.md.template from the HOST's wiki/ dir under +# nullglob, so a missing Edge-Types.md.template is a silent no-op and the +# stamped SCHEMA's [Edge-Types] links all dangle. + +STAGE="$SANDBOX/adopted-host-sync" +T="$STAGE/template" +H="$STAGE/host" + +# patch.sh declines (removes $STAGE) when no template source is available +# (offline, or derived checkout per issue #15). +if [ ! -d "$H" ]; then + skip "adopted-host-sync assertions" "no template clone available (offline, or run inside a derived project)" + return 0 2>/dev/null || true +fi + +assert "adopt --apply exited 0" \ + "[ \"\$(cat '$H.adopt-rc' 2>/dev/null)\" = '0' ]" + +# --- #74: the manifest ships, and the host-side sync tooling runs ----------- +assert "host has scripts/lib/template-manifest.sh" \ + "[ -f '$H/scripts/lib/template-manifest.sh' ]" + +update_rc=0 +( cd "$H" && ./scripts/update-from-template.sh --dry-run --template-url="$T" ) \ + > "$STAGE/update-dry-run.out" 2>&1 || update_rc=$? +assert_eq "host update-from-template.sh --dry-run exit status" "0" "$update_rc" +# The full banner line only prints after the manifest sourced and the +# template fetch succeeded; the pre-fix crash output does not contain it. +assert "host update dry-run printed its report banner" \ + "grep -qF '================ update-from-template ================' '$STAGE/update-dry-run.out'" + +check_rc=0 +( cd "$H" && ./scripts/check-template-version.sh --template-url="$T" ) \ + > "$STAGE/check-version.out" 2>&1 || check_rc=$? +# rc 0 = in sync, rc 1 = drift: both are completed runs. The pre-fix +# source crash also exits 1, so the banner assertion below is the one +# that separates "reported drift" from "died before reporting". +assert "host check-template-version completed (rc <= 1)" \ + "[ '$check_rc' -le 1 ]" +assert "host check-template-version printed its report banner" \ + "grep -qF '================ check-template-version ================' '$STAGE/check-version.out'" + +# --- #75: Edge-Types template ships and gets stamped into the wiki ---------- +assert "host received wiki/Edge-Types.md.template" \ + "[ -f '$H/wiki/Edge-Types.md.template' ]" +EDGE="$H/wiki/sync-host.wiki/Edge-Types.md" +assert "adopted wiki has Edge-Types.md (stamped by init-wiki)" \ + "[ -f '$EDGE' ]" +assert "stamped Edge-Types.md has the repo name substituted" \ + "grep -qF 'sync-host' '$EDGE'" +assert "stamped Edge-Types.md has no {{REPO_NAME}} placeholder left" \ + "! grep -qF '{{REPO_NAME}}' '$EDGE'" + +# --- #74 migration: legacy host (adopted pre-fix, no manifest on disk) ------ +# Its updater must BOOTSTRAP the manifest from the template ref instead of +# dying at the source line, then install the real file via the normal sync +# loop (apply mode). The Edge-Types PAGE is deliberately not asserted here: +# update syncs files, it does not stamp wikis — legacy wikis gain the page +# on their next init-wiki run. +H2="$STAGE/legacy-host" +assert "legacy host staged without the manifest" \ + "[ -d '$H2' ] && [ ! -f '$H2/scripts/lib/template-manifest.sh' ]" +legacy_rc=0 +( cd "$H2" && ./scripts/update-from-template.sh --template-url="$T" ) \ + > "$STAGE/legacy-update.out" 2>&1 || legacy_rc=$? +assert_eq "legacy host update-from-template.sh exit status" "0" "$legacy_rc" +assert "legacy update printed its report banner" \ + "grep -qF '================ update-from-template ================' '$STAGE/legacy-update.out'" +assert "legacy update announced the manifest bootstrap" \ + "grep -qi 'bootstrap' '$STAGE/legacy-update.out'" +assert "legacy host now has scripts/lib/template-manifest.sh on disk" \ + "[ -f '$H2/scripts/lib/template-manifest.sh' ]" +assert "restored manifest is byte-equal to the template's" \ + "cmp -s '$H2/scripts/lib/template-manifest.sh' '$T/scripts/lib/template-manifest.sh'" +assert "legacy host got wiki/Edge-Types.md.template back too" \ + "[ -f '$H2/wiki/Edge-Types.md.template' ]" +legacy_check_rc=0 +( cd "$H2" && ./scripts/check-template-version.sh --template-url="$T" ) \ + > "$STAGE/legacy-check.out" 2>&1 || legacy_check_rc=$? +assert "legacy host check-template-version completed (rc <= 1)" \ + "[ '$legacy_check_rc' -le 1 ]" +assert "legacy check printed its report banner" \ + "grep -qF '================ check-template-version ================' '$STAGE/legacy-check.out'" + +# --- #75: host whose wiki sub-repo already existed at adopt time ------------ +# adopt's init-wiki dispatch short-circuits on the existing wiki, so the +# already-present branch must stamp the MISSING template pages itself — +# and must not touch the host's pre-existing wiki content. +H3="$STAGE/prewiki-host" +assert "prewiki adopt --apply exited 0" \ + "[ \"\$(cat '$H3.adopt-rc' 2>/dev/null)\" = '0' ]" +EDGE3="$H3/wiki/prewiki-host.wiki/Edge-Types.md" +assert "prewiki host: Edge-Types.md stamped despite skipped init-wiki" \ + "[ -f '$EDGE3' ]" +assert "prewiki host: stamped page has the repo name substituted" \ + "grep -qF 'prewiki-host' '$EDGE3'" +assert "prewiki host: pre-existing SCHEMA content untouched" \ + "grep -qF 'PRE_EXISTING_SCHEMA_SENTINEL' '$H3/wiki/prewiki-host.wiki/SCHEMA_prewiki-host.md'" +assert "prewiki host: pre-existing Home content untouched" \ + "grep -qF 'PRE_EXISTING_HOME_SENTINEL' '$H3/wiki/prewiki-host.wiki/Home_prewiki-host.md'" + +# --- #74 review: GENUINE legacy host (pre-fix updater + no manifest) -------- +# The migration under test must not depend on host-side tooling: the old +# updater is the broken part, so the repair is a re-adopt from a current +# template clone. +H4="$STAGE/oldhost" +assert "old host adopt --apply exited 0" \ + "[ \"\$(cat '$H4.adopt-rc' 2>/dev/null)\" = '0' ]" + +# The constraint, observed: the pre-fix updater dies at its source line +# BEFORE any fetch. No bootstrap inside the updater can reach this host. +old_rc=0 +( cd "$H4" && ./scripts/update-from-template.sh --dry-run --template-url="$T" ) \ + > "$STAGE/oldhost-pre.out" 2>&1 || old_rc=$? +assert "pre-fix updater fails without the manifest (the #74 constraint)" \ + "[ '$old_rc' -ne 0 ]" +assert "pre-fix updater failure names template-manifest.sh" \ + "grep -q 'template-manifest.sh' '$STAGE/oldhost-pre.out'" + +# The documented migration is a re-adopt. Without --force the composite +# already-adopted detector refuses — correct, and why the doc says --force. +noforce_rc=0 +bash "$T/scripts/adopt.sh" --target="$H4" --apply --agent=claude-code \ + > "$STAGE/oldhost-noforce.out" 2>&1 || noforce_rc=$? +assert "re-adopt without --force is refused on the adopted host" \ + "[ '$noforce_rc' -ne 0 ]" + +# Migration: adopt --apply --force ADDs the manifest and the Edge-Types +# template, and stamps the missing wiki page (already-present branch). +force_rc=0 +bash "$T/scripts/adopt.sh" --target="$H4" --apply --force --agent=claude-code \ + > "$STAGE/oldhost-force.out" 2>&1 || force_rc=$? +assert_eq "migration adopt --apply --force exit status" "0" "$force_rc" +assert "migration landed scripts/lib/template-manifest.sh" \ + "[ -f '$H4/scripts/lib/template-manifest.sh' ]" +assert "migration landed wiki/Edge-Types.md.template" \ + "[ -f '$H4/wiki/Edge-Types.md.template' ]" +assert "migration stamped Edge-Types.md into the existing wiki" \ + "[ -f '$H4/wiki/oldhost.wiki/Edge-Types.md' ]" +assert "migration left the OLD updater in place (ADD never overwrites)" \ + "! cmp -s '$H4/scripts/update-from-template.sh' '$T/scripts/update-from-template.sh'" + +# With the manifest delivered, the OLD updater is unblocked: its source +# line succeeds and its own sync loop brings the host current — including +# replacing itself with the current updater. +post_rc=0 +( cd "$H4" && ./scripts/update-from-template.sh --template-url="$T" ) \ + > "$STAGE/oldhost-post.out" 2>&1 || post_rc=$? +assert_eq "old updater with delivered manifest exit status" "0" "$post_rc" +assert "old updater printed its report banner" \ + "grep -qF '================ update-from-template ================' '$STAGE/oldhost-post.out'" +assert "old updater synced ITSELF to the current version" \ + "cmp -s '$H4/scripts/update-from-template.sh' '$T/scripts/update-from-template.sh'" diff --git a/scripts/test/tests/integration/adopted-host-sync/patch.sh b/scripts/test/tests/integration/adopted-host-sync/patch.sh new file mode 100644 index 0000000..5929072 --- /dev/null +++ b/scripts/test/tests/integration/adopted-host-sync/patch.sh @@ -0,0 +1,163 @@ +#!/usr/bin/env bash +# Patch: stage a template copy and a virgin host, then adopt into the host. +# +# The assertions prove the ADOPTED HOST's own sync tooling works — the +# tooling adopt itself installs (#74: update-from-template.sh and +# check-template-version.sh source scripts/lib/template-manifest.sh from +# the HOST tree, so adopt must ship the manifest too) — and that the +# host's wiki carries the stamped Edge-Types page (#75: init-wiki stamps +# wiki/*.md.template from the HOST's wiki/ under nullglob, so a template +# file adopt never shipped is a silent no-op). +# +# The template side is staged via clone_template into the sandbox (a real +# git repo with one commit on a detectable default branch) rather than +# pointing at this checkout directly: CI checkouts are detached-HEAD and +# PR runs may lack a local main, which would break the host-side +# `git fetch template ` for reasons unrelated to #74. +# +# Inputs: SANDBOX env var. lib/template.sh's clone_template. +# Effects: $SANDBOX/adopted-host-sync/{template,host}; host adopted with +# --agent=claude-code. Declines cleanly (nothing staged) when no +# template source is available (offline, or derived checkout per +# issue #15). +# +# Idempotent. + +set -uo pipefail + +HARNESS_LIB="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../../lib" && pwd)" +# shellcheck source=../../../lib/template.sh +source "$HARNESS_LIB/template.sh" + +STAGE="$SANDBOX/adopted-host-sync" +T="$STAGE/template" +H="$STAGE/host" +mkdir -p "$STAGE" + +if [ -d "$T" ]; then + echo " adopted-host-sync template already staged at $T (idempotent re-run)." +elif ! clone_template "$T"; then + echo " adopted-host-sync assertions will skip: no template clone available." >&2 + rm -rf "$STAGE" + exit 0 +fi + +if [ ! -d "$H/.git" ]; then + git init -q "$H" + git -C "$H" remote add origin "https://github.com/acme/sync-host.git" + echo "# Sync Host" > "$H/README.md" + git -C "$H" -c user.email=t@x.invalid -c user.name=t add -A + git -C "$H" -c user.email=t@x.invalid -c user.name=t commit -qm "initial" +fi + +if [ ! -f "$H/llm-wiki.md" ]; then + rc=0 + bash "$T/scripts/adopt.sh" --target="$H" --apply --agent=claude-code \ + >/tmp/adopted-host-sync.log 2>&1 || rc=$? + # rc sidecar outside the tree; assertions.sh asserts rc == 0. + echo "$rc" > "$H.adopt-rc" + if [ "$rc" -ne 0 ]; then + echo " WARN: adopt --apply failed (rc=$rc); the exit-status assertion will fail." >&2 + sed 's/^/ /' /tmp/adopted-host-sync.log >&2 + fi +fi + +# --- H2: manifest-less host with the CURRENT updater (#74 bootstrap path) --- +# Covers the new updater's self-heal branch: manifest missing, updater +# recent enough to bootstrap it from the template ref. NOTE this is NOT +# the genuine pre-fix combination — a real legacy host has the OLD +# updater, which dies before any fetch; that case is H4 below. +H2="$STAGE/legacy-host" +if [ ! -d "$H2/.git" ]; then + git init -q "$H2" + git -C "$H2" remote add origin "https://github.com/acme/legacy-host.git" + echo "# Legacy Host" > "$H2/README.md" + git -C "$H2" -c user.email=t@x.invalid -c user.name=t add -A + git -C "$H2" -c user.email=t@x.invalid -c user.name=t commit -qm "initial" + rc=0 + bash "$T/scripts/adopt.sh" --target="$H2" --apply --agent=claude-code \ + >/tmp/adopted-host-sync-h2.log 2>&1 || rc=$? + echo "$rc" > "$H2.adopt-rc" + if [ "$rc" -ne 0 ]; then + echo " WARN: adopt --apply failed on legacy host (rc=$rc)." >&2 + sed 's/^/ /' /tmp/adopted-host-sync-h2.log >&2 + fi + rm -f "$H2/scripts/lib/template-manifest.sh" \ + "$H2/wiki/Edge-Types.md.template" \ + "$H2/wiki/legacy-host.wiki/Edge-Types.md" +fi + +# --- H3: host whose wiki sub-repo ALREADY exists at adopt time (#75) -------- +# adopt's init-wiki dispatch short-circuits on wiki/.wiki/.git, so +# stamping must happen via the already-present branch. The pre-existing +# wiki carries sentinel content that adopt must NOT touch. The committed +# .gitignore rule keeps the wiki sub-repo out of git status so adopt's +# clean-tree guard passes (the same rule adopt's own grant installs). +H3="$STAGE/prewiki-host" +if [ ! -d "$H3/.git" ]; then + git init -q "$H3" + git -C "$H3" remote add origin "https://github.com/acme/prewiki-host.git" + echo "# Prewiki Host" > "$H3/README.md" + printf 'wiki/*.wiki/\n' > "$H3/.gitignore" + mkdir -p "$H3/wiki/prewiki-host.wiki" + git init -q "$H3/wiki/prewiki-host.wiki" + printf '# SCHEMA\n\nPRE_EXISTING_SCHEMA_SENTINEL\n' \ + > "$H3/wiki/prewiki-host.wiki/SCHEMA_prewiki-host.md" + printf '# Home\n\nPRE_EXISTING_HOME_SENTINEL\n' \ + > "$H3/wiki/prewiki-host.wiki/Home_prewiki-host.md" + git -C "$H3" -c user.email=t@x.invalid -c user.name=t add -A + git -C "$H3" -c user.email=t@x.invalid -c user.name=t commit -qm "initial" + rc=0 + bash "$T/scripts/adopt.sh" --target="$H3" --apply --agent=claude-code \ + >/tmp/adopted-host-sync-h3.log 2>&1 || rc=$? + echo "$rc" > "$H3.adopt-rc" + if [ "$rc" -ne 0 ]; then + echo " WARN: adopt --apply failed on prewiki host (rc=$rc)." >&2 + sed 's/^/ /' /tmp/adopted-host-sync-h3.log >&2 + fi +fi + +# --- H4: GENUINE legacy host — pre-fix updater + no manifest (#74 review) --- +# H2 exercises the NEW updater's bootstrap; a real pre-fix host runs the +# OLD updater, which sources the manifest at its line 66 BEFORE any fetch, +# so no logic delivered by an update can ever reach it. Constructed +# hermetically from the pinned verbatim artifact +# _fixtures/legacy-update-from-template.sh (byte copy of +# origin/main @ 0eec87c; CI checkouts are depth-1, so extracting the old +# version from git history at run time is not an option). +# +# assertions.sh first OBSERVES the constraint (old updater dies naming the +# manifest), then runs the documented migration — re-adopt --apply --force +# from the template, no host-side tooling required — and finally proves +# the old updater, unblocked by the delivered manifest, syncs itself up +# to the current version. +H4="$STAGE/oldhost" +FIXTURES_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/_fixtures" && pwd)" +if [ ! -d "$H4/.git" ]; then + git init -q "$H4" + git -C "$H4" remote add origin "https://github.com/acme/oldhost.git" + echo "# Old Host" > "$H4/README.md" + git -C "$H4" -c user.email=t@x.invalid -c user.name=t add -A + git -C "$H4" -c user.email=t@x.invalid -c user.name=t commit -qm "initial" + rc=0 + bash "$T/scripts/adopt.sh" --target="$H4" --apply --agent=claude-code \ + >/tmp/adopted-host-sync-h4.log 2>&1 || rc=$? + echo "$rc" > "$H4.adopt-rc" + if [ "$rc" -ne 0 ]; then + echo " WARN: adopt --apply failed on old host (rc=$rc)." >&2 + sed 's/^/ /' /tmp/adopted-host-sync-h4.log >&2 + fi + # Legacy surgery: the pre-fix updater, no manifest, no Edge-Types + # artifacts — the exact on-disk state a pre-#74 adoption produced. + cp "$FIXTURES_DIR/legacy-update-from-template.sh" "$H4/scripts/update-from-template.sh" + chmod +x "$H4/scripts/update-from-template.sh" + rm -f "$H4/scripts/lib/template-manifest.sh" \ + "$H4/wiki/Edge-Types.md.template" \ + "$H4/wiki/oldhost.wiki/Edge-Types.md" + # The migration re-adopt requires a clean host tree; legacy hosts have + # long since committed their adoption, so commit the simulated state. + git -C "$H4" -c user.email=t@x.invalid -c user.name=t add -A + git -C "$H4" -c user.email=t@x.invalid -c user.name=t commit -qm "simulate pre-#74 adoption state" +fi + +echo " adopted-host-sync patch applied: template at $T, hosts at $H, $H2, $H3, $H4." diff --git a/scripts/test/tests/integration/cursor-setup/assertions.sh b/scripts/test/tests/integration/cursor-setup/assertions.sh index 274e45e..55746bb 100644 --- a/scripts/test/tests/integration/cursor-setup/assertions.sh +++ b/scripts/test/tests/integration/cursor-setup/assertions.sh @@ -22,6 +22,14 @@ # root from the current directory, so each run cds into a fixture. STAGE="$SANDBOX/cursor-setup" + +# patch.sh declines to stage (no $STAGE dir) when the checkout is not the +# template — derived projects lack .cursorrules.template and .cursor/rules. +if [ ! -d "$STAGE" ]; then + skip "cursor-setup assertions" "not a template checkout (derived project; cursor fixtures unavailable)" + return 0 2>/dev/null || true +fi + # assertions.sh is sourced by run.sh, so $HERE = scripts/test/; two up = repo root. REPO_ROOT_CU="$(cd "$HERE/../.." && pwd)" SETUP="$REPO_ROOT_CU/wiki/agents/cursor/setup.sh" diff --git a/scripts/test/tests/integration/cursor-setup/patch.sh b/scripts/test/tests/integration/cursor-setup/patch.sh index 4bf0d17..e7e4437 100644 --- a/scripts/test/tests/integration/cursor-setup/patch.sh +++ b/scripts/test/tests/integration/cursor-setup/patch.sh @@ -16,10 +16,20 @@ set -uo pipefail STAGE="$SANDBOX/cursor-setup" -mkdir -p "$STAGE" # patch.sh is executed, so locate the real repo from this file. REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")" && git rev-parse --show-toplevel)" + +# Template-checkout guard: the fixtures need .cursorrules.template and +# .cursor/rules from the checkout, which instantiate consumes/prunes in +# derived projects (observed: 10 spurious fails in a derived project's +# CI). Decline WITHOUT creating $STAGE so assertions.sh skips cleanly. +# Discriminator matches clone_template's issue-#15 guard. +if [ ! -f "$REPO_ROOT/CLAUDE.md.template" ]; then + echo " cursor-setup: not a template checkout (derived project); declining to stage." >&2 + exit 0 +fi +mkdir -p "$STAGE" TPL_SRC="$REPO_ROOT/wiki/agents/claude-code/templates" CURSORRULES_TPL="$REPO_ROOT/.cursorrules.template" RULES_SRC="$REPO_ROOT/.cursor/rules" diff --git a/scripts/test/tests/integration/overlay-marker-migration/assertions.sh b/scripts/test/tests/integration/overlay-marker-migration/assertions.sh index bc0499d..4e3569b 100644 --- a/scripts/test/tests/integration/overlay-marker-migration/assertions.sh +++ b/scripts/test/tests/integration/overlay-marker-migration/assertions.sh @@ -71,9 +71,15 @@ assert "old-no-kg: local edits preserved" \ "grep -qF 'LOCAL_EDIT_MAINT_ABC' '$ON'" # --- cursor overlay migrates via the same shared path --- -CB="$STAGE/cur-both/CLAUDE.md" -( cd "$STAGE/cur-both" && bash "$CU_SETUP" ) >/dev/null 2>&1 -assert "cursor: legacy section wrapped in sentinels" \ - "grep -qF '' '$CB'" -assert_eq "cursor: exactly one '### Memory boundary' (no duplicate)" "1" \ - "$(count '^### Memory boundary$' "$CB")" +# patch.sh stages cur-both only when the checkout ships the cursor overlay +# (instantiate prunes it in claude-code-derived projects). +if [ ! -d "$STAGE/cur-both" ]; then + skip "overlay-marker-migration cursor leg" "cursor overlay not in this checkout (pruned in derived projects)" +else + CB="$STAGE/cur-both/CLAUDE.md" + ( cd "$STAGE/cur-both" && bash "$CU_SETUP" ) >/dev/null 2>&1 + assert "cursor: legacy section wrapped in sentinels" \ + "grep -qF '' '$CB'" + assert_eq "cursor: exactly one '### Memory boundary' (no duplicate)" "1" \ + "$(count '^### Memory boundary$' "$CB")" +fi diff --git a/scripts/test/tests/integration/overlay-marker-migration/patch.sh b/scripts/test/tests/integration/overlay-marker-migration/patch.sh index fa71e31..1d7876f 100644 --- a/scripts/test/tests/integration/overlay-marker-migration/patch.sh +++ b/scripts/test/tests/integration/overlay-marker-migration/patch.sh @@ -65,11 +65,17 @@ EOF _mkproj "$STAGE/old-both" sigil _legacy_both > "$STAGE/old-both/CLAUDE.md" -_mkproj "$STAGE/cur-both" glyph -_legacy_both > "$STAGE/cur-both/CLAUDE.md" -# cursor overlay keys off .cursor/rules presence in some flows; not required -# here (setup.sh patches CLAUDE.md regardless), but mark the overlay active. -mkdir -p "$STAGE/cur-both/.cursor/rules" +# The cursor leg drives wiki/agents/cursor/setup.sh from the checkout, +# which instantiate prunes in claude-code-derived projects. Stage it only +# when the overlay exists; assertions.sh skips the cursor asserts when +# cur-both is absent. +if [ -f "$REPO_ROOT/wiki/agents/cursor/setup.sh" ]; then + _mkproj "$STAGE/cur-both" glyph + _legacy_both > "$STAGE/cur-both/CLAUDE.md" + # cursor overlay keys off .cursor/rules presence in some flows; not required + # here (setup.sh patches CLAUDE.md regardless), but mark the overlay active. + mkdir -p "$STAGE/cur-both/.cursor/rules" +fi _mkproj "$STAGE/old-partial" sigil cat > "$STAGE/old-partial/CLAUDE.md" <<'EOF' diff --git a/scripts/test/tests/smoke/manifest-convergence/assertions.sh b/scripts/test/tests/smoke/manifest-convergence/assertions.sh index 5b60af9..0306bd4 100644 --- a/scripts/test/tests/smoke/manifest-convergence/assertions.sh +++ b/scripts/test/tests/smoke/manifest-convergence/assertions.sh @@ -173,10 +173,6 @@ while IFS= read -r _disk; do # registry of project wikis on the host; not a manifest-managed # path. wiki/WIKI-INDEX.md) continue ;; - # init-wiki may leave a one-shot Edge-Types.md.template under - # wiki/ when the host had no wiki at adopt time; consumed on next - # init-wiki run. Not in the manifest's enforce set. - wiki/Edge-Types.md.template) continue ;; esac if ! grep -qxF -- "$_disk" "$STAGE/expected-claude.txt"; then EXTRAS+=("$_disk") diff --git a/scripts/test/tests/unit/manifest-shape/assertions.sh b/scripts/test/tests/unit/manifest-shape/assertions.sh index be33e49..3912d65 100644 --- a/scripts/test/tests/unit/manifest-shape/assertions.sh +++ b/scripts/test/tests/unit/manifest-shape/assertions.sh @@ -13,6 +13,19 @@ ROOT="$SANDBOX/manifest-shape" REPO_ROOT_LIB="$(cd "$HERE/../.." && pwd)" LW_MANIFEST="$REPO_ROOT_LIB/scripts/lib/template-manifest.sh" +# Template-checkout guard: the tree invariants below (every SUBSTITUTE +# entry still contains {{REPO_NAME}}, every overlay entry exists on disk) +# hold only in the template repo. This harness ships to derived projects +# via "Use this template", where instantiate has already substituted the +# placeholders and pruned the unused overlays — there the invariants are +# legitimately false (observed: 12 spurious fails in a derived project's +# CI). Same discriminator as clone_template's issue-#15 guard: only the +# template checkout carries CLAUDE.md.template. +if [ ! -f "$REPO_ROOT_LIB/CLAUDE.md.template" ]; then + skip "manifest-shape assertions" "not a template checkout (derived project; manifest tree-invariants do not apply)" + return 0 2>/dev/null || true +fi + # Subshell sourcer; stdout is the eval result, exit status is the call's. # shellcheck source=/dev/null lw_mcall() { ( set -uo pipefail; source "$LW_MANIFEST"; eval "$1" ); }