aboutsummaryrefslogtreecommitdiff
path: root/.ai/scripts/tests/sync-templates.bats
diff options
context:
space:
mode:
authorCraig Jennings <c@cjennings.net>2026-07-31 13:36:21 -0500
committerCraig Jennings <c@cjennings.net>2026-07-31 13:36:21 -0500
commitf69dc22c80156f92c6e3519e87bc014558d77dbf (patch)
tree6cee5efc1a2822abeb114ad90e25438426aca7b7 /.ai/scripts/tests/sync-templates.bats
parenta212eeb7c92f291045344f2769c9a7f85f2a38d5 (diff)
downloadrulesets-f69dc22c80156f92c6e3519e87bc014558d77dbf.tar.gz
rulesets-f69dc22c80156f92c6e3519e87bc014558d77dbf.zip
fix(sync): narrow the dirty-rulesets guard to per-file excludes
One uncommitted file under the synced template paths used to skip all three rsyncs for every project. On 2026-07-30 that froze every template everywhere for a full day, and five workflow files went stale in one project alone with nothing reporting it. The blast radius had no relation to what I was editing. Each dirty path now becomes an --exclude on its own rsync. rsync honors an exclude on both sides, so a withheld file is neither overwritten nor deleted downstream. Every clean file still propagates. A dirty protocols.org skips only its own single-file transfer. A rename withholds both names, since sweeping the old copy would delete a file the project still runs mid-rename. The run now names what it held back, so a project can see it's one file behind. The behind-upstream guard stays all-or-nothing. That staleness lives in the destination, so there's no single source file to narrow to. I verified it against the real checkout mid-change: 48 workflows propagated while the two files dirty at the time were withheld and named. The same run under the old guard would have synced nothing.
Diffstat (limited to '.ai/scripts/tests/sync-templates.bats')
-rw-r--r--.ai/scripts/tests/sync-templates.bats104
1 files changed, 95 insertions, 9 deletions
diff --git a/.ai/scripts/tests/sync-templates.bats b/.ai/scripts/tests/sync-templates.bats
index e4e92cf..6651c99 100644
--- a/.ai/scripts/tests/sync-templates.bats
+++ b/.ai/scripts/tests/sync-templates.bats
@@ -104,35 +104,121 @@ _mk_repo() {
# --- guard one: rulesets dirty under the synced paths --------------------------
-@test "a dirty synced path in rulesets skips the sync and names the file" {
+@test "a dirty file is withheld from the sync and named in the output" {
printf 'in-flight edit\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
printf 'stale\n' > "$PROJ/.ai/workflows/startup.org"
run bash "$SYNC" "$PROJ"
[ "$status" -eq 0 ]
- [[ "$output" == *"uncommitted changes under the synced template paths"* ]]
+ [[ "$output" == *"withheld"* ]]
[[ "$output" == *"startup.org"* ]]
- # Nothing propagated — this is the whole-window blast radius from 2026-07-30.
+ # The in-flight edit still must not land downstream.
[ "$(cat "$PROJ/.ai/workflows/startup.org")" = "stale" ]
}
-@test "ONE dirty file blocks ALL THREE rsyncs, not just its own" {
- # The 2026-07-30 incident in one assertion: an edit to a workflow file also
- # withheld protocols.org and every script. This is current behavior, and the
- # narrowing change lands by making this test fail on purpose.
+@test "ONE dirty file no longer blocks the other two rsyncs" {
+ # The 2026-07-30 incident, inverted. An edit to a workflow file used to
+ # withhold protocols.org and every script for every project; now it withholds
+ # only itself.
printf 'in-flight edit\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
printf 'stale protocols\n' > "$PROJ/.ai/protocols.org"
printf 'stale helper\n' > "$PROJ/.ai/scripts/helper"
run bash "$SYNC" "$PROJ"
[ "$status" -eq 0 ]
+ [ "$(cat "$PROJ/.ai/protocols.org")" = "canonical protocols" ]
+ [ "$(cat "$PROJ/.ai/scripts/helper")" = "canonical helper" ]
+}
+
+@test "a dirty file's clean siblings under the SAME path still sync" {
+ printf 'canonical wrap\n' > "$RS/claude-templates/.ai/workflows/wrap.org"
+ git -C "$RS" add -A && git -C "$RS" commit -q -m 'add wrap'
+ printf 'in-flight edit\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
+ printf 'stale wrap\n' > "$PROJ/.ai/workflows/wrap.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ # The narrowing is per-file, not per-directory: only startup.org is held back.
+ [ "$(cat "$PROJ/.ai/workflows/wrap.org")" = "canonical wrap" ]
+}
+
+@test "an excluded file is not deleted by --delete either" {
+ # rsync honors --exclude on both sides, so a withheld file that exists
+ # downstream must survive the run rather than being swept as a stray.
+ printf 'in-flight edit\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
+ printf 'project copy\n' > "$PROJ/.ai/workflows/startup.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ -e "$PROJ/.ai/workflows/startup.org" ]
+ [ "$(cat "$PROJ/.ai/workflows/startup.org")" = "project copy" ]
+}
+
+@test "a dirty protocols.org withholds only itself; workflows and scripts sync" {
+ printf 'in-flight edit\n' >> "$RS/claude-templates/.ai/protocols.org"
+ printf 'stale protocols\n' > "$PROJ/.ai/protocols.org"
+ printf 'stale startup\n' > "$PROJ/.ai/workflows/startup.org"
+ printf 'stale helper\n' > "$PROJ/.ai/scripts/helper"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
[ "$(cat "$PROJ/.ai/protocols.org")" = "stale protocols" ]
+ [ "$(cat "$PROJ/.ai/workflows/startup.org")" = "canonical startup" ]
+ [ "$(cat "$PROJ/.ai/scripts/helper")" = "canonical helper" ]
+}
+
+@test "dirty files across two synced paths withhold both, sync the third" {
+ printf 'in-flight\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
+ printf 'in-flight\n' >> "$RS/claude-templates/.ai/scripts/helper"
+ printf 'stale startup\n' > "$PROJ/.ai/workflows/startup.org"
+ printf 'stale helper\n' > "$PROJ/.ai/scripts/helper"
+ printf 'stale protocols\n' > "$PROJ/.ai/protocols.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ "$(cat "$PROJ/.ai/workflows/startup.org")" = "stale startup" ]
[ "$(cat "$PROJ/.ai/scripts/helper")" = "stale helper" ]
+ [ "$(cat "$PROJ/.ai/protocols.org")" = "canonical protocols" ]
}
-@test "an untracked file under a synced path also blocks the sync" {
+@test "an untracked file under a synced path is withheld, not blocking" {
printf 'new template\n' > "$RS/claude-templates/.ai/workflows/brand-new.org"
+ printf 'stale protocols\n' > "$PROJ/.ai/protocols.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ [[ "$output" == *"withheld"* ]]
+ # An unfinished new template must not ship half-written...
+ [ ! -e "$PROJ/.ai/workflows/brand-new.org" ]
+ # ...and must not hold back everything else.
+ [ "$(cat "$PROJ/.ai/protocols.org")" = "canonical protocols" ]
+}
+
+@test "an untracked DIRECTORY under a synced path is withheld whole" {
+ # git collapses an untracked dir to one porcelain line with a trailing slash
+ # ("?? .../plugins/"), so the exclude has to match the directory rather than
+ # the files inside it. A half-written plugin dir must not ship.
+ mkdir -p "$RS/claude-templates/.ai/workflows/plugins"
+ printf 'half written\n' > "$RS/claude-templates/.ai/workflows/plugins/new.org"
+ printf 'stale protocols\n' > "$PROJ/.ai/protocols.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ ! -e "$PROJ/.ai/workflows/plugins" ]
+ [ "$(cat "$PROJ/.ai/protocols.org")" = "canonical protocols" ]
+}
+
+@test "a file dirty in the index (staged, uncommitted) is withheld too" {
+ printf 'staged edit\n' >> "$RS/claude-templates/.ai/workflows/startup.org"
+ git -C "$RS" add claude-templates/.ai/workflows/startup.org
+ printf 'stale\n' > "$PROJ/.ai/workflows/startup.org"
+ run bash "$SYNC" "$PROJ"
+ [ "$status" -eq 0 ]
+ [ "$(cat "$PROJ/.ai/workflows/startup.org")" = "stale" ]
+}
+
+@test "a renamed template withholds both the old and the new path" {
+ git -C "$RS" mv claude-templates/.ai/workflows/startup.org \
+ claude-templates/.ai/workflows/renamed.org
+ printf 'stale startup\n' > "$PROJ/.ai/workflows/startup.org"
run bash "$SYNC" "$PROJ"
[ "$status" -eq 0 ]
- [[ "$output" == *"uncommitted changes under the synced template paths"* ]]
+ # The new name must not ship mid-rename, and the old copy must not be swept
+ # while the rename is still uncommitted.
+ [ ! -e "$PROJ/.ai/workflows/renamed.org" ]
+ [ "$(cat "$PROJ/.ai/workflows/startup.org")" = "stale startup" ]
}
@test "rulesets dirt OUTSIDE the synced paths does not block the sync" {