Skip to content

Commit ea41d55

Browse files
authored
Merge pull request #91 from crcresearch/feat/manifest-sync-trees
feat(manifest): sync trees — ship the test harness to derived and adopted repos (closes #90)
2 parents 55d94d9 + e4a552f commit ea41d55

8 files changed

Lines changed: 217 additions & 0 deletions

File tree

scripts/adopt.sh

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -289,6 +289,12 @@ ADD_PATHS=()
289289
while IFS= read -r _path; do
290290
ADD_PATHS+=("$_path")
291291
done < <(lw_manifest_assemble_active_files "" "$AGENT")
292+
# Sync-tree members (TEMPLATE_SYNC_TREES, e.g. the test harness) resolved
293+
# from the template checkout on disk — dir mode, since adopt copies from
294+
# $TEMPLATE_ROOT rather than a git ref (#90).
295+
while IFS= read -r _path; do
296+
ADD_PATHS+=("$_path")
297+
done < <(lw_manifest_tree_files dir "$TEMPLATE_ROOT")
292298
for path in "${ADD_PATHS[@]}"; do
293299
src="$TEMPLATE_ROOT/$path"
294300
dst="$TARGET/$path"

scripts/check-template-version.sh

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ FILES=()
9090
while IFS= read -r _path; do
9191
FILES+=("$_path")
9292
done < <(lw_manifest_assemble_active_files "$REPO_ROOT" "")
93+
# Sync-tree members (TEMPLATE_SYNC_TREES) resolved from the template ref (#90).
94+
while IFS= read -r _path; do
95+
FILES+=("$_path")
96+
done < <(lw_manifest_tree_files ref "$TEMPLATE_REF")
9397

9498
IN_SYNC=()
9599
OUT_OF_DATE=()

scripts/lib/template-manifest.sh

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,24 @@ TEMPLATE_SHARED_INFRA=(
6565
"scripts/wiki-write-protocol/scenarios/07-livelock-retry/run.sh"
6666
"scripts/wiki-write-protocol/scenarios/08-session-start-auto-pull/run.sh"
6767
"scripts/wiki-write-protocol/scenarios/09-session-start-divergent/run.sh"
68+
# CI workflow for the test harness below. Ships at instantiation; synced
69+
# here so derived repos' CI follows the harness instead of freezing (#90).
70+
".github/workflows/test-harness.yml"
71+
)
72+
73+
# Trees the template owns end-to-end, synced as WHOLE TREES. Membership is
74+
# resolved from the sync source at run time via lw_manifest_tree_files —
75+
# the template ref for update/check, the template checkout for adopt —
76+
# never enumerated here: a static list of the harness's ~110 files would
77+
# rot on every added fixture (#90; three field cases of frozen inherited
78+
# harnesses, one with red CI for ten days).
79+
#
80+
# Contract: copy-no-delete. Files the template removed linger on hosts
81+
# until cleaned manually; files the HOST added under a tree are never
82+
# touched — deliberate, because features install their tests INTO this
83+
# tree (feature.json tests.destination = scripts/test/tests/unit/<name>/).
84+
TEMPLATE_SYNC_TREES=(
85+
"scripts/test"
6886
)
6987

7088
# Claude Code overlay files. Active when adopt is invoked with
@@ -191,6 +209,24 @@ lw_manifest_assemble_active_files() {
191209
fi
192210
}
193211

212+
# Echo the current member files of every declared sync tree, one per line.
213+
# Two modes, matching the two authoritative sources:
214+
# ref: $2 is a git ref readable from CWD (e.g. template/main); membership
215+
# from git ls-tree — what update/check sync against.
216+
# dir: $2 is a directory root (the template checkout, for adopt);
217+
# membership from find, paths relative to that root, sorted.
218+
# Silent-empty when the tree is absent from the source: the consumer's
219+
# normal missing-file reporting handles it (no special case here).
220+
lw_manifest_tree_files() {
221+
local mode="$1" src="$2" tree
222+
for tree in ${TEMPLATE_SYNC_TREES[@]+"${TEMPLATE_SYNC_TREES[@]}"}; do
223+
case "$mode" in
224+
ref) git ls-tree -r --name-only "$src" -- "$tree" 2>/dev/null ;;
225+
dir) ( cd "$src" 2>/dev/null && find "$tree" -type f 2>/dev/null | sort ) ;;
226+
esac
227+
done
228+
}
229+
194230
# Echo the HOST_OWNED operation type for $1, or empty string if not host-
195231
# owned. Used by adopt to look up which grant type a host-owned path expects.
196232
lw_manifest_known_grant_type() {
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
#!/usr/bin/env bash
2+
# Assertions: sync trees declared in the manifest (issue #90).
3+
#
4+
# update/check must resolve scripts/test membership from the TEMPLATE REF
5+
# (delivering new files, replacing stale ones, never touching host-added
6+
# files under the tree); adopt must resolve it from the template checkout
7+
# on disk and ADD the harness to adopted hosts.
8+
9+
STAGE="$SANDBOX/harness-tree-sync"
10+
H="$STAGE/host"
11+
REPO_ROOT_HTS="$(cd "$HERE/../.." && pwd)"
12+
13+
# --- update leg -------------------------------------------------------------
14+
UP_LOG="$STAGE/update.log"
15+
( cd "$H" && bash scripts/update-from-template.sh --template-url="$STAGE/template-src" ) \
16+
> "$UP_LOG" 2>&1
17+
RC=$?
18+
19+
assert "update exits 0" "[ $RC -eq 0 ]"
20+
assert "stale harness runner REPLACED with the template's" \
21+
"grep -qF 'NEW-RUNNER-MARKER' '$H/scripts/test/run.sh'"
22+
assert "old runner content gone" \
23+
"! grep -qF 'OLD-RUNNER-MARKER' '$H/scripts/test/run.sh'"
24+
assert "new harness lib delivered" \
25+
"[ -f '$H/scripts/test/lib/guard.sh' ]"
26+
assert "new fixture delivered" \
27+
"[ -f '$H/scripts/test/tests/unit/sample/assertions.sh' ]"
28+
assert "CI workflow delivered" \
29+
"[ -f '$H/.github/workflows/test-harness.yml' ]"
30+
assert "host-owned feature test untouched (copy-no-delete)" \
31+
"grep -qF 'HOST-FEATURE-TEST-MARKER' '$H/scripts/test/tests/unit/myfeat/assertions.sh'"
32+
33+
# --- convergence + check leg ------------------------------------------------
34+
UP2_LOG="$STAGE/update2.log"
35+
( cd "$H" && bash scripts/update-from-template.sh --template-url="$STAGE/template-src" ) \
36+
> "$UP2_LOG" 2>&1
37+
assert "second update reports zero changed files" \
38+
"grep -qF 'Changed (0):' '$UP2_LOG'"
39+
40+
CK_LOG="$STAGE/check.log"
41+
( cd "$H" && bash scripts/check-template-version.sh --template-url="$STAGE/template-src" ) \
42+
> "$CK_LOG" 2>&1
43+
CK_RC=$?
44+
assert "check-template-version exits 0 (no drift incl. harness)" \
45+
"[ $CK_RC -eq 0 ]"
46+
47+
# --- adopt leg (dir-mode enumeration from the real template checkout) -------
48+
# Template-checkout only: this leg runs the CHECKOUT's own scripts/adopt.sh
49+
# against its own tree. In a derived project that binary is the frozen
50+
# instantiation-vintage adopt (not manifest-synced, by design), so its
51+
# behavior is unspecifiable here — observed in the field: a stub-era copy
52+
# dry-runs and ADDs nothing. Same discriminator as manifest-shape's guard.
53+
if [ ! -f "$REPO_ROOT_HTS/CLAUDE.md.template" ]; then
54+
skip "harness-tree-sync adopt leg" "not a template checkout (derived project; local adopt.sh vintage is unspecified)"
55+
return 0 2>/dev/null || true
56+
fi
57+
AD_LOG="$STAGE/adopt.log"
58+
( cd "$STAGE/adopt-host" && \
59+
bash "$REPO_ROOT_HTS/scripts/adopt.sh" --target=. --apply --agent=none ) \
60+
> "$AD_LOG" 2>&1
61+
AD_RC=$?
62+
assert "adopt --apply exits 0" "[ $AD_RC -eq 0 ]"
63+
assert "adopt ADDed the real harness runner" \
64+
"[ -f '$STAGE/adopt-host/scripts/test/run.sh' ]"
65+
assert "adopt ADDed the harness lib" \
66+
"[ -f '$STAGE/adopt-host/scripts/test/lib/template.sh' ]"
67+
assert "adopt ADDed the CI workflow" \
68+
"[ -f '$STAGE/adopt-host/.github/workflows/test-harness.yml' ]"
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
#!/usr/bin/env bash
2+
# Patch: fixture for manifest sync-trees (issue #90).
3+
#
4+
# Field case: scripts/test/ ships at instantiation but was absent from the
5+
# manifest, so derived repos ran a harness frozen at creation vintage —
6+
# red CI for days on naval-sensor-fusion (pre-#73 fixtures against a
7+
# derived tree). The fix declares scripts/test as a SYNC TREE whose
8+
# membership is resolved from the sync source at run time.
9+
#
10+
# Effects: creates $SANDBOX/harness-tree-sync/ with:
11+
# template-src/ stand-in template repo (branch main): a minimal harness
12+
# tree (run.sh, lib/, one fixture), the CI workflow file,
13+
# and one SHARED_INFRA control file
14+
# host/ derived project with CURRENT sync tooling (copied from
15+
# the working tree under test), a STALE harness runner,
16+
# and a host-owned feature test that the sync must never
17+
# touch (copy-no-delete contract)
18+
# adopt-host/ virgin repo for the adopt leg (dir-mode enumeration)
19+
#
20+
# Hermetic: remotes are local paths.
21+
22+
set -uo pipefail
23+
24+
HERE_PATCH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
25+
TEMPLATE_ROOT="$(cd "$HERE_PATCH/../../../../.." && pwd)"
26+
27+
STAGE="$SANDBOX/harness-tree-sync"
28+
mkdir -p "$STAGE"
29+
g() { git "$@"; }
30+
31+
# --- template-src ---
32+
T="$STAGE/template-src"
33+
g init -q "$T"
34+
g -C "$T" symbolic-ref HEAD refs/heads/main
35+
mkdir -p "$T/scripts/test/lib" "$T/scripts/test/tests/unit/sample" "$T/.github/workflows"
36+
printf '#!/usr/bin/env bash\n# NEW-RUNNER-MARKER (post-guard harness)\n' > "$T/scripts/test/run.sh"
37+
printf '# guard lib shipped by template\n' > "$T/scripts/test/lib/guard.sh"
38+
printf '# sample fixture assertions\n' > "$T/scripts/test/tests/unit/sample/assertions.sh"
39+
printf 'name: test-harness\non: [push]\n' > "$T/.github/workflows/test-harness.yml"
40+
printf 'control shared file\n' > "$T/llm-wiki.md"
41+
g -C "$T" add -A
42+
g -C "$T" commit -q -m "template content"
43+
44+
# --- host: current tooling + stale harness + host-owned feature test ---
45+
H="$STAGE/host"
46+
g init -q "$H"
47+
mkdir -p "$H/wiki/hosty.wiki" "$H/scripts/lib" \
48+
"$H/scripts/test/tests/unit/myfeat"
49+
: > "$H/wiki/hosty.wiki/SCHEMA_hosty.md"
50+
cp "$TEMPLATE_ROOT/scripts/update-from-template.sh" "$H/scripts/"
51+
cp "$TEMPLATE_ROOT/scripts/check-template-version.sh" "$H/scripts/"
52+
cp "$TEMPLATE_ROOT"/scripts/lib/*.sh "$H/scripts/lib/"
53+
printf '#!/usr/bin/env bash\n# OLD-RUNNER-MARKER (pre-guard, fails in derived checkouts)\n' \
54+
> "$H/scripts/test/run.sh"
55+
printf '# HOST-FEATURE-TEST-MARKER: installed by a feature; sync must never touch this\n' \
56+
> "$H/scripts/test/tests/unit/myfeat/assertions.sh"
57+
58+
# --- adopt leg: virgin host, adopted from the REAL template working tree ---
59+
A="$STAGE/adopt-host"
60+
g init -q "$A"
61+
g -C "$A" remote add origin "https://github.com/example-org/adopt-host.git"
62+
printf '# Adopt Host\n' > "$A/README.md"
63+
g -C "$A" add -A
64+
g -C "$A" commit -q -m "host baseline"
65+
66+
echo " harness-tree-sync patch applied: fixtures at $STAGE"

scripts/test/tests/smoke/manifest-convergence/patch.sh

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,13 @@ fi
110110
source "$TEMPLATE_ROOT/scripts/lib/template-manifest.sh"
111111
# adopt mode: empty repo_root, AGENT drives overlay inclusion.
112112
lw_manifest_assemble_active_files "" "claude-code"
113+
# Sync-tree members (dir mode, same source adopt itself uses, #90).
114+
lw_manifest_tree_files dir "$TEMPLATE_ROOT"
113115
} > "$STAGE/expected-claude.txt" 2>/dev/null
114116
{
115117
source "$TEMPLATE_ROOT/scripts/lib/template-manifest.sh"
116118
lw_manifest_assemble_active_files "" "none"
119+
lw_manifest_tree_files dir "$TEMPLATE_ROOT"
117120
} > "$STAGE/expected-none.txt" 2>/dev/null
118121

119122
echo " manifest-convergence patch applied: A=$A B=$B"

scripts/test/tests/unit/manifest-shape/assertions.sh

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,3 +169,32 @@ assert_eq "known_grant_sentinel(CLAUDE.md) is empty (managed-block delegates to
169169
"$(lw_mcall "lw_manifest_known_grant_sentinel 'CLAUDE.md'")"
170170
assert_eq "known_grant_sentinel(unknown path) is empty" "" \
171171
"$(lw_mcall "lw_manifest_known_grant_sentinel 'random/path.txt'")"
172+
173+
# --- Sync trees (TEMPLATE_SYNC_TREES + lw_manifest_tree_files, #90) --------
174+
# Membership is resolved at run time, so the invariants here are about the
175+
# declarations and the accessor, not a file list.
176+
assert_eq "TEMPLATE_SYNC_TREES is non-empty" "1" \
177+
"$(lw_mcall "[ \${#TEMPLATE_SYNC_TREES[@]} -gt 0 ] && echo 1 || echo 0")"
178+
179+
# Every declared tree exists as a directory in the template checkout.
180+
_missing_trees="$(lw_mcall "
181+
for t in \"\${TEMPLATE_SYNC_TREES[@]}\"; do
182+
[[ -d '$REPO_ROOT_LIB'/\$t ]] || echo \"\$t\"
183+
done")"
184+
assert "every sync tree exists in the template checkout" "[ -z \"$_missing_trees\" ]"
185+
186+
# No static entry may live under a declared tree: that would be double
187+
# ownership (the tree resolver and the static list would both sync it).
188+
_tree_overlap="$(lw_mcall "
189+
for t in \"\${TEMPLATE_SYNC_TREES[@]}\"; do
190+
for f in \"\${TEMPLATE_SHARED_INFRA[@]}\" \"\${TEMPLATE_OVERLAY_CLAUDE[@]}\" \"\${TEMPLATE_OVERLAY_CURSOR[@]}\"; do
191+
case \"\$f\" in \"\$t\"/*) echo \"\$f\";; esac
192+
done
193+
done")"
194+
assert "no static manifest entry lives under a sync tree" "[ -z \"$_tree_overlap\" ]"
195+
196+
# The dir-mode accessor enumerates real members from a checkout.
197+
assert "tree_files dir-mode lists the harness runner" \
198+
"lw_mcall \"lw_manifest_tree_files dir '$REPO_ROOT_LIB'\" | grep -qx 'scripts/test/run.sh'"
199+
assert "tree_files dir-mode returns a substantial member set" \
200+
"[ \"$(lw_mcall "lw_manifest_tree_files dir '$REPO_ROOT_LIB'" | wc -l | tr -d ' ')\" -gt 50 ]"

scripts/update-from-template.sh

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ FILES=()
144144
while IFS= read -r _path; do
145145
FILES+=("$_path")
146146
done < <(lw_manifest_assemble_active_files "$REPO_ROOT" "")
147+
# Sync-tree members (TEMPLATE_SYNC_TREES, e.g. the test harness) resolved
148+
# from the template ref — membership is never a static list (#90).
149+
while IFS= read -r _path; do
150+
FILES+=("$_path")
151+
done < <(lw_manifest_tree_files ref "$TEMPLATE_REF")
147152

148153
# --- Diff and apply ---
149154
CHANGED=()

0 commit comments

Comments
 (0)