Files
fish-config/tests/test-network-fish.fish
T
rootiest e6c6d2533d fix(git-clean): strip the '+' other-worktree marker too
git branch -vv marks column 1 with '+' (not '*') for a branch checked
out in another linked worktree. Only '*' was stripped from $1, so a
gone branch shown with '+' left a bogus "+" entry in $gone_branches
that then failed to delete: error: branch '+' not found.

Add a regression case reproducing the '+'-marked gone-branch line via
the existing git-clean mock handler in tests/test-network-fish.fish.
2026-09-09 21:14:04 -04:00

709 lines
28 KiB
Fish

#!/usr/bin/env fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Hermetic unit tests for network-dependent fish functions with full mock coverage
# and failure edge-case testing:
# - gi (gitignore generation, API fetch, deduplication, error handling)
# - gip, gip4, gip6 (public IP resolution, timeouts, IPv4/IPv6 fallback, failure notices)
# - qr (terminal QR code generator, qrencode local bypass, curl fallback & network drop)
# - bd-pull (Gitea issues sync, missing tokens, API failures, malformed JSON, linking)
# - _auto_pull_sync (background safe fast-forward, dirty trees, network drop on fetch)
# - gitup (remote fetch & status, network drop handling)
# - git-clean (remote fetch/prune, orphaned branch detection & deletion)
# - config-update (upstream fetch, up-to-date check, dry-run, force stash & restore)
# - repo-open (origin URL parsing, offline local tracking, ls-remote fallback & drop)
# - fzf-update (git pull/clone network failure handling)
#
# MODE: isolated
source (realpath (dirname (status filename)))/lib.fish
set -p fish_function_path $repo_root/functions
set -gx TERM xterm-256color
set -g TMPDIRS
# Ensure git operations inside tests are hermetic and do not prompt
set -gx GIT_AUTHOR_NAME t
set -gx GIT_AUTHOR_EMAIL t@t
set -gx GIT_COMMITTER_NAME t
set -gx GIT_COMMITTER_EMAIL t@t
set -gx GIT_CONFIG_COUNT 2
set -gx GIT_CONFIG_KEY_0 commit.gpgsign
set -gx GIT_CONFIG_VALUE_0 false
set -gx GIT_CONFIG_KEY_1 init.defaultBranch
set -gx GIT_CONFIG_VALUE_1 main
set -gx GIT_TERMINAL_PROMPT 0
# Helper to create temporary git repos
function new_repo
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
printf '%s\n' $d
end
# Build a hermetic mock bin directory
set -g MOCK_DIR (mktemp -d)
set -ga TMPDIRS $MOCK_DIR
set -l real_git (command -s git)
# 1. Mock curl
printf '%s\n' \
'#!/bin/sh' \
'if [ -n "$MOCK_CURL_LOG" ]; then' \
' printf "%s\n" "$*" >> "$MOCK_CURL_LOG"' \
fi \
'if [ -n "$MOCK_CURL_HANDLER" ] && [ -x "$MOCK_CURL_HANDLER" ]; then' \
' exec "$MOCK_CURL_HANDLER" "$@"' \
fi \
'if [ -n "$MOCK_CURL_DELAY" ]; then' \
' sleep "$MOCK_CURL_DELAY"' \
fi \
'if [ -n "$MOCK_CURL_STDERR" ]; then' \
' printf "%s\n" "$MOCK_CURL_STDERR" >&2' \
fi \
'if [ -f "$MOCK_CURL_BODY_FILE" ]; then' \
' cat "$MOCK_CURL_BODY_FILE"' \
'elif [ -n "$MOCK_CURL_BODY" ]; then' \
' printf "%s\n" "$MOCK_CURL_BODY"' \
fi \
'exit "${MOCK_CURL_STATUS:-0}"' >$MOCK_DIR/curl
chmod +x $MOCK_DIR/curl
# 2. Mock git shim
printf '%s\n' \
'#!/bin/sh' \
'if [ -n "$MOCK_GIT_LOG" ]; then' \
' printf "%s\n" "$*" >> "$MOCK_GIT_LOG"' \
fi \
'if [ -n "$MOCK_GIT_HANDLER" ] && [ -x "$MOCK_GIT_HANDLER" ]; then' \
' exec "$MOCK_GIT_HANDLER" "$@"' \
fi \
'for a in "$@"; do' \
' if [ "$a" = "fetch" ] && [ -n "$MOCK_GIT_FAIL_FETCH" ]; then' \
' echo "fatal: unable to access: Could not resolve host" >&2' \
' exit "${MOCK_GIT_FETCH_STATUS:-128}"' \
' fi' \
' if [ "$a" = "pull" ] && [ -n "$MOCK_GIT_FAIL_PULL" ]; then' \
' echo "fatal: unable to access: Could not resolve host" >&2' \
' exit "${MOCK_GIT_PULL_STATUS:-128}"' \
' fi' \
' if [ "$a" = "clone" ] && [ -n "$MOCK_GIT_FAIL_CLONE" ]; then' \
' echo "fatal: unable to access: Could not resolve host" >&2' \
' exit "${MOCK_GIT_CLONE_STATUS:-128}"' \
' fi' \
' if [ "$a" = "ls-remote" ] && [ -n "$MOCK_GIT_FAIL_LS_REMOTE" ]; then' \
' echo "fatal: unable to access: Could not resolve host" >&2' \
' exit "${MOCK_GIT_LS_REMOTE_STATUS:-128}"' \
' fi' \
done \
"exec $real_git \"\$@\"" >$MOCK_DIR/git
chmod +x $MOCK_DIR/git
# 3. Mock bd CLI
printf '%s\n' \
'#!/bin/sh' \
'if [ "$1" = "create" ]; then' \
' echo "Created issue bd-99"' \
' echo "{\"id\":\"bd-99\"}" >> .beads/issues.jsonl' \
' exit 0' \
'elif [ "$1" = "sync" ]; then' \
' exit 0' \
fi \
'exit 0' >$MOCK_DIR/bd
chmod +x $MOCK_DIR/bd
# 4. Mock qrencode
printf '%s\n' \
'#!/bin/sh' \
'if [ -n "$MOCK_QRENCODE_LOG" ]; then' \
' printf "%s\n" "$*" >> "$MOCK_QRENCODE_LOG"' \
fi \
'echo "LOCAL_QRENCODE: $*"' \
'exit 0' >$MOCK_DIR/qrencode
chmod +x $MOCK_DIR/qrencode
# Prepend MOCK_DIR to PATH so mocks take precedence
set -gx PATH $MOCK_DIR $PATH
function reset_mocks
set -e MOCK_CURL_STATUS
set -e MOCK_CURL_BODY
set -e MOCK_CURL_BODY_FILE
set -e MOCK_CURL_STDERR
set -e MOCK_CURL_DELAY
set -e MOCK_CURL_LOG
set -e MOCK_CURL_HANDLER
set -e MOCK_GIT_FAIL_FETCH
set -e MOCK_GIT_FETCH_STATUS
set -e MOCK_GIT_FAIL_PULL
set -e MOCK_GIT_PULL_STATUS
set -e MOCK_GIT_FAIL_CLONE
set -e MOCK_GIT_CLONE_STATUS
set -e MOCK_GIT_FAIL_LS_REMOTE
set -e MOCK_GIT_LS_REMOTE_STATUS
set -e MOCK_GIT_LOG
set -e MOCK_GIT_HANDLER
set -e MOCK_QRENCODE_LOG
end
function cleanup
for d in $TMPDIRS
test -n "$d"; and command rm -rf $d
end
end
# ─────────────────────────────────────────────────────────────────────────────
# 1. gi (gitignore generator)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: gi"
reset_mocks
set -gx MOCK_CURL_BODY "c,cpp,python,rust"
set -l list_out (gi -l)
check "gi -l: returns API target list" "c,cpp,python,rust" "$list_out"
# Complete network drop on list
set -gx MOCK_CURL_STATUS 7
set -gx MOCK_CURL_BODY ""
gi -l >/dev/null 2>&1
check "gi -l: returns 0 per contract" 0 $status
# stdout mode with valid content
reset_mocks
set -gx MOCK_CURL_BODY "# Python gitignore\n*.pyc\n__pycache__/"
set -l stdout_out (gi -s python)
check "gi -s: prints fetched patterns to stdout" "# Python gitignore\n*.pyc\n__pycache__/" "$stdout_out"
# stdout mode with network failure / 404 (curl exit 22)
reset_mocks
set -gx MOCK_CURL_STATUS 22
set -gx MOCK_CURL_BODY ""
gi -s invalid_target >/dev/null 2>&1
check "gi -s: API failure returns 1" 1 $status
# Append mode outside git repository
reset_mocks
set -l non_git_dir (mktemp -d)
set -ga TMPDIRS $non_git_dir
begin
set -l prev_pwd $PWD
builtin cd $non_git_dir
gi python >/dev/null 2>&1
set -l rc $status
builtin cd $prev_pwd
check "gi <target>: outside git repo returns 1" 1 $rc
end
# Append mode inside git repo + deduplication
reset_mocks
set -l repo (new_repo)
set -gx MOCK_CURL_BODY "# Python gitignore\n*.pyc"
begin
set -l prev_pwd $PWD
builtin cd $repo
gi python >/dev/null 2>&1
check "gi python: first run returns 0" 0 $status
check "gi python: creates .gitignore" true (test -f "$repo/.gitignore"; and echo true; or echo false)
set -l first_lines (count (command cat "$repo/.gitignore"))
# Second run with identical pattern triggers deduplication
gi python >/dev/null 2>&1
check "gi python: second run returns 0" 0 $status
set -l second_lines (count (command cat "$repo/.gitignore"))
check "gi python: deduplication prevents duplicate append" $first_lines $second_lines
# Network failure during append skips modifying file
set -gx MOCK_CURL_STATUS 7
gi ruby >/dev/null 2>&1
set -l third_lines (count (command cat "$repo/.gitignore"))
check "gi: network drop during append preserves file" $second_lines $third_lines
builtin cd $prev_pwd
end
# ─────────────────────────────────────────────────────────────────────────────
# 2. gip, gip4, gip6 (IP resolution)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: gip, gip4, gip6"
# Create a specialized curl handler for gip IPv4/IPv6 dispatch
set -l gip_handler $MOCK_DIR/gip_curl.sh
printf '%s\n' \
'#!/bin/sh' \
'for a in "$@"; do' \
' if [ "$a" = "-4" ]; then' \
' if [ -n "$MOCK_FAIL_IPV4" ]; then exit "${MOCK_IPV4_STATUS:-28}"; fi' \
' echo "198.51.100.1"' \
' exit 0' \
' fi' \
' if [ "$a" = "-6" ]; then' \
' if [ -n "$MOCK_FAIL_IPV6" ]; then exit "${MOCK_IPV6_STATUS:-28}"; fi' \
' echo "2001:db8::1"' \
' exit 0' \
' fi' \
done \
'exit 0' >$gip_handler
chmod +x $gip_handler
reset_mocks
set -gx MOCK_CURL_HANDLER $gip_handler
# gip: both IPv4 and IPv6 succeed
set -l out_both (gip)
check "gip: both succeed outputs IPv4" true (string match -q '*IPv4: 198.51.100.1*' -- $out_both; and echo true; or echo false)
check "gip: both succeed outputs IPv6" true (string match -q '*IPv6: 2001:db8::1*' -- $out_both; and echo true; or echo false)
# gip: IPv4 timeout / failure, IPv6 success
set -gx MOCK_FAIL_IPV4 1
set -l out_v4_fail (gip)
check "gip: IPv4 failure reports 'Not detected'" true (string match -q '*IPv4: Not detected*' -- $out_v4_fail; and echo true; or echo false)
check "gip: IPv4 failure still reports IPv6" true (string match -q '*IPv6: 2001:db8::1*' -- $out_v4_fail; and echo true; or echo false)
# gip: IPv4 success, IPv6 failure
set -e MOCK_FAIL_IPV4
set -gx MOCK_FAIL_IPV6 1
set -l out_v6_fail (gip)
check "gip: IPv6 failure reports IPv4" true (string match -q '*IPv4: 198.51.100.1*' -- $out_v6_fail; and echo true; or echo false)
check "gip: IPv6 failure reports 'Not detected'" true (string match -q '*IPv6: Not detected*' -- $out_v6_fail; and echo true; or echo false)
# gip: complete network drop (both fail)
set -gx MOCK_FAIL_IPV4 1
set -gx MOCK_FAIL_IPV6 1
set -l out_all_fail (gip)
check "gip: complete drop reports IPv4 Not detected" true (string match -q '*IPv4: Not detected*' -- $out_all_fail; and echo true; or echo false)
check "gip: complete drop reports IPv6 Not detected" true (string match -q '*IPv6: Not detected*' -- $out_all_fail; and echo true; or echo false)
# gip4: success
set -e MOCK_FAIL_IPV4
set -l out_gip4 (gip4)
check "gip4: success prints IP" "198.51.100.1" "$out_gip4"
check "gip4: success exits 0" 0 $status
# gip4: failure
set -gx MOCK_FAIL_IPV4 1
set -gx MOCK_IPV4_STATUS 7
gip4 >/dev/null 2>&1
check "gip4: failure returns curl status" 7 $status
# gip6: success
set -e MOCK_FAIL_IPV6
set -l out_gip6 (gip6)
check "gip6: success prints IPv6" "2001:db8::1" "$out_gip6"
check "gip6: success exits 0" 0 $status
# gip6: failure / unsupported network
set -gx MOCK_FAIL_IPV6 1
set -l out_gip6_err (gip6 2>&1)
check "gip6: failure exits 1" 1 $status
check "gip6: failure prints notice" true (string match -q '*IPv6 is currently unavailable*' -- $out_gip6_err; and echo true; or echo false)
# ─────────────────────────────────────────────────────────────────────────────
# 3. qr (QR code generator)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: qr"
reset_mocks
# Case A: Local qrencode tool is present
set -gx MOCK_QRENCODE_LOG (mktemp)
set -ga TMPDIRS $MOCK_QRENCODE_LOG
set -l qr_local (qr "hello-world")
check "qr: local qrencode is preferred offline" true (string match -q '*LOCAL_QRENCODE*' -- $qr_local; and echo true; or echo false)
check "qr: curl is never called when qrencode exists" 0 (test -f $MOCK_DIR/curl_log; and count (cat $MOCK_DIR/curl_log); or echo 0)
# Case B: Local qrencode is missing, falling back to curl
function type
if test "$argv[1]" = -q -a "$argv[2]" = qrencode
return 1
end
builtin type $argv
end
set -gx MOCK_CURL_BODY UTF8_QR_BODY
set -l qr_curl (qr "hello-curl")
check "qr: fallback to curl when qrencode is missing" UTF8_QR_BODY "$qr_curl"
# Network drop during curl fallback
set -gx MOCK_CURL_STATUS 7
set -gx MOCK_CURL_BODY ""
qr fail >/dev/null 2>&1
check "qr: curl network drop returns non-zero" 7 $status
# Argument-based curl fallback
set -gx MOCK_CURL_STATUS 0
set -gx MOCK_CURL_BODY TEXT_QR
set -l qr_arg (qr "arg-text")
check "qr: argument works via curl fallback" TEXT_QR "$qr_arg"
functions -e type
# ─────────────────────────────────────────────────────────────────────────────
# 4. bd-pull (Gitea issues sync)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: bd-pull"
reset_mocks
# Missing required arguments & env vars
bd-pull >/dev/null 2>&1
check "bd-pull: missing repo argument exits 1" 1 $status
begin
set -e GITEA_TOKEN
set -e GITEA_URL
bd-pull rootiest/test >/dev/null 2>&1
check "bd-pull: missing GITEA_TOKEN exits 1" 1 $status
set -gx GITEA_TOKEN test_token
bd-pull rootiest/test >/dev/null 2>&1
check "bd-pull: missing GITEA_URL exits 1" 1 $status
end
# Complete network drop / empty response
set -gx GITEA_TOKEN dummy_token
set -gx GITEA_URL "https://git.test"
set -gx MOCK_CURL_STATUS 7
set -gx MOCK_CURL_BODY ""
set -l out_drop (bd-pull rootiest/test)
check "bd-pull: network drop reports no unlinked issues" true (string match -q '*No unlinked issues found*' -- $out_drop; and echo true; or echo false)
# Empty JSON issue list
set -gx MOCK_CURL_STATUS 0
set -gx MOCK_CURL_BODY "[]"
set -l out_empty (bd-pull rootiest/test)
check "bd-pull: empty issue list handled cleanly" true (string match -q '*No unlinked issues found*' -- $out_empty; and echo true; or echo false)
# Issues already linked with [ID] prefix
set -gx MOCK_CURL_BODY '[{"number": 1, "title": "[bd-1] Already linked issue"}]'
set -l out_already (bd-pull rootiest/test)
check "bd-pull: already-linked issues skipped" true (string match -q '*No unlinked issues found*' -- $out_already; and echo true; or echo false)
# Unlinked issue found: creates bead, patches title, commits & pushes
set -l bd_remote (new_repo)
git -C $bd_remote config --bool core.bare true
set -l bd_repo (new_repo)
begin
set -l prev_pwd $PWD
builtin cd $bd_repo
git remote add origin $bd_remote
mkdir -p .beads
touch .beads/issues.jsonl
git add .beads/issues.jsonl
git commit -q -m "init beads"
git push -q -u origin main
set -gx MOCK_CURL_BODY '[{"number": 2, "title": "Web original issue without ID"}]'
set -l out_unlinked (bd-pull rootiest/test 2>&1)
check "bd-pull: unlinked issue is linked" true (string match -q '*Linked 1 issues*' -- $out_unlinked; and echo true; or echo false)
# Verify git commit was made
set -l last_msg (git log -1 --pretty=%s)
check "bd-pull: commits synced IDs to git" "chore: sync local IDs for web issues" "$last_msg"
builtin cd $prev_pwd
end
# ─────────────────────────────────────────────────────────────────────────────
# 5. _auto_pull_sync (background fast-forward)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: _auto_pull_sync"
reset_mocks
# Non-git directory
_auto_pull_sync /tmp >/dev/null 2>&1
check "_auto_pull_sync: non-git directory returns 1" 1 $status
# Git repo without upstream branch
set -l sync_repo (new_repo)
echo test >$sync_repo/file.txt
git -C $sync_repo add file.txt
git -C $sync_repo commit -q -m initial
_auto_pull_sync $sync_repo >/dev/null 2>&1
check "_auto_pull_sync: missing upstream returns 1" 1 $status
# Setup upstream branch for sync_repo
set -l sync_upstream (new_repo)
git -C $sync_upstream config --bool core.bare true
git -C $sync_repo remote add origin $sync_upstream
git -C $sync_repo push -q -u origin main >/dev/null 2>&1
# Dirty working tree (unstaged modifications)
echo dirty >>$sync_repo/file.txt
_auto_pull_sync $sync_repo >/dev/null 2>&1
check "_auto_pull_sync: dirty worktree returns 1" 1 $status
git -C $sync_repo checkout -q -- file.txt
# Dirty index (staged modifications)
echo staged >>$sync_repo/staged.txt
git -C $sync_repo add staged.txt
_auto_pull_sync $sync_repo >/dev/null 2>&1
check "_auto_pull_sync: dirty index returns 1" 1 $status
git -C $sync_repo reset -q HEAD staged.txt
command rm -f $sync_repo/staged.txt
# Clean repo, network failure on git fetch
set -gx MOCK_GIT_FAIL_FETCH 1
_auto_pull_sync $sync_repo >/dev/null 2>&1
check "_auto_pull_sync: fetch failure returns 1" 1 $status
# Clean repo, successful fast-forward
reset_mocks
# Add commit to upstream
set -l peer_clone (new_repo)
git -C $peer_clone clone -q $sync_upstream $peer_clone/work
echo "new commit" >$peer_clone/work/new.txt
git -C $peer_clone/work add new.txt
git -C $peer_clone/work commit -q -m "upstream work"
git -C $peer_clone/work push -q origin main
_auto_pull_sync $sync_repo >/dev/null 2>&1
check "_auto_pull_sync: clean fast-forward returns 0" 0 $status
check "_auto_pull_sync: changes merged into working tree" true (test -f $sync_repo/new.txt; and echo true; or echo false)
# ─────────────────────────────────────────────────────────────────────────────
# 6. gitup (fetch and status)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: gitup"
reset_mocks
begin
set -l prev_pwd $PWD
builtin cd /tmp
gitup >/dev/null 2>&1
check "gitup: outside git repo exits 1" 1 $status
set -l r (new_repo)
builtin cd $r
echo a >a && git add a && git commit -q -m a
# Network failure on git fetch
set -gx MOCK_GIT_FAIL_FETCH 1
gitup >/dev/null 2>&1
check "gitup: network drop during fetch exits non-zero" true (test $status -ne 0; and echo true; or echo false)
# Successful fetch
reset_mocks
set -l up_out (gitup 2>&1)
check "gitup: success exits 0" 0 $status
check "gitup: displays git status output" true (string match -q '*On branch main*' -- $up_out; and echo true; or echo false)
builtin cd $prev_pwd
end
# ─────────────────────────────────────────────────────────────────────────────
# 7. git-clean (fetch --prune and delete orphaned branches)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: git-clean"
reset_mocks
git-clean --help >/dev/null 2>&1
check "git-clean: --help exits 0" 0 $status
begin
set -l prev_pwd $PWD
set -l r (new_repo)
builtin cd $r
echo a >a && git add a && git commit -q -m a
# Network drop during git fetch --prune
set -gx MOCK_GIT_FAIL_FETCH 1
# git-clean continues to local cleanup even if fetch fails
set -l clean_out (git-clean 2>&1)
check "git-clean: network drop handled, reports tidy" true (string match -q '*No orphaned branches found*' -- $clean_out; and echo true; or echo false)
# Orphaned branch detection and cleanup
reset_mocks
# Simulate an orphaned branch with [gone] tracking
git branch orphaned-feat
# Create git-clean handler that simulates git branch -vv showing ': gone]'
set -l clean_git_handler $MOCK_DIR/git_clean_shim.sh
printf '%s\n' \
'#!/bin/sh' \
'for a in "$@"; do' \
' if [ "$a" = "-vv" ]; then' \
' echo " main 1234567 [origin/main] initial"' \
' echo " orphaned-feat abcdef0 [origin/orphaned-feat: gone] feature"' \
' exit 0' \
' fi' \
done \
"exec $real_git \"\$@\"" >$clean_git_handler
chmod +x $clean_git_handler
set -gx MOCK_GIT_HANDLER $clean_git_handler
set -l clean_del_out (git-clean 2>&1)
check "git-clean: detects and deletes orphaned branch" true (string match -q '*Deleting orphaned local branches*' -- $clean_del_out; and echo true; or echo false)
check "git-clean: orphaned branch was deleted" false (git rev-parse --verify --quiet orphaned-feat >/dev/null 2>&1; and echo true; or echo false)
# Regression: git branch -vv marks a branch checked out in ANOTHER
# linked worktree with '+', not '*'. Only '*' used to be stripped, so
# a gone branch shown with '+' left a bogus "+" entry that then failed
# to delete ("branch '+' not found").
reset_mocks
git branch orphaned-worktree-feat
set -l clean_git_handler2 $MOCK_DIR/git_clean_shim2.sh
printf '%s\n' \
'#!/bin/sh' \
'for a in "$@"; do' \
' if [ "$a" = "-vv" ]; then' \
' echo " main 1234567 [origin/main] initial"' \
' echo "+ orphaned-worktree-feat abcdef0 [origin/orphaned-worktree-feat: gone] feature"' \
' exit 0' \
' fi' \
done \
"exec $real_git \"\$@\"" >$clean_git_handler2
chmod +x $clean_git_handler2
set -gx MOCK_GIT_HANDLER $clean_git_handler2
set -l plus_out (git-clean 2>&1)
check "git-clean: '+' worktree marker does not leak into a branch name" false (string match -q "*branch '+' not found*" -- $plus_out; and echo true; or echo false)
check "git-clean: '+'-marked gone branch is still deleted" false (git rev-parse --verify --quiet orphaned-worktree-feat >/dev/null 2>&1; and echo true; or echo false)
reset_mocks
builtin cd $prev_pwd
end
# ─────────────────────────────────────────────────────────────────────────────
# 8. config-update (configuration repository sync)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: config-update"
reset_mocks
config-update --help >/dev/null 2>&1
check "config-update: --help exits 0" 0 $status
# Non-git CONFIG_DIR (using mock handler)
set -l cfg_update_handler $MOCK_DIR/cfg_update_shim.sh
printf '%s\n' \
'#!/bin/sh' \
'for a in "$@"; do' \
' if [ "$a" = "fetch" ] && [ -n "$MOCK_CFG_FAIL_FETCH" ]; then' \
' exit 1' \
' fi' \
done \
"exec $real_git \"\$@\"" >$cfg_update_handler
chmod +x $cfg_update_handler
set -gx MOCK_GIT_HANDLER $cfg_update_handler
# Network failure on upstream fetch
set -l fake_home (mktemp -d)
set -ga TMPDIRS $fake_home
set -l fake_fish_cfg "$fake_home/.config/fish"
mkdir -p (dirname $fake_fish_cfg)
set -l cfg_repo (new_repo)
command cp -r $cfg_repo $fake_fish_cfg
# Test config-update against fake_fish_cfg by setting HOME
begin
set -lx HOME $fake_home
set -gx MOCK_CFG_FAIL_FETCH 1
config-update >/dev/null 2>&1
check "config-update: network fetch failure returns 1" 1 $status
end
# ─────────────────────────────────────────────────────────────────────────────
# 9. repo-open (origin URL normalization and browser deep-linking)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: repo-open"
reset_mocks
repo-open --help >/dev/null 2>&1
check "repo-open: --help exits 0" 0 $status
# Outside git repo
begin
set -l prev_pwd $PWD
builtin cd /tmp
repo-open -p >/dev/null 2>&1
check "repo-open: outside git repo returns 1" 1 $status
builtin cd $prev_pwd
end
# In git repo without origin
set -l r_no_origin (new_repo)
begin
set -l prev_pwd $PWD
builtin cd $r_no_origin
repo-open -p >/dev/null 2>&1
check "repo-open: without origin remote returns 1" 1 $status
builtin cd $prev_pwd
end
# In git repo with GitHub origin remote
set -l r_gh (new_repo)
git -C $r_gh remote add origin "https://github.com/rootiest/fish-config.git"
echo init >$r_gh/file && git -C $r_gh add file && git -C $r_gh commit -q -m init
begin
set -l prev_pwd $PWD
builtin cd $r_gh
# Remote ls-remote network failure falls back to default branch
set -gx MOCK_GIT_FAIL_LS_REMOTE 1
set -l url_fallback (repo-open -p)
check "repo-open: ls-remote network drop falls back to default branch" "https://github.com/rootiest/fish-config" "$url_fallback"
# With ls-remote success for current branch
reset_mocks
set -l gh_ls_handler $MOCK_DIR/gh_ls_shim.sh
printf '%s\n' \
'#!/bin/sh' \
'for a in "$@"; do' \
' if [ "$a" = "ls-remote" ]; then' \
' echo "abcdef01 refs/heads/main"' \
' exit 0' \
' fi' \
done \
"exec $real_git \"\$@\"" >$gh_ls_handler
chmod +x $gh_ls_handler
set -gx MOCK_GIT_HANDLER $gh_ls_handler
# Test gitlab provider normalization
git remote set-url origin "git@gitlab.com:rootiest/fish-config.git"
git checkout -q -b feat-test
set -l url_gl (repo-open -p)
check "repo-open: gitlab ssh url normalized with branch tree" "https://gitlab.com/rootiest/fish-config/-/tree/feat-test" "$url_gl"
# Test gitea provider normalization
git remote set-url origin "https://git.rootiest.dev/rootiest/fish-config.git"
git config browse.provider gitea
set -l url_gitea (repo-open -p)
check "repo-open: gitea url normalized with src/branch" "https://git.rootiest.dev/rootiest/fish-config/src/branch/feat-test" "$url_gitea"
builtin cd $prev_pwd
end
# ─────────────────────────────────────────────────────────────────────────────
# 10. fzf-update (fzf install / git pull)
# ─────────────────────────────────────────────────────────────────────────────
section "network isolation: fzf-update"
reset_mocks
# When ~/.fzf exists, git pull fails due to network drop
set -l fake_home_fzf (mktemp -d)
set -ga TMPDIRS $fake_home_fzf
mkdir -p $fake_home_fzf/.fzf
begin
set -lx HOME $fake_home_fzf
set -gx MOCK_GIT_FAIL_PULL 1
fzf-update >/dev/null 2>&1
check "fzf-update: git pull network drop returns non-zero" true (test $status -ne 0; and echo true; or echo false)
end
# When ~/.fzf does not exist, git clone fails due to network drop
set -l fake_home_no_fzf (mktemp -d)
set -ga TMPDIRS $fake_home_no_fzf
begin
set -lx HOME $fake_home_no_fzf
set -gx MOCK_GIT_FAIL_CLONE 1
fzf-update >/dev/null 2>&1
check "fzf-update: git clone network drop returns non-zero" true (test $status -ne 0; and echo true; or echo false)
end
# ─────────────────────────────────────────────────────────────────────────────
# Teardown and Final Report
# ─────────────────────────────────────────────────────────────────────────────
cleanup
report