fix(agents-vault): dedupe local-slug formula, drop dead code, widen migration coverage
The slug-migration fallback (used when there is no live symlink to read the previous slug from) recomputed the local-* candidate by lowercasing the basename only, while _agents_repo_slug sanitizes it. The two formulas had drifted, so the fallback silently found nothing for any project directory whose basename needed sanitizing. Extract the formula into a single private helper, _agents_repo_local_slug, and have both _agents_repo_slug's no-remote branch and agents-vault's migration fallback call it, so there is one place left to drift. Also drop two dead lines the review flagged: an unused local, and an unreachable mkdir -p (path dirname ...) — slugs never contain a path separator, so dirname always resolves to a directory that already exists by that point. Widen migration test coverage: the current-entry-present-but-empty case, a remote URL rewrite, a remote removal, and a dirty-basename fallback test that fails without the sanitization fix and passes with it.
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
# Copyright (C) 2026 Rootiest
|
||||||
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# SYNOPSIS
|
||||||
|
# _agents_repo_local_slug <dir>
|
||||||
|
#
|
||||||
|
# DESCRIPTION
|
||||||
|
# Builds the path-derived fallback slug used when a project has no git
|
||||||
|
# remote: local-<sanitized-basename>-<8 hex of sha256(realpath)>. The
|
||||||
|
# basename is lowercased and every character outside [a-z0-9._-] is
|
||||||
|
# mapped to a dash, matching the sanitization the remote-URL branch of
|
||||||
|
# _agents_repo_slug applies to hostnames and paths.
|
||||||
|
#
|
||||||
|
# This is the single source of truth for that formula. It exists so the
|
||||||
|
# rule is written once: _agents_repo_slug's no-remote branch calls it to
|
||||||
|
# produce the slug, and agents-vault's slug-migration fallback (used when
|
||||||
|
# there is no live symlink yet to read the previous slug from) calls it
|
||||||
|
# to recompute the same candidate. Duplicating the formula in both places
|
||||||
|
# let them drift once before; this closes that gap for good.
|
||||||
|
#
|
||||||
|
# ARGUMENTS
|
||||||
|
# dir Absolute or relative path to the project directory
|
||||||
|
#
|
||||||
|
# EXIT STATUS
|
||||||
|
# 0 Slug printed
|
||||||
|
# 1 No directory argument given
|
||||||
|
#
|
||||||
|
# RETURNS
|
||||||
|
# The local-* slug, one line on stdout.
|
||||||
|
#
|
||||||
|
# EXAMPLE
|
||||||
|
# set -l slug (_agents_repo_local_slug /home/user/myproject)
|
||||||
|
function _agents_repo_local_slug --argument-names dir
|
||||||
|
test -n "$dir"; or return 1
|
||||||
|
|
||||||
|
set -l rp (path resolve "$dir")
|
||||||
|
set -l base (string lower -- (path basename "$rp") | string replace -ra '[^a-z0-9._-]' '-')
|
||||||
|
set -l digest (printf '%s' "$rp" | sha256sum | string split -f1 ' ')
|
||||||
|
printf 'local-%s-%s\n' "$base" (string sub -l 8 -- "$digest")
|
||||||
|
end
|
||||||
@@ -1,6 +1,9 @@
|
|||||||
# Copyright (C) 2026 Rootiest
|
# Copyright (C) 2026 Rootiest
|
||||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
# DEPENDENCIES
|
||||||
|
# _agents_repo_local_slug
|
||||||
|
#
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
# _agents_repo_slug <dir>
|
# _agents_repo_slug <dir>
|
||||||
#
|
#
|
||||||
@@ -63,8 +66,5 @@ function _agents_repo_slug --argument-names dir
|
|||||||
return 0
|
return 0
|
||||||
end
|
end
|
||||||
|
|
||||||
set -l rp (path resolve "$dir")
|
_agents_repo_local_slug "$dir"
|
||||||
set -l base (string lower -- (path basename "$rp") | string replace -ra '[^a-z0-9._-]' '-')
|
|
||||||
set -l digest (printf '%s' "$rp" | sha256sum | string split -f1 ' ')
|
|
||||||
printf 'local-%s-%s\n' "$base" (string sub -l 8 -- "$digest")
|
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -5,8 +5,9 @@
|
|||||||
# 12-ai-and-developer-tools
|
# 12-ai-and-developer-tools
|
||||||
#
|
#
|
||||||
# DEPENDENCIES
|
# DEPENDENCIES
|
||||||
# _agents_vault_dir, _agents_repo_slug, _agents_repo_ensure_symlink,
|
# _agents_vault_dir, _agents_repo_slug, _agents_repo_local_slug,
|
||||||
# _agents_repo_sync, _agents_repo_install_tools, git, hostname
|
# _agents_repo_ensure_symlink, _agents_repo_sync,
|
||||||
|
# _agents_repo_install_tools, git, hostname
|
||||||
#
|
#
|
||||||
# SYNOPSIS
|
# SYNOPSIS
|
||||||
# agents-vault [--link] [--push] [--restore] [--status]
|
# agents-vault [--link] [--push] [--restore] [--status]
|
||||||
@@ -230,17 +231,13 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
end
|
end
|
||||||
if test -z "$prev_slug"
|
if test -z "$prev_slug"
|
||||||
# No link yet (fresh machine): try the path-derived candidate.
|
# No link yet (fresh machine): try the path-derived candidate.
|
||||||
set -l cand_rp (path resolve "$root")
|
set -l cand (_agents_repo_local_slug "$root")
|
||||||
set -l cand_base (string lower -- (path basename "$cand_rp"))
|
|
||||||
set -l cand_digest (printf '%s' "$cand_rp" | sha256sum | string split -f1 ' ')
|
|
||||||
set -l cand "local-$cand_base-"(string sub -l 8 -- "$cand_digest")
|
|
||||||
if test "$cand" != "$slug"; and test -d "$vault/projects/$cand"
|
if test "$cand" != "$slug"; and test -d "$vault/projects/$cand"
|
||||||
set prev_slug $cand
|
set prev_slug $cand
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
if test -n "$prev_slug"; and test "$prev_slug" != "$slug"
|
if test -n "$prev_slug"; and test "$prev_slug" != "$slug"
|
||||||
set -l prev_mem "$vault/projects/$prev_slug/claude/memory"
|
|
||||||
set -l cur_content
|
set -l cur_content
|
||||||
test -d "$vmem"; and set cur_content (command ls -A "$vmem" 2>/dev/null)
|
test -d "$vmem"; and set cur_content (command ls -A "$vmem" 2>/dev/null)
|
||||||
if test (count $cur_content) -gt 0
|
if test (count $cur_content) -gt 0
|
||||||
@@ -249,7 +246,6 @@ function agents-vault --description 'track curated agent memory in a host-scoped
|
|||||||
return 1
|
return 1
|
||||||
end
|
end
|
||||||
test -d "$vmem"; and rm -rf "$vault/projects/$slug"
|
test -d "$vmem"; and rm -rf "$vault/projects/$slug"
|
||||||
mkdir -p (path dirname "$vault/projects/$slug")
|
|
||||||
if not git -C "$vault" mv "projects/$prev_slug" "projects/$slug" 2>/dev/null
|
if not git -C "$vault" mv "projects/$prev_slug" "projects/$slug" 2>/dev/null
|
||||||
command mv "$vault/projects/$prev_slug" "$vault/projects/$slug"; or return 1
|
command mv "$vault/projects/$prev_slug" "$vault/projects/$slug"; or return 1
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -365,6 +365,108 @@ check "ambiguous migration fails" 1 "$arc"
|
|||||||
check "ambiguous leaves old entry" old (cat $vroot2/agent-vault/projects/$aslug/claude/memory/x.md)
|
check "ambiguous leaves old entry" old (cat $vroot2/agent-vault/projects/$aslug/claude/memory/x.md)
|
||||||
check "ambiguous leaves new entry" new (cat $vroot2/agent-vault/projects/git.rootiest.dev-rootiest-clash/claude/memory/y.md)
|
check "ambiguous leaves new entry" new (cat $vroot2/agent-vault/projects/git.rootiest.dev-rootiest-clash/claude/memory/y.md)
|
||||||
|
|
||||||
|
# Case 2 of the three required cases: the *current* (new-slug) entry is
|
||||||
|
# present but empty -- neither absent (handled above) nor holding content
|
||||||
|
# (the ambiguous case above). Migration must still proceed.
|
||||||
|
set -l emp (new_repo)
|
||||||
|
set -l emangled (string replace -a '/' '-' -- $emp | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot2/$emangled/memory
|
||||||
|
echo precious2 >$croot2/$emangled/memory/keep.md
|
||||||
|
pushd $emp >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
set -l eslug (_agents_repo_slug $emp)
|
||||||
|
popd >/dev/null
|
||||||
|
|
||||||
|
git -C $emp remote add origin https://git.rootiest.dev/rootiest/emptycase.git
|
||||||
|
set -l enew_slug git.rootiest.dev-rootiest-emptycase
|
||||||
|
# Pre-create the destination entry as an empty directory -- present, not
|
||||||
|
# absent -- before migration runs.
|
||||||
|
mkdir -p $vroot2/agent-vault/projects/$enew_slug/claude/memory
|
||||||
|
|
||||||
|
pushd $emp >/dev/null
|
||||||
|
set -l erc (agents-vault --silent 2>/dev/null; echo $status)
|
||||||
|
popd >/dev/null
|
||||||
|
check "empty-current migration succeeds" 0 "$erc"
|
||||||
|
check "empty-current migrated content" precious2 (cat $vroot2/agent-vault/projects/$enew_slug/claude/memory/keep.md)
|
||||||
|
check "empty-current old entry removed" false (test -d $vroot2/agent-vault/projects/$eslug; and echo true; or echo false)
|
||||||
|
|
||||||
|
# Remote-URL-rewrite transition: origin changes from one forge URL to
|
||||||
|
# another (distinct from adding a remote where none existed).
|
||||||
|
set -l rw (new_repo https://git.rootiest.dev/rootiest/rewrite-old.git)
|
||||||
|
set -l rwmangled (string replace -a '/' '-' -- $rw | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot2/$rwmangled/memory
|
||||||
|
echo rewrite-precious >$croot2/$rwmangled/memory/keep.md
|
||||||
|
pushd $rw >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
set -l rw_old_slug git.rootiest.dev-rootiest-rewrite-old
|
||||||
|
check "rewrite: old entry populated" rewrite-precious (cat $vroot2/agent-vault/projects/$rw_old_slug/claude/memory/keep.md)
|
||||||
|
|
||||||
|
git -C $rw remote set-url origin https://git.rootiest.dev/rootiest/rewrite-new.git
|
||||||
|
pushd $rw >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
set -l rw_new_slug git.rootiest.dev-rootiest-rewrite-new
|
||||||
|
check "rewrite: migrated to new remote slug" rewrite-precious (cat $vroot2/agent-vault/projects/$rw_new_slug/claude/memory/keep.md)
|
||||||
|
check "rewrite: old entry removed" false (test -d $vroot2/agent-vault/projects/$rw_old_slug; and echo true; or echo false)
|
||||||
|
|
||||||
|
# Remote-removal transition: origin removed, slug reverts to local-*.
|
||||||
|
set -l rmv (new_repo https://git.rootiest.dev/rootiest/removeme.git)
|
||||||
|
set -l rmvmangled (string replace -a '/' '-' -- $rmv | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot2/$rmvmangled/memory
|
||||||
|
echo removal-precious >$croot2/$rmvmangled/memory/keep.md
|
||||||
|
pushd $rmv >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
set -l rmv_remote_slug git.rootiest.dev-rootiest-removeme
|
||||||
|
check "removal: remote entry populated" removal-precious (cat $vroot2/agent-vault/projects/$rmv_remote_slug/claude/memory/keep.md)
|
||||||
|
|
||||||
|
git -C $rmv remote remove origin
|
||||||
|
pushd $rmv >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
set -l rmv_local_slug (_agents_repo_slug $rmv)
|
||||||
|
popd >/dev/null
|
||||||
|
check "removal: migrated to local slug" removal-precious (cat $vroot2/agent-vault/projects/$rmv_local_slug/claude/memory/keep.md)
|
||||||
|
check "removal: old remote entry removed" false (test -d $vroot2/agent-vault/projects/$rmv_remote_slug; and echo true; or echo false)
|
||||||
|
|
||||||
|
# Fallback-candidate sanitization: a dirty basename (space, !) must produce
|
||||||
|
# the same local-* slug that _agents_repo_slug would derive, so that a
|
||||||
|
# lost-symlink recovery (no live link, but the vault entry survives) can
|
||||||
|
# still find and adopt it. Before the fix, the fallback only lowercased
|
||||||
|
# the basename instead of sanitizing it like _agents_repo_slug does, so it
|
||||||
|
# could never match the real entry directory for a name like this.
|
||||||
|
set -l dirty_root (mktemp -d); set -ga TMPDIRS $dirty_root
|
||||||
|
set -l dp "$dirty_root/My Project!"
|
||||||
|
mkdir -p "$dp"
|
||||||
|
git -C "$dp" init -q
|
||||||
|
git -C "$dp" config user.email t@t
|
||||||
|
git -C "$dp" config user.name t
|
||||||
|
git -C "$dp" config commit.gpgsign false
|
||||||
|
git -C "$dp" config core.hooksPath /dev/null
|
||||||
|
|
||||||
|
set -l dmangled (string replace -a '/' '-' -- $dp | string replace -a '.' '-')
|
||||||
|
mkdir -p $croot2/$dmangled/memory
|
||||||
|
echo "dirty-precious" >$croot2/$dmangled/memory/keep.md
|
||||||
|
|
||||||
|
pushd $dp >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
set -l dirty_local_slug (_agents_repo_slug $dp)
|
||||||
|
popd >/dev/null
|
||||||
|
check "dirty local entry populated" dirty-precious (cat $vroot2/agent-vault/projects/$dirty_local_slug/claude/memory/keep.md)
|
||||||
|
|
||||||
|
# Lose the live symlink, as a fresh-machine restore would, so migration
|
||||||
|
# must fall back to recomputing the candidate from the path instead of
|
||||||
|
# reading it off the (now-absent) live symlink.
|
||||||
|
rm -f $croot2/$dmangled/memory
|
||||||
|
|
||||||
|
git -C $dp remote add origin https://git.rootiest.dev/rootiest/dirty.git
|
||||||
|
pushd $dp >/dev/null
|
||||||
|
agents-vault --silent
|
||||||
|
popd >/dev/null
|
||||||
|
set -l dirty_new_slug git.rootiest.dev-rootiest-dirty
|
||||||
|
check "fallback finds sanitized local entry" dirty-precious (cat $vroot2/agent-vault/projects/$dirty_new_slug/claude/memory/keep.md)
|
||||||
|
check "fallback old entry removed" false (test -d $vroot2/agent-vault/projects/$dirty_local_slug; and echo true; or echo false)
|
||||||
|
|
||||||
set -e __fish_agent_vault_dir
|
set -e __fish_agent_vault_dir
|
||||||
set -e __fish_agent_vault_claude_root
|
set -e __fish_agent_vault_claude_root
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user