From ab1d7a8ae9ba04144715a4447121e416e133b4fd Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 21 Oct 2025 02:06:29 -0700 Subject: [PATCH 1/4] test: cover remote snapshot restore regressions --- test/26_remote_restore.bats | 79 +++++++++++++++++++++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 test/26_remote_restore.bats diff --git a/test/26_remote_restore.bats b/test/26_remote_restore.bats new file mode 100644 index 0000000..4fcd40d --- /dev/null +++ b/test/26_remote_restore.bats @@ -0,0 +1,79 @@ +#!/usr/bin/env bats + +load helpers/common + +setup() { + REMOTE_UNDER_TEST="shimtest-${BATS_TEST_NUMBER}" + SECONDARY_REMOTE="${REMOTE_UNDER_TEST}-secondary" + TMP_PRIMARY_REMOTE="" + TMP_SECONDARY_REMOTE="" + shiplog_reset_remote_snapshot_state >/dev/null 2>&1 || true + shiplog_git_caller remote remove "$REMOTE_UNDER_TEST" >/dev/null 2>&1 || true + shiplog_git_caller remote remove "$SECONDARY_REMOTE" >/dev/null 2>&1 || true +} + +teardown() { + shiplog_git_caller remote remove "$REMOTE_UNDER_TEST" >/dev/null 2>&1 || true + shiplog_git_caller remote remove "$SECONDARY_REMOTE" >/dev/null 2>&1 || true + [ -z "$TMP_PRIMARY_REMOTE" ] || rm -rf "$TMP_PRIMARY_REMOTE" + [ -z "$TMP_SECONDARY_REMOTE" ] || rm -rf "$TMP_SECONDARY_REMOTE" + shiplog_reset_remote_snapshot_state >/dev/null 2>&1 || true +} + +capture_remote_section() { + local remote="$1" + shiplog_git_caller config --local --get-regexp "^remote\\.${remote}\\." 2>/dev/null | sort +} + +@test "restore removes remotes that were added after snapshot" { + shiplog_snapshot_caller_repo_state + + TMP_PRIMARY_REMOTE="$(mktemp -d)" + git init -q --bare "$TMP_PRIMARY_REMOTE" + + shiplog_git_caller remote add "$REMOTE_UNDER_TEST" "$TMP_PRIMARY_REMOTE" + shiplog_git_caller remote set-url --add "$REMOTE_UNDER_TEST" "https://example.invalid/${REMOTE_UNDER_TEST}.git" + shiplog_git_caller remote set-url --push "$REMOTE_UNDER_TEST" "ssh://example.invalid/${REMOTE_UNDER_TEST}-push.git" + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.mirror" true + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.fetch" "+refs/heads/*:refs/remotes/${REMOTE_UNDER_TEST}/*" + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.prune" true + + run shiplog_restore_caller_remotes + [ "$status" -eq 0 ] + + run shiplog_git_caller remote + [ "$status" -eq 0 ] + echo "$output" | grep -qv "^${REMOTE_UNDER_TEST}\$" +} + +@test "restore rehydrates complex remote configuration" { + TMP_PRIMARY_REMOTE="$(mktemp -d)" + TMP_SECONDARY_REMOTE="$(mktemp -d)" + git init -q --bare "$TMP_PRIMARY_REMOTE" + git init -q --bare "$TMP_SECONDARY_REMOTE" + + shiplog_git_caller remote add "$REMOTE_UNDER_TEST" "$TMP_PRIMARY_REMOTE" + shiplog_git_caller remote set-url --add "$REMOTE_UNDER_TEST" "https://example.invalid/${REMOTE_UNDER_TEST}.git" + shiplog_git_caller remote set-url --push "$REMOTE_UNDER_TEST" "ssh://example.invalid/${REMOTE_UNDER_TEST}-push.git" + shiplog_git_caller remote set-url --push --add "$REMOTE_UNDER_TEST" "ssh://example.invalid/${REMOTE_UNDER_TEST}-push-secondary.git" + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.mirror" true + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.prune" true + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.fetch" "+refs/heads/*:refs/remotes/${REMOTE_UNDER_TEST}/*" + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.fetch" "+refs/tags/*:refs/tags/*" + + local baseline + baseline="$(capture_remote_section "$REMOTE_UNDER_TEST")" + + shiplog_snapshot_caller_repo_state + + shiplog_git_caller remote remove "$REMOTE_UNDER_TEST" + shiplog_git_caller remote add "$REMOTE_UNDER_TEST" "$TMP_SECONDARY_REMOTE" + shiplog_git_caller config --local --add "remote.${REMOTE_UNDER_TEST}.fetch" "+refs/heads/main:refs/tmp/main" + + run shiplog_restore_caller_remotes + [ "$status" -eq 0 ] + + local restored + restored="$(capture_remote_section "$REMOTE_UNDER_TEST")" + [ "$baseline" = "$restored" ] +} From e870680c466f677b38e9530ab9340e4da614c9ac Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 21 Oct 2025 02:06:37 -0700 Subject: [PATCH 2/4] test: scaffold remote snapshot helpers --- test/helpers/common.bash | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/test/helpers/common.bash b/test/helpers/common.bash index 6c0574f..e898fca 100644 --- a/test/helpers/common.bash +++ b/test/helpers/common.bash @@ -5,6 +5,36 @@ SHIPLOG_SANDBOX_REPO="${SHIPLOG_SANDBOX_REPO:-https://github.com/flyingrobots/sh SHIPLOG_SANDBOX_BRANCH="${SHIPLOG_SANDBOX_BRANCH:-main}" SHIPLOG_TEST_ROOT="${SHIPLOG_TEST_ROOT:-$(pwd)}" +declare -ag SHIPLOG_TEMP_REMOTE_DIRS=() +declare -ag SHIPLOG_ORIG_REMOTE_ORDER=() +declare -Ag SHIPLOG_ORIG_REMOTES_CONFIG=() +declare -gi SHIPLOG_CALLER_REPO_CAPTURED=0 + +shiplog_git_caller() { + echo "ERROR: shiplog_git_caller not yet implemented" >&2 + return 1 +} + +shiplog_helper_error() { + echo "ERROR: $*" >&2 + return 1 +} + +shiplog_reset_remote_snapshot_state() { + SHIPLOG_ORIG_REMOTE_ORDER=() + unset SHIPLOG_ORIG_REMOTES_CONFIG + declare -Ag SHIPLOG_ORIG_REMOTES_CONFIG=() + SHIPLOG_CALLER_REPO_CAPTURED=0 +} + +shiplog_snapshot_caller_repo_state() { + shiplog_helper_error "shiplog_snapshot_caller_repo_state not yet implemented" +} + +shiplog_restore_caller_remotes() { + shiplog_helper_error "shiplog_restore_caller_remotes not yet implemented" +} + shiplog_install_cli() { local project_home="${SHIPLOG_HOME:-$SHIPLOG_PROJECT_ROOT}" From d04655b9fbe14e0b1321d487d3fc7806063ce9d8 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 21 Oct 2025 02:10:15 -0700 Subject: [PATCH 3/4] test: implement caller remote snapshot restore --- test/helpers/common.bash | 128 +++++++++++++++++++++++++++++++++++++-- 1 file changed, 124 insertions(+), 4 deletions(-) diff --git a/test/helpers/common.bash b/test/helpers/common.bash index e898fca..7373819 100644 --- a/test/helpers/common.bash +++ b/test/helpers/common.bash @@ -11,8 +11,7 @@ declare -Ag SHIPLOG_ORIG_REMOTES_CONFIG=() declare -gi SHIPLOG_CALLER_REPO_CAPTURED=0 shiplog_git_caller() { - echo "ERROR: shiplog_git_caller not yet implemented" >&2 - return 1 + git -c safe.directory="$SHIPLOG_TEST_ROOT" -C "$SHIPLOG_TEST_ROOT" "$@" } shiplog_helper_error() { @@ -28,11 +27,132 @@ shiplog_reset_remote_snapshot_state() { } shiplog_snapshot_caller_repo_state() { - shiplog_helper_error "shiplog_snapshot_caller_repo_state not yet implemented" + local -a new_order=() + local -A new_config=() + + if ! shiplog_git_caller rev-parse --is-inside-work-tree >/dev/null 2>&1; then + shiplog_helper_error "Caller repository is not a git repository: $SHIPLOG_TEST_ROOT" || return 1 + fi + + local remote_list + if ! remote_list=$(shiplog_git_caller remote 2>&1); then + shiplog_helper_error "Failed to list caller remotes: $remote_list" || return 1 + fi + + while IFS= read -r remote; do + [ -n "$remote" ] || continue + new_order+=("$remote") + local escaped config + escaped=$(printf '%s' "$remote" | sed 's/[][\\.*^$]/\\&/g') + config=$(shiplog_git_caller config --local --get-regexp "^remote\\.${escaped}\\." 2>/dev/null) + new_config["$remote"]="$config" + done <<< "$remote_list" + + shiplog_reset_remote_snapshot_state + SHIPLOG_CALLER_REPO_CAPTURED=1 + SHIPLOG_ORIG_REMOTE_ORDER=("${new_order[@]}") + local remote + for remote in "${new_order[@]}"; do + SHIPLOG_ORIG_REMOTES_CONFIG["$remote"]="${new_config[$remote]}" + done + return 0 } shiplog_restore_caller_remotes() { - shiplog_helper_error "shiplog_restore_caller_remotes not yet implemented" + [ "$SHIPLOG_CALLER_REPO_CAPTURED" -eq 1 ] || return 0 + + local remote_list + if ! remote_list=$(shiplog_git_caller remote 2>&1); then + shiplog_helper_error "Failed to list caller remotes during restore: $remote_list" || return 1 + fi + + declare -A expected=() + local remote + for remote in "${SHIPLOG_ORIG_REMOTE_ORDER[@]}"; do + expected["$remote"]=1 + done + + while IFS= read -r remote; do + [ -n "$remote" ] || continue + if [[ -z ${expected[$remote]+_} ]]; then + shiplog_git_caller remote remove "$remote" >/dev/null 2>&1 || true + shiplog_git_caller config --local --remove-section "remote.$remote" >/dev/null 2>&1 || true + fi + done <<< "$remote_list" + + for remote in "${SHIPLOG_ORIG_REMOTE_ORDER[@]}"; do + local desired_config="${SHIPLOG_ORIG_REMOTES_CONFIG[$remote]}" + local -a desired_lines=() + local first_url="" + + if [ -n "$desired_config" ]; then + while IFS= read -r line; do + [ -n "$line" ] || continue + desired_lines+=("$line") + local key value + key=${line%% *} + value=${line#* } + if [[ "$key" == "remote.$remote.url" && -z "$first_url" ]]; then + first_url="$value" + fi + done <<< "$desired_config" + fi + + shiplog_git_caller remote remove "$remote" >/dev/null 2>&1 || true + shiplog_git_caller config --local --remove-section "remote.$remote" >/dev/null 2>&1 || true + + if [ ${#desired_lines[@]} -eq 0 ]; then + continue + fi + + if [ -z "$first_url" ]; then + shiplog_helper_error "Missing URL while restoring remote \"$remote\"" || return 1 + fi + + local add_err + if ! add_err=$(shiplog_git_caller remote add "$remote" "$first_url" 2>&1); then + shiplog_helper_error "Failed to re-add remote \"$remote\": $add_err" || return 1 + fi + + shiplog_git_caller config --local --unset-all "remote.$remote.fetch" >/dev/null 2>&1 || true + shiplog_git_caller config --local --unset-all "remote.$remote.pushurl" >/dev/null 2>&1 || true + + local primary_url_seen=0 + local line key value + for line in "${desired_lines[@]}"; do + key=${line%% *} + value=${line#* } + case "$key" in + "remote.$remote.url") + if [ "$value" = "$first_url" ] && [ $primary_url_seen -eq 0 ]; then + primary_url_seen=1 + continue + fi + if ! shiplog_git_caller remote set-url --add "$remote" "$value" >/dev/null 2>&1; then + shiplog_helper_error "Failed to add additional URL for \"$remote\": $value" || return 1 + fi + ;; + "remote.$remote.pushurl") + if ! shiplog_git_caller remote set-url --push --add "$remote" "$value" >/dev/null 2>&1; then + shiplog_helper_error "Failed to add pushurl for \"$remote\": $value" || return 1 + fi + ;; + "remote.$remote.fetch") + if ! shiplog_git_caller config --local --add "remote.$remote.fetch" "$value" >/dev/null 2>&1; then + shiplog_helper_error "Failed to restore fetch spec for \"$remote\": $value" || return 1 + fi + ;; + *) + if ! shiplog_git_caller config --local --add "$key" "$value" >/dev/null 2>&1; then + shiplog_helper_error "Failed to restore $key=$value" || return 1 + fi + ;; + esac + done + done + + shiplog_reset_remote_snapshot_state + return 0 } shiplog_install_cli() { From a35a8a705a3d1e33275dc4405f8723803a9c7d03 Mon Sep 17 00:00:00 2001 From: James Ross Date: Tue, 21 Oct 2025 02:10:46 -0700 Subject: [PATCH 4/4] docs: note remote snapshot helpers for tests --- test/README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/README.md b/test/README.md index 301ae00..0cad879 100644 --- a/test/README.md +++ b/test/README.md @@ -27,7 +27,8 @@ The `test/` directory contains the Bats-based integration suite that exercises t 2. Reuse shared setup logic via `load helpers/common` and environment variables (`SHIPLOG_*`) rather than duplicating installation code. 3. Use the global `--yes` flag instead of piping `yes |` when a test needs to auto-confirm prompts. 4. Keep tests hermetic: they should initialize their own temporary repos, configure required policy files, and avoid relying on host state. -5. Document new scenarios by updating `docs/features/` or the README feature table so tooling stays in sync. +5. If a test needs to mutate the repository’s remotes, call `shiplog_snapshot_caller_repo_state` before making changes and `shiplog_restore_caller_remotes` (or rely on the shared teardown) so the caller repo is restored accurately—including multiple URLs, push URLs, and custom fetch specs. +6. Document new scenarios by updating `docs/features/` or the README feature table so tooling stays in sync. ## Running Specific Tests