Files
fish-config/tests/test-agents-vault.fish
T
rootiest 51543cb7ca feat(agents-vault): add sync helper that refuses to commit conflicts
agents-init currently swallows a failed rebase and then stages and commits
whatever is in the tree, which records conflict markers under a routine
message. No AGENTS repo has a remote today so the pull never runs, but the
vault gives these repos remotes and arms it.

The shared helper aborts the rebase, commits nothing, and returns 2. It
also redirects git's own stdout during the pull/abort: git prints
"CONFLICT (content): ..." to stdout, not stderr, so without this the
message would leak into the helper's own stdout instead of staying
diagnostic-only.

The conflict fixture commits "ours" locally before diverging, since an
uncommitted worktree change has nothing for --autostash's rebase step to
replay -- it fast-forwards cleanly and only the stash pop would conflict.
2026-09-03 18:56:44 -04:00

190 lines
7.7 KiB
Fish

#!/usr/bin/env fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Hermetic tests for the agent memory vault helpers. Every test builds its
# own throwaway git repos and directories under mktemp; nothing touches the
# live vault, ~/.claude, or this checkout.
#
# Usage: fish tests/test-agents-vault.fish
set -l here (realpath (dirname (status filename)))
set -g repo_root (realpath $here/..)
set -p fish_function_path $repo_root/functions
set -g TESTS_RUN 0
set -g TESTS_FAILED 0
set -g TMPDIRS
function check --argument-names label want got
set -g TESTS_RUN (math $TESTS_RUN + 1)
if test "$want" = "$got"
echo " PASS $label"
else
echo " FAIL $label"
echo " want: $want"
echo " got: $got"
set -g TESTS_FAILED (math $TESTS_FAILED + 1)
end
end
# Create a throwaway git repo, optionally with an origin remote.
function new_repo --argument-names url
set -l d (mktemp -d)
set -ga TMPDIRS $d
git -C $d init -q
git -C $d config user.email t@t
git -C $d config user.name t
git -C $d config commit.gpgsign false
git -C $d config core.hooksPath /dev/null
test -n "$url"; and git -C $d remote add origin $url
printf '%s\n' $d
end
function cleanup
for d in $TMPDIRS
test -n "$d"; and rm -rf $d
end
end
# ─────────────────────────── slug derivation ───────────────────────────
echo "== _agents_repo_slug =="
set -l want git.rootiest.dev-rootiest-fish-config
set -l r (new_repo https://git.rootiest.dev/rootiest/fish-config.git)
check "https form" $want (_agents_repo_slug $r)
set r (new_repo git@git.rootiest.dev:rootiest/fish-config.git)
check "scp form" $want (_agents_repo_slug $r)
set r (new_repo ssh://git@git.rootiest.dev:22/rootiest/fish-config.git)
check "ssh form with port" $want (_agents_repo_slug $r)
set r (new_repo https://git.rootiest.dev/rootiest/fish-config)
check "no .git suffix" $want (_agents_repo_slug $r)
set r (new_repo HTTPS://Git.Rootiest.DEV/Rootiest/Fish-Config.git)
check "case folded" $want (_agents_repo_slug $r)
# Remote-less: local- prefix, stable across runs, distinct per path.
set -l n1 (new_repo)
set -l s1 (_agents_repo_slug $n1)
set -l s2 (_agents_repo_slug $n1)
check "local slug is stable" $s1 $s2
check "local slug is prefixed" true (string match -q 'local-*' -- $s1; and echo true; or echo false)
set -l n2 (new_repo)
check "local slugs differ by path" false (test "$s1" = (_agents_repo_slug $n2); and echo true; or echo false)
# A non-origin remote is used when origin is absent.
set -l r3 (new_repo)
git -C $r3 remote add upstream https://git.rootiest.dev/rootiest/fish-config.git
check "falls back to first remote" $want (_agents_repo_slug $r3)
# Slug sanitization test: special chars in fallback (local-) branch.
set -l dirt (mktemp -d); set -ga TMPDIRS $dirt
mkdir -p "$dirt/projects/My Project!"
git -C "$dirt/projects/My Project!" init -q
git -C "$dirt/projects/My Project!" config user.email t@t
git -C "$dirt/projects/My Project!" config user.name t
set -l slug_dirty (_agents_repo_slug "$dirt/projects/My Project!")
set -l has_bad_chars (string match -q '*[ !]*' -- "$slug_dirty"; and echo true; or echo false)
check "sanitizes special chars in local slug" false "$has_bad_chars"
# ──────────────────────── ensure_symlink rails ─────────────────────────
echo ""
echo "== _agents_repo_ensure_symlink =="
set -l w (mktemp -d); set -ga TMPDIRS $w
mkdir -p $w/target $w/live
# Fresh link onto an empty live parent.
_agents_repo_ensure_symlink $w/live/memory $w/target >/dev/null
check "creates the link" true (test -L $w/live/memory; and echo true; or echo false)
check "link resolves to target" (path resolve $w/target) (path resolve $w/live/memory)
# Idempotent: a second run prints nothing.
set -l second (_agents_repo_ensure_symlink $w/live/memory $w/target)
check "idempotent, silent" "" "$second"
# Refuses a non-directory target (a symlinked FILE breaks agent edits).
touch $w/afile
check "refuses file target" 1 (_agents_repo_ensure_symlink $w/live/f2 $w/afile 2>/dev/null; echo $status)
# Refuses to create a dangling link when the target is missing.
check "refuses missing target" 1 (_agents_repo_ensure_symlink $w/live/f3 $w/nope 2>/dev/null; echo $status)
check "no dangling link left" false (test -L $w/live/f3; and echo true; or echo false)
# Non-destructive adoption: content on both sides, nothing overwritten.
set -l a (mktemp -d); set -ga TMPDIRS $a
mkdir -p $a/vault $a/live/memory
echo vault-version >$a/vault/shared.md
echo vault-only >$a/vault/vaultonly.md
echo live-version >$a/live/memory/shared.md
echo live-only >$a/live/memory/liveonly.md
_agents_repo_ensure_symlink $a/live/memory $a/vault >/dev/null
check "adoption keeps vault copy" vault-version (cat $a/vault/shared.md)
check "adoption imports live-only file" live-only (cat $a/vault/liveonly.md)
check "adoption keeps vault-only file" vault-only (cat $a/vault/vaultonly.md)
check "adoption replaced dir with link" true (test -L $a/live/memory; and echo true; or echo false)
# Repins a link that points somewhere else.
set -l p (mktemp -d); set -ga TMPDIRS $p
mkdir -p $p/one $p/two
ln -s $p/one $p/link
_agents_repo_ensure_symlink $p/link $p/two >/dev/null
check "repins a wrong link" (path resolve $p/two) (path resolve $p/link)
# ─────────────────────────── sync policy ───────────────────────────────
echo ""
echo "== _agents_repo_sync =="
set -l s (new_repo)
echo hello >$s/a.md
_agents_repo_sync $s "chore: test" >/dev/null
check "commits new content" 1 (git -C $s rev-list --count HEAD)
# Nothing to do: no second commit, no output.
set -l out (_agents_repo_sync $s "chore: test")
check "idempotent, no new commit" 1 (git -C $s rev-list --count HEAD)
check "idempotent, silent" "" "$out"
# Conflict: two clones diverge with conflicting commits on the same line.
# (Merely leaving "ours" uncommitted in the worktree isn't enough to force
# a rebase conflict -- with nothing local to replay, --autostash's rebase
# step fast-forwards cleanly and only the stash *pop* would conflict,
# leaving "theirs" committed on HEAD with "ours" stranded in the stash.
# Committing "ours" locally first means the rebase itself must replay a
# real commit over "theirs" on the same line, which is where the intended
# conflict-and-abort path actually lives.)
set -l origin (mktemp -d); set -ga TMPDIRS $origin
git -C $origin init -q --bare
git -C $s remote add origin $origin
git -C $s push -q -u origin HEAD:refs/heads/main 2>/dev/null
set -l clone (mktemp -d); set -ga TMPDIRS $clone
git clone -q $origin $clone
git -C $clone config user.email t@t
git -C $clone config user.name t
git -C $clone config commit.gpgsign false
git -C $clone config core.hooksPath /dev/null
echo theirs >$clone/a.md
git -C $clone commit -qam theirs
git -C $clone push -q origin HEAD:main
echo ours >$s/a.md
git -C $s commit -qam ours
set -l before_count (git -C $s rev-list --count HEAD)
set -l rc (_agents_repo_sync $s "chore: test" 2>/dev/null; echo $status)
check "conflict returns 2" 2 "$rc"
check "conflict leaves no rebase in progress" false (test -d $s/.git/rebase-merge -o -d $s/.git/rebase-apply; and echo true; or echo false)
check "conflict commits nothing" $before_count (git -C $s rev-list --count HEAD)
check "conflict content survives" ours (cat $s/a.md)
check "conflict left no markers" false (grep -q '<<<<<<<' $s/a.md; and echo true; or echo false)
cleanup
echo ""
echo (math $TESTS_RUN - $TESTS_FAILED)"/$TESTS_RUN passed"
exit $TESTS_FAILED