#!/usr/bin/env bash # test-rsync-ship-idempotency.sh — regression test for the ship-step bug fixed # 2026-08-27 (bin/mempalace-pi-session: rsync --update -> --checksum). # # THE BUG: the stage file's mtime is deliberately the SOURCE transcript's mtime # (os.utime() at :903, "preserve session mtime for dedup stability"), so a # re-export of a session that has not been appended to since the last ship # carries a mtime that is NOT newer than the receiver copy. rsync --update # skips a file whose mtime is not strictly newer than the destination's, so a # scrubbed re-export of a DORMANT session was silently dropped: content # differed, mtime did not, "success" was reported, and unscrubbed bytes stayed # in the palace host's inbox indefinitely. Measured on mbp-m1-2020 2026-08-27. # # THE ASSERTION mirrors that exactly and needs no ssh, no palace, no secret: # stage a file, ship it, rewrite the content while RESTORING the original # mtime (the same thing os.utime() does), ship again, assert the receiver's # sha256 changed. On the pre-fix flag (--update) this fails; with --checksum # (content comparison, mtime-independent) it passes. # # Deliberately does NOT invoke bin/mempalace-pi-session itself: that needs a # live ssh target, a palace, MEMPALACE_* env — disproportionate scaffolding for # what is, at its core, one rsync flag. Pulls the flag list out of the script # instead of hand-copying it, so a future change to the ship command either # updates this test's expectation or fails it loudly rather than drifting # silently out of sync with what actually ships. # # Usage: scripts/test-rsync-ship-idempotency.sh (exit 0 = fix still holds) set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" SHIP_SCRIPT="$REPO_ROOT/bin/mempalace-pi-session" SRC="$(mktemp -d)"; DST="$(mktemp -d)" trap 'rm -rf "$SRC" "$DST"' EXIT EXPECT='rsync -a --checksum --no-owner --no-group' if ! grep -qF "$EXPECT" "$SHIP_SCRIPT"; then echo "FAIL: $SHIP_SCRIPT no longer ships with '$EXPECT'." >&2 echo " Either the fix regressed, or the flags changed -- update this" >&2 echo " test's EXPECT string to match, don't just delete the test." >&2 exit 1 fi ship() { rsync -a --checksum --no-owner --no-group \ --include='*.jsonl' --exclude='*' \ "$SRC/" "$DST/" } # Same BYTE LENGTH on purpose, mirroring mbp-m1-2020's own reasoning for why # dropping --update outright would not be enough: rsync's default quick check # (no --update, no --checksum) already transfers when SIZE differs, so a test # with mismatched lengths would pass by accident and prove nothing about # --checksum specifically. A real redaction whose placeholder happens to match # the secret's length is exactly the case that stays silently undetected unless # content itself, not size or mtime, is compared. UNSCRUBBED='live: MEMPALACE_REMOTE_TOKEN=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' SCRUBBED='live: MEMPALACE_REMOTE_TOKEN=' if [[ ${#UNSCRUBBED} -ne ${#SCRUBBED} ]]; then echo "FAIL: test fixture bug -- the two fixture strings must be equal length (${#UNSCRUBBED} vs ${#SCRUBBED}); this test would not exercise --checksum at all otherwise." >&2 exit 1 fi # 1. Baseline: ship a session, receiver now matches sender. printf '%s\n' "$UNSCRUBBED" > "$SRC/session.jsonl" touch -d '2026-08-23T23:53:18' "$SRC/session.jsonl" ship before="$(sha256sum "$DST/session.jsonl" | cut -d' ' -f1)" # 2. The bug's exact shape: rewrite content (same length!) as a scrubbed # re-export would, then restore the ORIGINAL mtime, as the exporter's # os.utime() does. printf '%s\n' "$SCRUBBED" > "$SRC/session.jsonl" touch -d '2026-08-23T23:53:18' "$SRC/session.jsonl" ship after="$(sha256sum "$DST/session.jsonl" | cut -d' ' -f1)" if [[ "$before" == "$after" ]]; then echo "FAIL: receiver sha256 unchanged after a content-only re-export at a held-constant mtime." >&2 echo " This is the exact defect --checksum was added to fix." >&2 exit 1 fi if ! grep -q '&2 exit 1 fi echo "PASS: ship step transfers changed content even when mtime is deliberately held constant (before=$before after=$after)"