test: expand test coverage across core functions, enforce strict fish_indent linting, and add network isolation harnesses #143

Merged
rootiest merged 8 commits from test/network-edge-case-coverage into main 2026-09-10 01:00:47 +00:00
64 changed files with 3068 additions and 978 deletions
+2 -3
View File
@@ -108,7 +108,6 @@ function __bd_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __bd_comp_results
# - Return false if file completion should be performed
@@ -215,7 +214,7 @@ end
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "bd"
if type -q bd
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
@@ -226,7 +225,7 @@ end
complete -c bd -e
# this will get called after the two calls below and clear the $__bd_perform_completion_once_result global
complete -c bd -n '__bd_clear_perform_completion_once_result'
complete -c bd -n __bd_clear_perform_completion_once_result
# The call to __bd_prepare_completions will setup __bd_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
+22 -22
View File
@@ -1,14 +1,14 @@
# fish completion for copilot
# Generated by `copilot completion fish`. Do not edit by hand.
complete -c copilot -n '__fish_use_subcommand' -f -a 'login' -d 'Authenticate with Copilot'
complete -c copilot -n '__fish_use_subcommand' -f -a 'help' -d 'Display help information'
complete -c copilot -n '__fish_use_subcommand' -f -a 'init' -d 'Initialize Copilot instructions'
complete -c copilot -n '__fish_use_subcommand' -f -a 'update' -d 'Download the latest version'
complete -c copilot -n '__fish_use_subcommand' -f -a 'version' -d 'Display version information'
complete -c copilot -n '__fish_use_subcommand' -f -a 'plugin' -d 'Manage plugins'
complete -c copilot -n '__fish_use_subcommand' -f -a 'mcp' -d 'Manage MCP servers'
complete -c copilot -n '__fish_use_subcommand' -f -a 'completion' -d 'Generate a shell completion script'
complete -c copilot -n __fish_use_subcommand -f -a login -d 'Authenticate with Copilot'
complete -c copilot -n __fish_use_subcommand -f -a help -d 'Display help information'
complete -c copilot -n __fish_use_subcommand -f -a init -d 'Initialize Copilot instructions'
complete -c copilot -n __fish_use_subcommand -f -a update -d 'Download the latest version'
complete -c copilot -n __fish_use_subcommand -f -a version -d 'Display version information'
complete -c copilot -n __fish_use_subcommand -f -a plugin -d 'Manage plugins'
complete -c copilot -n __fish_use_subcommand -f -a mcp -d 'Manage MCP servers'
complete -c copilot -n __fish_use_subcommand -f -a completion -d 'Generate a shell completion script'
complete -c copilot -l version -s v -f -d 'show version information'
complete -c copilot -l interactive -s i -r -d 'Start interactive mode and automatically execute this prompt'
complete -c copilot -l prompt -s p -r -d 'Execute a prompt in non-interactive mode (exits after completion)'
@@ -75,22 +75,22 @@ complete -c copilot -l acp -f -d 'Start as Agent Client Protocol server'
complete -c copilot -l remote -f -d 'Enable remote control of your session from GitHub web and mobile'
complete -c copilot -l no-remote -f -d 'Disable remote control of your session from GitHub web and mobile'
complete -c copilot -n '__fish_seen_subcommand_from login' -l host -r -d 'GitHub host URL (default: https://github.com)'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'install' -d 'Install a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'uninstall' -d 'Uninstall a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'update' -d 'Update a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'list' -d 'List installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a 'marketplace' -d 'Manage plugin marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a install -d 'Install a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a uninstall -d 'Uninstall a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a update -d 'Update a plugin'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a list -d 'List installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin' -f -a marketplace -d 'Manage plugin marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from update' -l all -f -d 'Update all installed plugins'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'add' -d 'Add a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'remove' -d 'Remove a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'list' -d 'List registered marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'browse' -d 'Browse plugins in a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a 'update' -d 'Update marketplace plugin catalogs'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a add -d 'Add a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a remove -d 'Remove a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a list -d 'List registered marketplaces'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a browse -d 'Browse plugins in a marketplace'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace' -f -a update -d 'Update marketplace plugin catalogs'
complete -c copilot -n '__fish_seen_subcommand_from plugin; and __fish_seen_subcommand_from marketplace; and __fish_seen_subcommand_from remove' -l force -s f -f -d 'Force removal even if plugins are installed'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'list' -d 'List configured MCP servers'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'get' -d 'Show server details'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'add' -d 'Add an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a 'remove' -d 'Remove an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a list -d 'List configured MCP servers'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a get -d 'Show server details'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a add -d 'Add an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp' -f -a remove -d 'Remove an MCP server'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from list' -l json -f -d 'Output as JSON'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from get' -l json -f -d 'Output as JSON'
complete -c copilot -n '__fish_seen_subcommand_from mcp; and __fish_seen_subcommand_from get' -l show-secrets -f -d 'Show full environment variable and header values (masked by default)'
+44 -44
View File
@@ -24,15 +24,15 @@ function __fish_deadbranch_using_subcommand
contains -- $cmd[1] $argv
end
complete -c deadbranch -n "__fish_deadbranch_needs_command" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "list" -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "clean" -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "config" -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "backup" -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "stats" -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "completions" -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n __fish_deadbranch_needs_command -s h -l help -d 'Print help'
complete -c deadbranch -n __fish_deadbranch_needs_command -s V -l version -d 'Print version'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a list -d 'List stale branches'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a clean -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a config -d 'Manage configuration'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a backup -d 'Manage backups'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a stats -d 'Show repository branch statistics'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a completions -d 'Generate shell completion scripts'
complete -c deadbranch -n __fish_deadbranch_needs_command -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -s d -l days -d 'Only show branches older than N days (default: from config or 30)' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -l local -d 'Only show local branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand list" -l remote -d 'Only show remote branches'
@@ -51,11 +51,11 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand clean" -s h -l hel
complete -c deadbranch -n "__fish_deadbranch_using_subcommand clean" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and not __fish_seen_subcommand_from set show edit reset help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from set" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from set" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from show" -s h -l help -d 'Print help'
@@ -64,18 +64,18 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from edit" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from reset" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from reset" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand config; and __fish_seen_subcommand_from help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a clean -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and not __fish_seen_subcommand_from list restore stats clean help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -l repo -d 'Show backups for a specific repository by name' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -l current -d 'Only show backups for current repository'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from list" -s h -l help -d 'Print help'
@@ -94,28 +94,28 @@ complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s y -l yes -d 'Skip confirmation prompt'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from clean" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a clean -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand backup; and __fish_seen_subcommand_from help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s d -l days -d 'Treat branches older than N days as stale (default: from config or 30)' -r
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand stats" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand completions" -s h -l help -d 'Print help'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand completions" -s V -l version -d 'Print version'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "list" -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "clean" -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "config" -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "backup" -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "stats" -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "completions" -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "set" -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "show" -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "edit" -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a "reset" -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "list" -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "restore" -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "stats" -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a "clean" -d 'Remove old backups, keeping the most recent ones'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a list -d 'List stale branches'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a clean -d 'Delete stale branches (merged only by default, use --force for unmerged)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a config -d 'Manage configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a backup -d 'Manage backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a stats -d 'Show repository branch statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a completions -d 'Generate shell completion scripts'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and not __fish_seen_subcommand_from list clean config backup stats completions help" -f -a help -d 'Print this message or the help of the given subcommand(s)'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a set -d 'Set a configuration value'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a show -d 'Show current configuration'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a edit -d 'Open config file in $EDITOR'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from config" -f -a reset -d 'Reset configuration to defaults'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a list -d 'List available backups'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a restore -d 'Restore a branch from backup'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a stats -d 'Show backup storage statistics'
complete -c deadbranch -n "__fish_deadbranch_using_subcommand help; and __fish_seen_subcommand_from backup" -f -a clean -d 'Remove old backups, keeping the most recent ones'
+2 -3
View File
@@ -108,7 +108,6 @@ function __ov_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __ov_comp_results
# - Return false if file completion should be performed
@@ -215,7 +214,7 @@ end
# so we can properly delete any completions provided by another script.
# Only do this if the program can be found, or else fish may print some errors; besides,
# the existing completions will only be loaded if the program can be found.
if type -q "ov"
if type -q ov
# The space after the program name is essential to trigger completion for the program
# and not completion of the program name itself.
# Also, we use '> /dev/null 2>&1' since '&>' is not supported in older versions of fish.
@@ -226,7 +225,7 @@ end
complete -c ov -e
# this will get called after the two calls below and clear the $__ov_perform_completion_once_result global
complete -c ov -n '__ov_clear_perform_completion_once_result'
complete -c ov -n __ov_clear_perform_completion_once_result
# The call to __ov_prepare_completions will setup __ov_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
+1 -2
View File
@@ -125,7 +125,6 @@ function __tailscale_requires_order_preservation
return 1
end
# This function does two things:
# - Obtain the completions and store them in the global __tailscale_comp_results
# - Return false if file completion should be performed
@@ -242,7 +241,7 @@ end
complete -c tailscale -e
# this will get called after the two calls below and clear the $__tailscale_perform_completion_once_result global
complete -c tailscale -n '__tailscale_clear_perform_completion_once_result'
complete -c tailscale -n __tailscale_clear_perform_completion_once_result
# The call to __tailscale_prepare_completions will setup __tailscale_comp_results
# which provides the program's completion choices.
# If this doesn't require order preservation, we don't use the -k flag
+65 -66
View File
@@ -80,69 +80,68 @@ set -g __fish_config_op_registry_keys \
"zoxide:"
set -g __fish_config_op_registry_values \
"autoexec/venv" \
"logging/terminal-capture" \
"autoexec/sync" \
"logging/multiplexer-capture" \
"integrations/terminal-abbrs" \
"overrides/key-bindings" \
"aliases/dev-tools" \
"autoexec/sync" \
"overrides/key-bindings" \
"aliases/shell-tools" \
"overrides/key-bindings" \
"aliases/filesystem" \
"aliases/dev-tools" \
"aliases/filesystem" \
"overrides/key-bindings" \
"overrides/environment" \
"overrides/environment" \
"overrides/key-bindings" \
"greeting/greeting-message" \
"overrides/environment" \
"overrides/environment" \
"overrides/privacy" \
"overrides/key-bindings" \
"integrations/notifications" \
"aliases/filesystem" \
"aliases/dev-tools" \
"autoexec/plugin-management" \
"greeting/first-run" \
"overrides/prompt" \
"aliases/shell-tools" \
"integrations/history-logs" \
"overrides/key-bindings" \
"logging/terminal-capture" \
"logging/terminal-capture" \
"aliases/shell-tools" \
"integrations/history-logs" \
"aliases/filesystem" \
"aliases/filesystem" \
"aliases/filesystem" \
"autoexec/pkg-wrappers" \
"logging/pkg-logs" \
"aliases/network" \
"overrides/key-bindings" \
"aliases/search" \
"aliases/filesystem" \
"overrides/key-bindings" \
"logging/terminal-capture" \
"integrations/window-mgmt" \
"integrations/window-mgmt" \
"aliases/network" \
"overrides/prompt" \
"integrations/window-mgmt" \
"overrides/prompt" \
"logging/multiplexer-capture" \
"aliases/monitor" \
"aliases/filesystem" \
"overrides/key-bindings" \
"overrides/environment" \
"integrations/pkg-upgrade" \
"autoexec/telemetry" \
"integrations/notifications" \
"autoexec/pkg-wrappers" \
"logging/pkg-logs" \
"aliases/network" \
"aliases/filesystem"
autoexec/venv \
logging/terminal-capture \
autoexec/sync \
logging/multiplexer-capture \
integrations/terminal-abbrs \
overrides/key-bindings \
aliases/dev-tools \
autoexec/sync \
overrides/key-bindings \
aliases/shell-tools \
overrides/key-bindings \
aliases/filesystem \
aliases/dev-tools \
aliases/filesystem \
overrides/key-bindings \
overrides/environment \
overrides/environment \
overrides/key-bindings \
greeting/greeting-message \
overrides/environment \
overrides/environment \
overrides/privacy \
overrides/key-bindings \
integrations/notifications \
aliases/filesystem \
aliases/dev-tools \
autoexec/plugin-management \
greeting/first-run \
overrides/prompt \
aliases/shell-tools \
integrations/history-logs \
overrides/key-bindings \
logging/terminal-capture \
logging/terminal-capture \
aliases/shell-tools \
integrations/history-logs \
aliases/filesystem \
aliases/filesystem \
aliases/filesystem \
autoexec/pkg-wrappers \
logging/pkg-logs \
aliases/network \
overrides/key-bindings \
aliases/search \
aliases/filesystem \
overrides/key-bindings \
logging/terminal-capture \
integrations/window-mgmt \
integrations/window-mgmt \
aliases/network \
overrides/prompt \
integrations/window-mgmt \
overrides/prompt \
logging/multiplexer-capture \
aliases/monitor \
aliases/filesystem \
overrides/key-bindings \
overrides/environment \
integrations/pkg-upgrade \
autoexec/telemetry \
integrations/notifications \
autoexec/pkg-wrappers \
logging/pkg-logs \
aliases/network \
aliases/filesystem
+6 -2
View File
@@ -17,7 +17,9 @@ function expand_bang_all --description 'Execute expand_bang_all'
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
if test -z "$token"
set token (commandline -t)
end
set -l tokens (string split -n " " -- $history[1])
if test (count $tokens) -gt 1
@@ -46,7 +48,9 @@ function expand_bang_minus_n --description 'Execute expand_bang_minus_n'
__fish_config_op_enabled (status basename); or return 1
set -l token $argv[1]
if test -z "$token"; set token (commandline -t); end
if test -z "$token"
set token (commandline -t)
end
# Extract the number from the regex match
if string match -qr '!-(\d+)' -- "$token"
+5 -6
View File
@@ -60,7 +60,7 @@ printf '%s\n' \
"cmd_str=\"$_paru_real\"" \
'for arg in "$@"; do' \
' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \
'done' \
done \
'script -q -e -c "$cmd_str" "$log_file"' \
'exit_code=$?' \
'' \
@@ -70,19 +70,18 @@ printf '%s\n' \
'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \
'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \
' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \
'else' \
else \
' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \
'fi' \
fi \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/paru_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
done \
'' \
'exit $exit_code' \
> $_paru_wrapper
'exit $exit_code' >$_paru_wrapper
chmod +x $_paru_wrapper
+4 -3
View File
@@ -28,7 +28,7 @@ function __register_wakatime_fish_before_exec -e fish_postexec
set exec_command_str (string split -f1 ' ' "$argv")
if test "$exec_command_str" = 'exit'
if test "$exec_command_str" = exit
return 0
end
@@ -49,8 +49,9 @@ function __register_wakatime_fish_before_exec -e fish_postexec
if git rev-parse --is-inside-work-tree &>/dev/null
set project (basename (git rev-parse --show-toplevel))
else
set project "Terminal"
set project Terminal
end
$wakatime_path --write --plugin "$PLUGIN_NAME/$PLUGIN_VERSION" --entity-type app --project "$project" --entity "$exec_command_str" &> /dev/null&; disown
$wakatime_path --write --plugin "$PLUGIN_NAME/$PLUGIN_VERSION" --entity-type app --project "$project" --entity "$exec_command_str" &>/dev/null &
disown
end
+5 -6
View File
@@ -60,7 +60,7 @@ printf '%s\n' \
"cmd_str=\"$_yay_real\"" \
'for arg in "$@"; do' \
' cmd_str+=" $(printf '"'"'%q'"'"' "$arg")"' \
'done' \
done \
'script -q -e -c "$cmd_str" "$log_file"' \
'exit_code=$?' \
'' \
@@ -70,19 +70,18 @@ printf '%s\n' \
'cleaner="${XDG_CONFIG_HOME:-$HOME/.config}/fish/scripts/clean_progress_log.py"' \
'if command -v python3 >/dev/null 2>&1 && [[ -f "$cleaner" ]]; then' \
' python3 "$cleaner" < "$log_file" > "${log_file}.tmp" 2>/dev/null && mv "${log_file}.tmp" "$log_file" || rm -f "${log_file}.tmp"' \
'else' \
else \
' sed -i "/^Script \(started\|done\) on /d" "$log_file" 2>/dev/null || true' \
'fi' \
fi \
'' \
'max_files="${SCROLLBACK_HISTORY_MAX_FILES:-100}"' \
'mapfile -t logs < <(ls -1t "$log_dir"/yay_*.log 2>/dev/null)' \
'excess=$(( ${#logs[@]} - max_files ))' \
'for (( i = ${#logs[@]} - 1; i >= ${#logs[@]} - excess && i >= 0; i-- )); do' \
' rm -f "${logs[$i]}"' \
'done' \
done \
'' \
'exit $exit_code' \
> $_yay_wrapper
'exit $exit_code' >$_yay_wrapper
chmod +x $_yay_wrapper
+8 -2
View File
@@ -99,10 +99,16 @@ def render(registry: dict[str, list[str]]) -> str:
lines += [f" {k} \\" for k in quoted_keys[:-1]] + [f" {quoted_keys[-1]}"]
lines.append("")
values = ['"' + " ".join(registry[k]) + '"' for k in keys]
values = [
(
'"' + " ".join(registry[k]) + '"'
if any(c in " ".join(registry[k]) for c in ' \t*?[]"\'\\$')
else " ".join(registry[k])
)
for k in keys
]
lines.append("set -g __fish_config_op_registry_values \\")
lines += [f" {v} \\" for v in values[:-1]] + [f" {values[-1]}"]
lines.append("")
return "\n".join(lines) + "\n"
+3 -3
View File
@@ -28,9 +28,9 @@ function __config_settings_subcats --description 'List the sub-categories for on
case __fish_config_op_aliases
printf '%s\t%s\t%s\n' \
filesystem Filesystem "ls, cat, cd, du, mkdir, rm, mv, zoxide" \
search Search "rg" \
search Search rg \
network Network "ping, ssh, yt-dlp" \
monitor Monitor "top" \
monitor Monitor top \
shell-tools Shell-tools "bash, less, help" \
dev-tools Dev-tools "claude, edit, agy"
case __fish_config_op_autoexec
@@ -52,7 +52,7 @@ function __config_settings_subcats --description 'List the sub-categories for on
window-mgmt Window-mgmt "spwin, tab, split" \
notifications Notifications "done, WakaTime hook" \
history-logs History-logs "hist, logs" \
pkg-upgrade Pkg-upgrade "upgrade"
pkg-upgrade Pkg-upgrade upgrade
case __fish_config_op_logging
printf '%s\t%s\t%s\n' \
terminal-capture Term-capture "Kitty watcher, smart_exit scrollback" \
+13 -5
View File
@@ -75,7 +75,7 @@ function _fish_deps_install
set -l _major (fish --version 2>&1 | string match -r 'version (\d+)')[2]
if test -n "$_major"; and test "$_major" -lt 4
set needs_install 1
set upgrade_label "Upgrade"
set upgrade_label Upgrade
end
end
@@ -193,7 +193,9 @@ function _fish_deps_install
echo " Available methods:"
set -l m 1
for lbl in $method_labels
set_color brblack; echo -n " $m) "; set_color normal
set_color brblack
echo -n " $m) "
set_color normal
echo $lbl
set m (math $m + 1)
end
@@ -202,7 +204,9 @@ function _fish_deps_install
set chosen_method $methods[$_choice]
end
else
set_color brblack; echo " "(string lower $upgrade_label)"ing via $method_labels[1]"; set_color normal
set_color brblack
echo " "(string lower $upgrade_label)"ing via $method_labels[1]"
set_color normal
end
# Execute chosen method
@@ -329,14 +333,18 @@ function _fish_deps_install
if test $status -eq 0
set installed_any 1
set_color green; echo " $bin "(string lower $upgrade_label)"ed."; set_color normal
set_color green
echo " $bin "(string lower $upgrade_label)"ed."
set_color normal
if test "$bin" = fish
set_color yellow
echo " Fish upgraded — restart your shell to use the new version."
set_color normal
end
else
set_color red; echo " $bin "(string lower $upgrade_label)" failed."; set_color normal
set_color red
echo " $bin "(string lower $upgrade_label)" failed."
set_color normal
end
end
set i (math $i + 1)
+33 -11
View File
@@ -23,35 +23,57 @@ function _fish_deps_status
set -l _major (fish --version 2>&1 | string match -r 'version (\d+)')[2]
if test -n "$_major"; and test "$_major" -lt 4
set -l _ver (fish --version 2>&1 | string replace 'fish, ' '')
set_color yellow; echo -n " ⚠ "; set_color normal
set_color yellow
echo -n " ⚠ "
set_color normal
echo -n "$bin "
set_color brblack; echo "($_ver — upgrade to 4.0+ required)"; set_color normal
set_color brblack
echo "($_ver — upgrade to 4.0+ required)"
set_color normal
return
end
end
set_color green; echo -n " ✓ "; set_color normal
set_color green
echo -n " ✓ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Found at "(__fish_real_command $bin)")"; set_color normal
set_color brblack
echo "(Found at "(__fish_real_command $bin)")"
set_color normal
else if test "$tier" = req
set_color red; echo -n " ✗ "; set_color normal
set_color red
echo -n " ✗ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
else if test "$tier" = rec
set_color yellow; echo -n " ⚠ "; set_color normal
set_color yellow
echo -n " ⚠ "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
else
# opt / term / int: absence is expected and not alarming
set_color brblack; echo -n " "; set_color normal
set_color brblack
echo -n " "
set_color normal
echo -n "$bin "
set_color brblack; echo "(Not installed)"; set_color normal
set_color brblack
echo "(Not installed)"
set_color normal
end
end
for tier_label in "Required Dependencies:req" "Recommended Dependencies:rec" "Optional Dependencies:opt" "Terminal Emulators:term" "Integrations:int"
set -l label (string split : $tier_label)[1]
set -l tier (string split : $tier_label)[2]
set_color cyan; echo $label; set_color normal
set_color cyan
echo $label
set_color normal
set -l i 1
for bin in $_fdc_bins
if test "$_fdc_tiers[$i]" = $tier
+8 -4
View File
@@ -101,10 +101,14 @@ function _fish_deps_update
echo "Updating $bin..."
set -l _arch (uname -m)
switch $_arch
case x86_64; set _arch amd64
case aarch64 arm64; set _arch arm64
case armv7l; set _arch arm
case '*'; set _arch amd64
case x86_64
set _arch amd64
case aarch64 arm64
set _arch arm64
case armv7l
set _arch arm
case '*'
set _arch amd64
end
set -l _zip "wakatime-cli-linux-$_arch.zip"
set -l _bin_src "wakatime-cli-linux-$_arch"
-1
View File
@@ -7,4 +7,3 @@ function _puffer_fish_expand_bang
commandline --insert '!'
end
end
+2 -4
View File
@@ -893,8 +893,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped
command cp -rn "$stash/." "$entry/" 2>/dev/null
rm -rf "$stash"
end
printf 'renamed: %s → %s (%s)\n' "$prev_slug" "$slug" (date -I) \
>>"$entry/origin"
printf 'renamed: %s → %s (%s)\n' "$prev_slug" "$slug" (date -I) >>"$entry/origin"
# Only a link is dropped here, and only so the relink below has
# somewhere to put the new one. Usually $live is exactly that: a
# symlink at the old entry, now dangling. But the migration is
@@ -1078,8 +1077,7 @@ function agents-vault --description 'track curated agent memory in a host-scoped
end
set -l reached 1
if git -C "$vault" rev-parse --abbrev-ref --symbolic-full-name '@{u}' >/dev/null 2>&1
if not $gitnet git -C "$vault" pull --rebase --autostash -q \
>/dev/null 2>/dev/null
if not $gitnet git -C "$vault" pull --rebase --autostash -q >/dev/null 2>/dev/null
# Two unrelated failures land here and reporting them as
# one sends the user hunting for a conflict that never
# existed. A rebase that genuinely started and stopped
+4 -4
View File
@@ -52,10 +52,10 @@ function agy --wraps=agy --description 'agy wrapper: auto-initializes AGENTS/ su
agents-vault --quiet
for i in (seq (count $argv))
if test "$argv[$i]" = "-r"
set argv[$i] "-c"
else if test "$argv[$i]" = "--resume"
set argv[$i] "--continue"
if test "$argv[$i]" = -r
set argv[$i] -c
else if test "$argv[$i]" = --resume
set argv[$i] --continue
else if string match -q -- "--resume=*" "$argv[$i]"
set argv[$i] (string replace -- "--resume=" "--continue=" "$argv[$i]")
end
+12 -3
View File
@@ -25,11 +25,20 @@
function bd-pull --description 'Pull new Gitea issues into local Beads and link them'
__fish_help_header (status current-function) $argv; and return 0
if not set -q argv[1]; echo "Need repo owner/name"; return 1; end
if not set -q GITEA_TOKEN; echo "\$GITEA_TOKEN not set"; return 1; end
if not set -q argv[1]
echo "Need repo owner/name"
return 1
end
if not set -q GITEA_TOKEN
echo "\$GITEA_TOKEN not set"
return 1
end
set -l REPO $argv[1]
if not set -q GITEA_URL; echo "\$GITEA_URL not set"; return 1; end
if not set -q GITEA_URL
echo "\$GITEA_URL not set"
return 1
end
set -l IMPORT_COUNT 0
echo (set_color blue)"📡 Checking Gitea: $REPO..."(set_color normal)
+1 -2
View File
@@ -151,8 +151,7 @@ function logs --description 'Browse terminal log files interactively with fzf'
'case "$confirm" in [nN]) exit 0 ;; esac' \
"fish -c \"rm \$FILE\"" \
"grep -vF \"\$FILE\" $tmpfile > $tmpfile.new" \
"mv $tmpfile.new $tmpfile" \
> $deletescript
"mv $tmpfile.new $tmpfile" >$deletescript
chmod +x $deletescript
set -l selected (command cat $tmpfile | fzf \
+64 -16
View File
@@ -187,28 +187,52 @@ function pkg --description 'Install or remove packages via the system package ma
switch $pm
case paru yay
$pm -S $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case pacman
sudo pacman -S $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case apt
sudo apt install -y $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case dnf
sudo dnf install -y $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case zypper
sudo zypper install -y $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case brew
brew install $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case pkg
sudo pkg install -y $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case yum
sudo yum install -y $to_install
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
end
end
@@ -217,28 +241,52 @@ function pkg --description 'Install or remove packages via the system package ma
switch $pm
case paru yay
$pm -Rns $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case pacman
sudo pacman -Rns $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case apt
sudo apt remove -y $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case dnf
sudo dnf remove -y $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case zypper
sudo zypper remove -y $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case brew
brew uninstall $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case pkg
sudo pkg delete -y $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
case yum
sudo yum remove -y $to_remove
or begin; functions -e __pkg_is_installed; return $status; end
or begin
functions -e __pkg_is_installed
return $status
end
end
end
end
+4 -4
View File
@@ -62,8 +62,8 @@ function rand_string --description 'Generate random, memorable strings from cura
return 0
end
set -l sep "dash"
set -l casing "lower"
set -l sep dash
set -l casing lower
# Locate the words directory relative to this function
set -l words_dir ""
@@ -114,9 +114,9 @@ function rand_string --description 'Generate random, memorable strings from cura
set -l actual_sep $sep
switch $sep
case dash
set actual_sep "-"
set actual_sep -
case underscore
set actual_sep "_"
set actual_sep _
case dot
set actual_sep "."
case none empty
+1 -1
View File
@@ -38,7 +38,7 @@ function sbver --description 'Verifies Secure Boot status of EFI binaries using
# Flags
set brief_mode false
if test "$argv[1]" = "--brief"
if test "$argv[1]" = --brief
set brief_mode true
end
+1 -1
View File
@@ -23,7 +23,7 @@
# spark 1 1 2 5 14 42
# seq 64 | sort --random-sort | spark
# echo "3 7 2 9 1" | spark
function spark --description 'Sparklines'
function spark --description Sparklines
argparse --ignore-unknown --name=spark v/version h/help m/min= M/max= -- $argv || return
if set --query _flag_version[1]
+1 -1
View File
@@ -33,7 +33,7 @@
# set -U -a sponge_filters sponge_filter_secrets
function sponge_filter_secrets --argument-names command
# Find all exported variables with security-sensitive names
set -l sensitive_vars (set --names --export | string match --regex -- \
set -l sensitive_vars (set --names --export | string match --entire --regex -- \
'(?i)(?:TOKEN|PASSWORD|PASSWD|SECRET|API[_-]KEY|PRIVATE[_-]KEY|ACCESS[_-]KEY|AUTH[_-]KEY|CREDENTIAL|KOPIA_PASSWORD)')
for var in $sensitive_vars
+1 -1
View File
@@ -62,7 +62,7 @@ function yt-dlp --description 'yt-dlp with embedding + SponsorBlock defaults'
# --sponsorblock-remove all
# Skip if the user set their own remove (bare or --opt=value form), or
# disabled SponsorBlock entirely with --no-sponsorblock.
if not string match -q -- '--sponsorblock-remove' $argv
if not string match -q -- --sponsorblock-remove $argv
and not string match -q -- '--sponsorblock-remove=*' $argv
and not contains -- --no-sponsorblock $argv
set -a extra --sponsorblock-remove all
+16 -12
View File
@@ -13,7 +13,6 @@
# - $FZF_ALT_C_COMMAND
# - $FZF_ALT_C_OPTS
# Key bindings
# ------------
# The oldest supported fish version is 3.1b1. To maintain compatibility, the
@@ -56,10 +55,10 @@ function fzf_key_bindings
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
else if test "$FZF_TMUX" = 1
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
echo fzf
end
end
@@ -85,10 +84,10 @@ function fzf_key_bindings
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
test "$tokens[1]" = --; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
test "$tokens[1]" = --; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
@@ -245,7 +244,7 @@ function fzf_key_bindings
\( "$v[3]" -gt 3 -o "$v[3]" -eq 3 -a \
\( -n "$v[4]" -a "$v[4]" -ge 3 \) \)
set -a -- FZF_DEFAULT_OPTS '--ansi'
set -a -- FZF_DEFAULT_OPTS --ansi
set -a -- FZF_DEFAULT_COMMAND '--color=always'
end
@@ -338,10 +337,10 @@ function fzf_completion_setup
test -n "$FZF_TMUX_HEIGHT"; or set -l FZF_TMUX_HEIGHT 40%
if test -n "$FZF_TMUX_OPTS"
echo "fzf-tmux $FZF_TMUX_OPTS -- "
else if test "$FZF_TMUX" = "1"
else if test "$FZF_TMUX" = 1
echo "fzf-tmux -d$FZF_TMUX_HEIGHT -- "
else
echo "fzf"
echo fzf
end
end
@@ -367,10 +366,10 @@ function fzf_completion_setup
switch "$tokens[1]"
case builtin command
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
test "$tokens[1]" = --; and set -e -- tokens[1]
case env
set -e -- tokens[1]
test "$tokens[1]" = "--"; and set -e -- tokens[1]
test "$tokens[1]" = --; and set -e -- tokens[1]
while string match -qr -- '^[\w]+=' "$tokens[1]"
set -e -- tokens[1]
end
@@ -451,7 +450,10 @@ function fzf_completion_setup
function __fzf_complete_native
set -l -- token (commandline -t)
set -l -- completions (eval complete -C \"$argv[1]\")
test -n "$completions"; or begin commandline -f repaint; return; end
test -n "$completions"; or begin
commandline -f repaint
return
end
# Calculate tabstop based on longest completion item (sample first 500 for performance)
set -l -- tabstop 20
@@ -498,7 +500,9 @@ function fzf_completion_setup
set -lx FZF_DEFAULT_COMMAND
set -l -- fzf_query (commandline -t | string escape)
set -l result
eval (__fzfcmd) --query=$fzf_query | while read -l r; set -a -- result $r; end
eval (__fzfcmd) --query=$fzf_query | while read -l r
set -a -- result $r
end
and if functions -q $post_func
commandline -rt -- (string collect -- $result | eval $post_func $args[2] | string join ' ')' '
else
+7 -4
View File
@@ -24,9 +24,12 @@ set -l mode bytes
set -l baseline main
for i in (seq (count $argv))
switch $argv[$i]
case --structural; set mode structural
case --startup; set mode startup
case --baseline; set baseline $argv[(math $i + 1)]
case --structural
set mode structural
case --startup
set mode startup
case --baseline
set baseline $argv[(math $i + 1)]
end
end
@@ -71,7 +74,7 @@ set -l cases \
"p --help" "pkg --help" "play-media --help" "qc --help" \
"rand_string --help" "replay --help" "repo-open --help" "scrub --help" \
"smart_exit --help" "spark --help" "y --help" \
"mkcd" "auto-pull remove __no_such_repo__" \
mkcd "auto-pull remove __no_such_repo__" \
"agents-init --no-such-flag" "pkg __no_such_subcommand__"
function __pb_run --argument-names cfg stub cmd out
+17 -11
View File
@@ -3,7 +3,7 @@
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# CI test runner for this fish configuration.
# 1. Syntax-lints every tracked .fish file (fish -n).
# 1. Syntax-lints and indent-checks every tracked .fish file (fish -n, fish_indent --check).
# 2. Discovers tests/test-*.fish and reads the mode each suite declares
# in its own header (`# MODE: isolated` or `# MODE: in-session`).
# 3. Runs each isolated suite as its own --no-config fish process with
@@ -24,25 +24,32 @@ set -l script_dir (realpath (dirname (status filename)))
set -l repo_root (realpath $script_dir/..)
set -l overall_failed 0
# ---- Phase 1: syntax lint ------------------------------------------------
echo "== Syntax lint =="
# ---- Phase 1: syntax & indent lint ---------------------------------------
echo "== Syntax & indent lint =="
set -l lint_files $repo_root/config.fish
for dir in functions conf.d completions integrations
for dir in functions conf.d completions integrations tests
set -a lint_files (find $repo_root/$dir -name '*.fish' | sort)
end
set -l lint_failed 0
set -l syntax_failed 0
set -l indent_failed 0
for f in $lint_files
set -l out (fish -n $f 2>&1)
if test $status -ne 0
echo " FAIL "(string replace $repo_root/ '' $f)
echo " FAIL (syntax) "(string replace $repo_root/ '' $f)
printf '%s\n' $out
set lint_failed (math $lint_failed + 1)
set syntax_failed (math $syntax_failed + 1)
end
if not fish_indent --check $f >/dev/null 2>&1
echo " FAIL (indent) "(string replace $repo_root/ '' $f)
set indent_failed (math $indent_failed + 1)
end
end
set -l lint_total (count $lint_files)
echo (math $lint_total - $lint_failed)"/$lint_total files passed lint"
if test $lint_failed -ne 0
echo (math $lint_total - $syntax_failed)"/$lint_total files passed syntax check"
echo (math $lint_total - $indent_failed)"/$lint_total files passed indent check"
if test $syntax_failed -ne 0 -o $indent_failed -ne 0
set overall_failed 1
end
@@ -160,8 +167,7 @@ if test (count $session_suites) -gt 0
__fish_config_op_autoexec=off \
FISH_CONFIG_TEST_ROOT=$repo_root \
FISH_CONFIG_TEST_COUNTS=$counts \
fish -i -c "source $repo_root/tests/lib.fish; $srcs report" \
2>$err_file
fish -i -c "source $repo_root/tests/lib.fish; $srcs report" 2>$err_file
set -l session_status $status
set -l stderr_out (command cat $err_file)
+197 -109
View File
@@ -70,7 +70,7 @@ function failing_shim --argument-names name match
'#!/bin/sh' \
'for a in "$@"; do' \
" case \"\$a\" in *$match*) exit 1 ;; esac" \
'done' \
done \
"exec $real \"\$@\"" >$d/$name
chmod +x $d/$name
printf '%s\n' $d
@@ -166,7 +166,8 @@ 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
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
@@ -179,7 +180,8 @@ check "sanitizes special chars in local slug" false "$has_bad_chars"
echo ""
echo "== _agents_repo_ensure_symlink =="
set -l w (mktemp -d); set -ga TMPDIRS $w
set -l w (mktemp -d)
set -ga TMPDIRS $w
mkdir -p $w/target $w/live
# Fresh link onto an empty live parent.
@@ -200,7 +202,8 @@ check "refuses missing target" 1 (_agents_repo_ensure_symlink $w/live/f3 $w/nope
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
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
@@ -213,7 +216,8 @@ 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
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
@@ -239,12 +243,14 @@ check "idempotent, silent" "" "$out"
# guarantee, and the old pull-first shape broke it: a failed fetch took
# the local commit down with it. The divergence is still built here so
# that guarantee is tested against the case that used to fail.
set -l origin (mktemp -d); set -ga TMPDIRS $origin
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
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
@@ -297,7 +303,8 @@ set -l h (new_repo)
echo first >$h/a.md
_agents_repo_sync $h "chore: init" >/dev/null
set -l hooks (mktemp -d); set -ga TMPDIRS $hooks
set -l hooks (mktemp -d)
set -ga TMPDIRS $hooks
printf '#!/bin/sh\nexit 1\n' >$hooks/pre-commit
chmod +x $hooks/pre-commit
git -C $h config core.hooksPath $hooks
@@ -312,8 +319,10 @@ check "commit hook rejection commits nothing" $before_hook_count (git -C $h rev-
echo ""
echo "== agents-vault (scaffold + link) =="
set -l vroot (mktemp -d); set -ga TMPDIRS $vroot
set -l croot (mktemp -d); set -ga TMPDIRS $croot
set -l vroot (mktemp -d)
set -ga TMPDIRS $vroot
set -l croot (mktemp -d)
set -ga TMPDIRS $croot
set -g __fish_agent_vault_dir $vroot/agent-vault
set -g __fish_agent_vault_claude_root $croot
@@ -359,8 +368,10 @@ set -e __fish_agent_vault_claude_root
echo ""
echo "== agents-vault (emergent restore) =="
set -l vroot2 (mktemp -d); set -ga TMPDIRS $vroot2
set -l croot2 (mktemp -d); set -ga TMPDIRS $croot2
set -l vroot2 (mktemp -d)
set -ga TMPDIRS $vroot2
set -l croot2 (mktemp -d)
set -ga TMPDIRS $croot2
set -g __fish_agent_vault_dir $vroot2/agent-vault
set -g __fish_agent_vault_claude_root $croot2
@@ -393,8 +404,10 @@ set -e __fish_agent_vault_claude_root
echo ""
echo "== agents-vault (link failure surfaces as exit 1) =="
set -l vroot3 (mktemp -d); set -ga TMPDIRS $vroot3
set -l croot3 (mktemp -d); set -ga TMPDIRS $croot3
set -l vroot3 (mktemp -d)
set -ga TMPDIRS $vroot3
set -l croot3 (mktemp -d)
set -ga TMPDIRS $croot3
set -g __fish_agent_vault_dir $vroot3/agent-vault
set -g __fish_agent_vault_claude_root $croot3
@@ -420,8 +433,10 @@ set -e __fish_agent_vault_claude_root
echo ""
echo "== agents-vault (slug migration) =="
set -l vroot2 (mktemp -d); set -ga TMPDIRS $vroot2
set -l croot2 (mktemp -d); set -ga TMPDIRS $croot2
set -l vroot2 (mktemp -d)
set -ga TMPDIRS $vroot2
set -l croot2 (mktemp -d)
set -ga TMPDIRS $croot2
set -g __fish_agent_vault_dir $vroot2/agent-vault
set -g __fish_agent_vault_claude_root $croot2
@@ -429,7 +444,7 @@ set -g __fish_agent_vault_claude_root $croot2
set -l mp (new_repo)
set -l mmangled (string replace -a '/' '-' -- $mp | string replace -a '.' '-')
mkdir -p $croot2/$mmangled/memory
echo "precious" >$croot2/$mmangled/memory/keep.md
echo precious >$croot2/$mmangled/memory/keep.md
pushd $mp >/dev/null
agents-vault --silent
@@ -495,8 +510,7 @@ set -l enew_slug git.rootiest.dev-rootiest-emptycase
# hand-built shape is covered separately just below.
mkdir -p $vroot2/agent-vault/projects/$enew_slug
printf 'remote: %s\npath: %s\nhost: %s\n' \
https://git.rootiest.dev/rootiest/emptycase.git /gone/elsewhere othermachine \
>$vroot2/agent-vault/projects/$enew_slug/origin
https://git.rootiest.dev/rootiest/emptycase.git /gone/elsewhere othermachine >$vroot2/agent-vault/projects/$enew_slug/origin
pushd $emp >/dev/null
set -l erc (agents-vault --silent 2>/dev/null; echo $status)
@@ -579,7 +593,8 @@ check "removal: old remote entry removed" false (test -d $vroot2/agent-vault/pro
# 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 dirty_root (mktemp -d)
set -ga TMPDIRS $dirty_root
set -l dp "$dirty_root/My Project!"
mkdir -p "$dp"
git -C "$dp" init -q
@@ -590,7 +605,7 @@ 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
echo dirty-precious >$croot2/$dmangled/memory/keep.md
pushd $dp >/dev/null
agents-vault --silent
@@ -621,7 +636,8 @@ check "fallback old entry removed" false (test -d $vroot2/agent-vault/projects/$
# is not: a --silent run that did the right thing and returned 0 still
# printed "rm: cannot remove ...: Is a directory", which is the only
# thing the user sees and reads as a failure.
set -l real_root (mktemp -d); set -ga TMPDIRS $real_root
set -l real_root (mktemp -d)
set -ga TMPDIRS $real_root
set -l rp "$real_root/proj"
mkdir -p "$rp"
git -C "$rp" init -q
@@ -632,7 +648,7 @@ git -C "$rp" config core.hooksPath /dev/null
set -l rmangled (string replace -a '/' '-' -- $rp | string replace -a '.' '-')
mkdir -p $croot2/$rmangled/memory
echo "banked" >$croot2/$rmangled/memory/old.md
echo banked >$croot2/$rmangled/memory/old.md
pushd $rp >/dev/null
agents-vault --silent
@@ -643,10 +659,11 @@ popd >/dev/null
# never seen, then change the slug so the migration runs.
rm -f $croot2/$rmangled/memory
mkdir -p $croot2/$rmangled/memory
echo "written-live" >$croot2/$rmangled/memory/fresh.md
echo written-live >$croot2/$rmangled/memory/fresh.md
git -C $rp remote add origin https://git.rootiest.dev/rootiest/realdir.git
set -l rerr (mktemp); set -ga TMPDIRS $rerr
set -l rerr (mktemp)
set -ga TMPDIRS $rerr
pushd $rp >/dev/null
set -l real_rc (agents-vault --silent 2>$rerr; echo $status)
popd >/dev/null
@@ -673,8 +690,10 @@ echo ""
echo "== agents-vault (a real git clone) =="
# Machine A: populate a vault and let agents-vault commit it.
set -l cl_vroot (mktemp -d); set -ga TMPDIRS $cl_vroot
set -l cl_croot (mktemp -d); set -ga TMPDIRS $cl_croot
set -l cl_vroot (mktemp -d)
set -ga TMPDIRS $cl_vroot
set -l cl_croot (mktemp -d)
set -ga TMPDIRS $cl_croot
set -g __fish_agent_vault_dir $cl_vroot/agent-vault
set -g __fish_agent_vault_claude_root $cl_croot
@@ -698,7 +717,8 @@ agents-vault --silent
popd >/dev/null
# The clone, exactly as the README tells a user to make it.
set -l cl_new (mktemp -d); set -ga TMPDIRS $cl_new
set -l cl_new (mktemp -d)
set -ga TMPDIRS $cl_new
git clone -q $cl_vroot/agent-vault $cl_new/agent-vault
git -C $cl_new/agent-vault config user.email t@t
git -C $cl_new/agent-vault config user.name t
@@ -709,7 +729,8 @@ check "clone: the empty entry has no claude/ subtree" false (test -d $cl_new/age
check "clone: the empty entry is its origin file alone" true (test -f $cl_new/agent-vault/projects/$cl_empty_slug/origin; and echo true; or echo false)
# Machine B, case 1: an ordinary run against the clone restores memory.
set -l cl_croot2 (mktemp -d); set -ga TMPDIRS $cl_croot2
set -l cl_croot2 (mktemp -d)
set -ga TMPDIRS $cl_croot2
set -g __fish_agent_vault_dir $cl_new/agent-vault
set -g __fish_agent_vault_claude_root $cl_croot2
@@ -762,10 +783,14 @@ set -e __fish_agent_vault_claude_root
echo ""
echo "== agents-vault (global state) =="
set -l vroot5 (mktemp -d); set -ga TMPDIRS $vroot5
set -l croot5 (mktemp -d); set -ga TMPDIRS $croot5
set -l chome5 (mktemp -d); set -ga TMPDIRS $chome5
set -l agy5 (mktemp -d); set -ga TMPDIRS $agy5
set -l vroot5 (mktemp -d)
set -ga TMPDIRS $vroot5
set -l croot5 (mktemp -d)
set -ga TMPDIRS $croot5
set -l chome5 (mktemp -d)
set -ga TMPDIRS $chome5
set -l agy5 (mktemp -d)
set -ga TMPDIRS $agy5
set -g __fish_agent_vault_dir $vroot5/agent-vault
set -g __fish_agent_vault_claude_root $croot5
set -g __fish_agent_vault_claude_home $chome5
@@ -802,7 +827,8 @@ printf 'transcript\n' >$agy5/knowledge/session.jsonl
# the store boundary has to hold on its own. $outside5 stands in for that
# home: a directory the store has no business reaching into, planted with
# exactly the shapes that qualify.
set -l outside5 (mktemp -d); set -ga TMPDIRS $outside5
set -l outside5 (mktemp -d)
set -ga TMPDIRS $outside5
mkdir -p $outside5/nested
echo SECRET-OUTSIDE-KNOWLEDGE >$outside5/leaked.json
echo SECRET-OUTSIDE-KNOWLEDGE >$outside5/target.md
@@ -905,10 +931,14 @@ set -e __fish_agent_vault_claude_root
echo ""
echo "== agents-vault (global emergent restore) =="
set -l vroot6 (mktemp -d); set -ga TMPDIRS $vroot6
set -l croot6 (mktemp -d); set -ga TMPDIRS $croot6
set -l chome6 (mktemp -d); set -ga TMPDIRS $chome6
set -l agy6 (mktemp -d); set -ga TMPDIRS $agy6
set -l vroot6 (mktemp -d)
set -ga TMPDIRS $vroot6
set -l croot6 (mktemp -d)
set -ga TMPDIRS $croot6
set -l chome6 (mktemp -d)
set -ga TMPDIRS $chome6
set -l agy6 (mktemp -d)
set -ga TMPDIRS $agy6
set -g __fish_agent_vault_dir $vroot6/agent-vault
set -g __fish_agent_vault_claude_root $croot6
set -g __fish_agent_vault_claude_home $chome6
@@ -927,8 +957,10 @@ check "global restore: vault content readable through the link" restored-global
# A home with neither side populated must not have a memory/ invented for
# it: ~/.claude/memory does not exist by default.
set -l chome7 (mktemp -d); set -ga TMPDIRS $chome7
set -l vroot7 (mktemp -d); set -ga TMPDIRS $vroot7
set -l chome7 (mktemp -d)
set -ga TMPDIRS $chome7
set -l vroot7 (mktemp -d)
set -ga TMPDIRS $vroot7
set -g __fish_agent_vault_dir $vroot7/agent-vault
set -g __fish_agent_vault_claude_home $chome7
set -l gp7 (new_repo https://git.rootiest.dev/rootiest/globals-absent.git)
@@ -951,10 +983,14 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (global link failure is non-fatal) =="
set -l vroot8 (mktemp -d); set -ga TMPDIRS $vroot8
set -l croot8 (mktemp -d); set -ga TMPDIRS $croot8
set -l chome8 (mktemp -d); set -ga TMPDIRS $chome8
set -l agy8 (mktemp -d); set -ga TMPDIRS $agy8
set -l vroot8 (mktemp -d)
set -ga TMPDIRS $vroot8
set -l croot8 (mktemp -d)
set -ga TMPDIRS $croot8
set -l chome8 (mktemp -d)
set -ga TMPDIRS $chome8
set -l agy8 (mktemp -d)
set -ga TMPDIRS $agy8
set -g __fish_agent_vault_dir $vroot8/agent-vault
set -g __fish_agent_vault_claude_root $croot8
set -g __fish_agent_vault_claude_home $chome8
@@ -972,7 +1008,8 @@ set -l fmangled (string replace -a '/' '-' -- $fp | string replace -a '.' '-')
mkdir -p $croot8/$fmangled/memory
echo project-memory >$croot8/$fmangled/memory/p.md
set -l ferr (mktemp); set -ga TMPDIRS $ferr
set -l ferr (mktemp)
set -ga TMPDIRS $ferr
pushd $fp >/dev/null
set -l frc (agents-vault --silent 2>$ferr; echo $status)
popd >/dev/null
@@ -991,7 +1028,8 @@ check "global link failure: per-project memory is committed" true (git -C $vroot
# A failed global link must not be recorded as done: the next run has to
# re-enter the block and warn again, not treat the vault as correct.
pushd $fp >/dev/null
set -l ferr2 (mktemp); set -ga TMPDIRS $ferr2
set -l ferr2 (mktemp)
set -ga TMPDIRS $ferr2
agents-vault --silent 2>$ferr2
popd >/dev/null
check "global link failure: retried on the next run" true (string match -q "*$chome8/memory*" -- (cat $ferr2); and echo true; or echo false)
@@ -1008,17 +1046,22 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (status, remote, adopt) =="
set -l vroot9 (mktemp -d); set -ga TMPDIRS $vroot9
set -l croot9 (mktemp -d); set -ga TMPDIRS $croot9
set -l chome9 (mktemp -d); set -ga TMPDIRS $chome9
set -l agy9 (mktemp -d); set -ga TMPDIRS $agy9
set -l vroot9 (mktemp -d)
set -ga TMPDIRS $vroot9
set -l croot9 (mktemp -d)
set -ga TMPDIRS $croot9
set -l chome9 (mktemp -d)
set -ga TMPDIRS $chome9
set -l agy9 (mktemp -d)
set -ga TMPDIRS $agy9
set -g __fish_agent_vault_dir $vroot9/agent-vault
set -g __fish_agent_vault_claude_root $croot9
set -g __fish_agent_vault_claude_home $chome9
set -g __fish_agent_vault_agy_root $agy9
# A report asked for before the vault exists must say so, not scaffold one.
set -l s0out (mktemp); set -ga TMPDIRS $s0out
set -l s0out (mktemp)
set -ga TMPDIRS $s0out
set -l s0rc (agents-vault --status >$s0out; echo $status)
check "status without a vault exits 0" 0 "$s0rc"
check "status without a vault says so" true (string match -q '*no vault*' -- (cat $s0out); and echo true; or echo false)
@@ -1068,7 +1111,8 @@ check "status reports the remote" true (string match -q '*rootiest/agent-vault.g
# A *failed* remote update must return non-zero. Reporting success after a
# git command that did not run is the same silent-false-success shape that
# a hook-rejected commit produced earlier in this project.
set -l rerr (mktemp); set -ga TMPDIRS $rerr
set -l rerr (mktemp)
set -ga TMPDIRS $rerr
set -l rshim (failing_shim git set-url)
set -l rpath $PATH
set PATH $rshim $PATH
@@ -1106,10 +1150,11 @@ agents-vault --silent
popd >/dev/null
set -l projects_before (command ls -A $vroot9/agent-vault/projects | sort | string join ',')
set -l bad_slugs ../escape has/slash . .. 'UPPER' 'sp ace' ''
set -l bad_slugs ../escape has/slash . .. UPPER 'sp ace' ''
pushd $bp >/dev/null
for bad in $bad_slugs
set -l berr (mktemp); set -ga TMPDIRS $berr
set -l berr (mktemp)
set -ga TMPDIRS $berr
set -l brc (agents-vault --adopt=$bad --silent 2>$berr; echo $status)
check "adopt refuses '$bad'" 1 "$brc"
check "adopt refuses '$bad' out loud" true (string match -q '*invalid*' -- (cat $berr); and echo true; or echo false)
@@ -1150,7 +1195,8 @@ set -l pre_head (git -C $vroot9/agent-vault rev-list --count HEAD)
set -l pre_porcelain (git -C $vroot9/agent-vault status --porcelain | string join ',')
set -l pre_link (path resolve $croot9/$tmang/memory)
set -l terr (mktemp); set -ga TMPDIRS $terr
set -l terr (mktemp)
set -ga TMPDIRS $terr
set -l tshim (failing_shim rm $tmang)
set -l tpath $PATH
set PATH $tshim $PATH
@@ -1203,8 +1249,7 @@ popd >/dev/null
# The target must be *tracked* for this to bite: stashing it takes files
# git knows about out from under the index, which is the whole hazard.
mkdir -p $vroot9/agent-vault/projects/stash-target/claude/memory
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere \
>$vroot9/agent-vault/projects/stash-target/origin
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere >$vroot9/agent-vault/projects/stash-target/origin
pushd $sa >/dev/null
agents-vault --silent
popd >/dev/null
@@ -1245,8 +1290,7 @@ agents-vault --silent
popd >/dev/null
mkdir -p $vroot9/agent-vault/projects/stashfail-target/claude/memory
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere-else \
>$vroot9/agent-vault/projects/stashfail-target/origin
printf 'remote: (none)\npath: %s\nhost: t\n' /nowhere-else >$vroot9/agent-vault/projects/stashfail-target/origin
pushd $sf >/dev/null
agents-vault --silent
popd >/dev/null
@@ -1257,7 +1301,8 @@ set -l sf_porcelain (git -C $vroot9/agent-vault status --porcelain | string join
set -l sf_link (path resolve $croot9/$sfmang/memory)
check "stash adopt rollback: vault clean before the adopt" "" "$sf_porcelain"
set -l sferr (mktemp); set -ga TMPDIRS $sferr
set -l sferr (mktemp)
set -ga TMPDIRS $sferr
set -l sfshim (failing_shim rm $sfmang)
set -l sfpath $PATH
set PATH $sfshim $PATH
@@ -1359,10 +1404,14 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (restore) =="
set -l vroot10 (mktemp -d); set -ga TMPDIRS $vroot10
set -l croot10 (mktemp -d); set -ga TMPDIRS $croot10
set -l chome10 (mktemp -d); set -ga TMPDIRS $chome10
set -l agy10 (mktemp -d); set -ga TMPDIRS $agy10
set -l vroot10 (mktemp -d)
set -ga TMPDIRS $vroot10
set -l croot10 (mktemp -d)
set -ga TMPDIRS $croot10
set -l chome10 (mktemp -d)
set -ga TMPDIRS $chome10
set -l agy10 (mktemp -d)
set -ga TMPDIRS $agy10
set -g __fish_agent_vault_dir $vroot10/agent-vault
set -g __fish_agent_vault_claude_root $croot10
set -g __fish_agent_vault_claude_home $chome10
@@ -1389,9 +1438,9 @@ check "restore: names what it restored" true (string match -q '*restoreme*' -- "
# succeeds and says which entry needs --adopt.
mkdir -p $vroot10/agent-vault/projects/ghost-entry/claude/memory
echo x >$vroot10/agent-vault/projects/ghost-entry/claude/memory/x.md
printf 'remote: (none)\npath: %s\nhost: t\n' $vroot10/gone-forever \
>$vroot10/agent-vault/projects/ghost-entry/origin
set -l r2out (mktemp); set -ga TMPDIRS $r2out
printf 'remote: (none)\npath: %s\nhost: t\n' $vroot10/gone-forever >$vroot10/agent-vault/projects/ghost-entry/origin
set -l r2out (mktemp)
set -ga TMPDIRS $r2out
set -l r2rc (agents-vault --restore >$r2out; echo $status)
check "restore: exits 0 with an unplaceable entry" 0 "$r2rc"
check "restore: reports the unplaceable entry" true (string match -q '*ghost-entry*' -- (cat $r2out); and echo true; or echo false)
@@ -1406,8 +1455,7 @@ set -l dotp (new_repo https://git.rootiest.dev/rootiest/dotted.git)
set -l dotmang (string replace -a '/' '-' -- $dotp | string replace -a '.' '-')
mkdir -p $vroot10/agent-vault/projects/.dot-entry/claude/memory
echo dot-precious >$vroot10/agent-vault/projects/.dot-entry/claude/memory/keep.md
printf 'remote: (none)\npath: %s\nhost: t\n' $dotp \
>$vroot10/agent-vault/projects/.dot-entry/origin
printf 'remote: (none)\npath: %s\nhost: t\n' $dotp >$vroot10/agent-vault/projects/.dot-entry/origin
check "dot-led entry: a glob really does skip it" false (string match -q '*.dot-entry*' -- (echo $vroot10/agent-vault/projects/*); and echo true; or echo false)
set -l dotreport (agents-vault --status)
@@ -1425,7 +1473,8 @@ rm -f $croot10/$rmang/memory
set -l r3shim (failing_shim ln $rmang)
set -l r3path $PATH
set PATH $r3shim $PATH
set -l r3err (mktemp); set -ga TMPDIRS $r3err
set -l r3err (mktemp)
set -ga TMPDIRS $r3err
set -l r3rc (agents-vault --restore >/dev/null 2>$r3err; echo $status)
set PATH $r3path
check "restore: a failed relink returns non-zero" 1 "$r3rc"
@@ -1440,11 +1489,16 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (push) =="
set -l vroot11 (mktemp -d); set -ga TMPDIRS $vroot11
set -l croot11 (mktemp -d); set -ga TMPDIRS $croot11
set -l chome11 (mktemp -d); set -ga TMPDIRS $chome11
set -l agy11 (mktemp -d); set -ga TMPDIRS $agy11
set -l bare (mktemp -d); set -ga TMPDIRS $bare
set -l vroot11 (mktemp -d)
set -ga TMPDIRS $vroot11
set -l croot11 (mktemp -d)
set -ga TMPDIRS $croot11
set -l chome11 (mktemp -d)
set -ga TMPDIRS $chome11
set -l agy11 (mktemp -d)
set -ga TMPDIRS $agy11
set -l bare (mktemp -d)
set -ga TMPDIRS $bare
git init -q --bare $bare
set -g __fish_agent_vault_dir $vroot11/agent-vault
set -g __fish_agent_vault_claude_root $croot11
@@ -1459,7 +1513,8 @@ echo pushed >$croot11/$pmang/memory/p.md
# --push with no remote must fail loudly. The commit still happened, so
# silently returning 0 would read as "backed up off this machine".
set -l perr (mktemp); set -ga TMPDIRS $perr
set -l perr (mktemp)
set -ga TMPDIRS $perr
pushd $pp >/dev/null
set -l prc0 (agents-vault --push --silent 2>$perr; echo $status)
popd >/dev/null
@@ -1498,7 +1553,8 @@ check "autopush pushes when enabled" pushed-auto (git -C $bare show $vbranch:pro
set -l deadremote $vroot11/not-a-repo.git
agents-vault --remote=$deadremote --silent
echo pushed-never >$croot11/$pmang/memory/p4.md
set -l fperr (mktemp); set -ga TMPDIRS $fperr
set -l fperr (mktemp)
set -ga TMPDIRS $fperr
pushd $pp >/dev/null
set -l fprc (agents-vault --push --silent 2>$fperr; echo $status)
popd >/dev/null
@@ -1510,7 +1566,8 @@ check "failing push still committed locally" true (git -C $vroot11/agent-vault l
# saying "Synced" must not come with a zero exit when the push failed.
echo pushed-never-2 >$croot11/$pmang/memory/p5.md
set -g __fish_agent_vault_autopush 1
set -l fp2err (mktemp); set -ga TMPDIRS $fp2err
set -l fp2err (mktemp)
set -ga TMPDIRS $fp2err
pushd $pp >/dev/null
set -l fp2rc (agents-vault --quiet 2>$fp2err >/dev/null; echo $status)
popd >/dev/null
@@ -1561,10 +1618,14 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (the user's ssh command wins) =="
set -l vroot14 (mktemp -d); set -ga TMPDIRS $vroot14
set -l croot14 (mktemp -d); set -ga TMPDIRS $croot14
set -l chome14 (mktemp -d); set -ga TMPDIRS $chome14
set -l agy14 (mktemp -d); set -ga TMPDIRS $agy14
set -l vroot14 (mktemp -d)
set -ga TMPDIRS $vroot14
set -l croot14 (mktemp -d)
set -ga TMPDIRS $croot14
set -l chome14 (mktemp -d)
set -ga TMPDIRS $chome14
set -l agy14 (mktemp -d)
set -ga TMPDIRS $agy14
set -g __fish_agent_vault_dir $vroot14/agent-vault
set -g __fish_agent_vault_claude_root $croot14
set -g __fish_agent_vault_claude_home $chome14
@@ -1574,7 +1635,8 @@ set -g __fish_agent_vault_agy_root $agy14
# plain `ssh` (what the injected default resolves to) and one named
# explicitly by the user. Both refuse the connection, so nothing leaves
# the machine and no test waits on a network.
set -l sbin (mktemp -d); set -ga TMPDIRS $sbin
set -l sbin (mktemp -d)
set -ga TMPDIRS $sbin
set -l pathssh_log $sbin/path-ssh.log
set -l usessh $sbin/user-ssh
set -l usessh_log $sbin/user-ssh.log
@@ -1655,11 +1717,16 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (autopush without timeout) =="
set -l vroot15 (mktemp -d); set -ga TMPDIRS $vroot15
set -l croot15 (mktemp -d); set -ga TMPDIRS $croot15
set -l chome15 (mktemp -d); set -ga TMPDIRS $chome15
set -l agy15 (mktemp -d); set -ga TMPDIRS $agy15
set -l bare15 (mktemp -d); set -ga TMPDIRS $bare15
set -l vroot15 (mktemp -d)
set -ga TMPDIRS $vroot15
set -l croot15 (mktemp -d)
set -ga TMPDIRS $croot15
set -l chome15 (mktemp -d)
set -ga TMPDIRS $chome15
set -l agy15 (mktemp -d)
set -ga TMPDIRS $agy15
set -l bare15 (mktemp -d)
set -ga TMPDIRS $bare15
git init -q --bare $bare15
set -g __fish_agent_vault_dir $vroot15/agent-vault
set -g __fish_agent_vault_claude_root $croot15
@@ -1678,7 +1745,8 @@ popd >/dev/null
# rev-parse would report the literal string HEAD with a fatal on stderr.
set -l vb15 (git -C $vroot15/agent-vault symbolic-ref --short HEAD)
set -l shimroot (mktemp -d); set -ga TMPDIRS $shimroot
set -l shimroot (mktemp -d)
set -ga TMPDIRS $shimroot
set -l nopath
set -l shimn 0
for d in $PATH
@@ -1696,7 +1764,8 @@ end
set -l realpath15 $PATH
echo t2 >$croot15/$tmang15/memory/t2.md
set -l terr (mktemp); set -ga TMPDIRS $terr
set -l terr (mktemp)
set -ga TMPDIRS $terr
set -g __fish_agent_vault_autopush 1
set -g PATH $nopath
check "the shimmed PATH really has no timeout" false (type -q timeout; and echo true; or echo false)
@@ -1737,10 +1806,14 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (a failed vault commit is fatal) =="
set -l vroot13 (mktemp -d); set -ga TMPDIRS $vroot13
set -l croot13 (mktemp -d); set -ga TMPDIRS $croot13
set -l chome13 (mktemp -d); set -ga TMPDIRS $chome13
set -l agy13 (mktemp -d); set -ga TMPDIRS $agy13
set -l vroot13 (mktemp -d)
set -ga TMPDIRS $vroot13
set -l croot13 (mktemp -d)
set -ga TMPDIRS $croot13
set -l chome13 (mktemp -d)
set -ga TMPDIRS $chome13
set -l agy13 (mktemp -d)
set -ga TMPDIRS $agy13
set -g __fish_agent_vault_dir $vroot13/agent-vault
set -g __fish_agent_vault_claude_root $croot13
set -g __fish_agent_vault_claude_home $chome13
@@ -1763,7 +1836,8 @@ printf '#!/bin/sh\nexit 1\n' >$vroot13/agent-vault/.agents-tools/hooks/pre-commi
chmod +x $vroot13/agent-vault/.agents-tools/hooks/pre-commit
echo hook-more >$croot13/$hmang13/memory/keep2.md
set -l hhead13 (git -C $vroot13/agent-vault rev-list --count HEAD)
set -l herr13 (mktemp); set -ga TMPDIRS $herr13
set -l herr13 (mktemp)
set -ga TMPDIRS $herr13
pushd $hp13 >/dev/null
set -l hrc13 (agents-vault --silent 2>$herr13; echo $status)
popd >/dev/null
@@ -1782,16 +1856,21 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
# moves ahead (or goes out of reach) is not a backup; --push is where the
# divergence has to be reckoned with, and where the two ways it can fail
# have to be told apart.
set -l vroot14 (mktemp -d); set -ga TMPDIRS $vroot14
set -l croot14 (mktemp -d); set -ga TMPDIRS $croot14
set -l chome14 (mktemp -d); set -ga TMPDIRS $chome14
set -l agy14 (mktemp -d); set -ga TMPDIRS $agy14
set -l vroot14 (mktemp -d)
set -ga TMPDIRS $vroot14
set -l croot14 (mktemp -d)
set -ga TMPDIRS $croot14
set -l chome14 (mktemp -d)
set -ga TMPDIRS $chome14
set -l agy14 (mktemp -d)
set -ga TMPDIRS $agy14
set -g __fish_agent_vault_dir $vroot14/agent-vault
set -g __fish_agent_vault_claude_root $croot14
set -g __fish_agent_vault_claude_home $chome14
set -g __fish_agent_vault_agy_root $agy14
set -l bare14 (mktemp -d); set -ga TMPDIRS $bare14
set -l bare14 (mktemp -d)
set -ga TMPDIRS $bare14
git init -q --bare $bare14
set -l cp14 (new_repo https://git.rootiest.dev/rootiest/conflict.git)
@@ -1806,7 +1885,8 @@ agents-vault --remote=$bare14 --silent
git -C $vroot14/agent-vault push -q -u origin HEAD:refs/heads/main
# Another machine records a conflicting change to the same line...
set -l cclone14 (mktemp -d); set -ga TMPDIRS $cclone14
set -l cclone14 (mktemp -d)
set -ga TMPDIRS $cclone14
git clone -q $bare14 $cclone14
git -C $cclone14 config user.email t@t
git -C $cclone14 config user.name t
@@ -1819,7 +1899,8 @@ git -C $cclone14 push -q origin HEAD:main
# ... while this one writes conflicting memory of its own on the same
# line. The ordinary run has to commit it: it never fetches, so the
# divergence is invisible to it and irrelevant.
set -l cerr14 (mktemp); set -ga TMPDIRS $cerr14
set -l cerr14 (mktemp)
set -ga TMPDIRS $cerr14
echo ours >$croot14/$cmang14/memory/keep.md
set -l chead14 (git -C $vroot14/agent-vault rev-list --count HEAD)
pushd $cp14 >/dev/null
@@ -1885,10 +1966,14 @@ set -g __fish_agent_vault_agy_root $HERMETIC_HOME/agy
echo ""
echo "== agents-vault (dangling global memory link) =="
set -l vroot12 (mktemp -d); set -ga TMPDIRS $vroot12
set -l croot12 (mktemp -d); set -ga TMPDIRS $croot12
set -l chome12 (mktemp -d); set -ga TMPDIRS $chome12
set -l agy12 (mktemp -d); set -ga TMPDIRS $agy12
set -l vroot12 (mktemp -d)
set -ga TMPDIRS $vroot12
set -l croot12 (mktemp -d)
set -ga TMPDIRS $croot12
set -l chome12 (mktemp -d)
set -ga TMPDIRS $chome12
set -l agy12 (mktemp -d)
set -ga TMPDIRS $agy12
set -g __fish_agent_vault_dir $vroot12/agent-vault
set -g __fish_agent_vault_claude_root $croot12
set -g __fish_agent_vault_claude_home $chome12
@@ -1900,7 +1985,8 @@ check "dangling: -L is the only signal" true (test -L $chome12/memory; and echo
check "dangling: the vault side is empty" false (test -e $vroot12/agent-vault/global/claude/memory; and echo true; or echo false)
set -l dp (new_repo https://git.rootiest.dev/rootiest/dangling.git)
set -l derr (mktemp); set -ga TMPDIRS $derr
set -l derr (mktemp)
set -ga TMPDIRS $derr
pushd $dp >/dev/null
set -l drc (agents-vault --silent 2>$derr; echo $status)
popd >/dev/null
@@ -1934,7 +2020,8 @@ check "agents-init: committed the AGENTS repo" true (test (git -C $ip/AGENTS rev
# Offline. The pull that used to run here blocked the launch until the
# remote timed out and then took the commit down with it, so an agent's
# edits went unrecorded on every launch away from the network.
set -l ibare (mktemp -d); set -ga TMPDIRS $ibare
set -l ibare (mktemp -d)
set -ga TMPDIRS $ibare
git init -q --bare $ibare
git -C $ip/AGENTS remote add origin $ibare
git -C $ip/AGENTS push -q -u origin HEAD 2>/dev/null
@@ -1957,7 +2044,8 @@ printf '#!/bin/sh\nexit 1\n' >$ip/AGENTS/.agents-tools/hooks/pre-commit
chmod +x $ip/AGENTS/.agents-tools/hooks/pre-commit
echo blocked >$ip/AGENTS/devlogs/blocked.md
set -l ibhead (git -C $ip/AGENTS rev-list --count HEAD)
set -l ierr (mktemp); set -ga TMPDIRS $ierr
set -l ierr (mktemp)
set -ga TMPDIRS $ierr
pushd $ip >/dev/null
set -l ibrc (agents-init --silent 2>$ierr; echo $status)
popd >/dev/null
+357
View File
@@ -0,0 +1,357 @@
#!/usr/bin/env fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# MODE: isolated
source (realpath (dirname (status filename)))/lib.fish
set -p fish_function_path $repo_root/functions
set -gx TERM xterm-256color
set -g __fish_config_dir $repo_root 2>/dev/null; or true
# =============================================================================
# 1. sponge_filter_secrets: Credential Leakage Prevention
# =============================================================================
section sponge_filter_secrets
# Export test variables matching sensitive heuristics
set -gx MY_AWS_SECRET_ACCESS_KEY wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY
set -gx GITHUB_TOKEN ghp_0123456789abcdefghijklmnopqrstuv
set -gx KOPIA_PASSWORD MyLongPassword123
# Secret appears in command -> returns 0 (filtered from history)
sponge_filter_secrets "aws s3 cp file.txt s3://bucket/ --key wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
check "aws secret in command filtered" 0 $status
sponge_filter_secrets "curl -H 'Authorization: token ghp_0123456789abcdefghijklmnopqrstuv' https://api.github.com"
check "github token in command filtered" 0 $status
sponge_filter_secrets "kopia repository connect --password MyLongPassword123"
check "kopia password in command filtered" 0 $status
# Clean command with no secret -> returns 1 (kept in history)
sponge_filter_secrets "git status -s"
check "clean command retained" 1 $status
sponge_filter_secrets "echo 'Hello World'"
check "ordinary echo retained" 1 $status
# Short secret (<= 8 chars) -> ignored, returns 1 (kept)
set -gx SHORT_API_KEY 12345678
set -gx SHORT_SECRET short
sponge_filter_secrets "curl -H 'X-Key: 12345678' https://example.com"
check "8-char secret ignored" 1 $status
sponge_filter_secrets "echo short"
check "short secret ignored" 1 $status
# Path-like secret (starts with / or ~) -> ignored, returns 1 (kept)
set -gx SECRET_FILE_PATH "/home/rootiest/.token"
set -gx HOME_SECRET_PATH "~/secrets/token.txt"
sponge_filter_secrets "cat /home/rootiest/.token"
check "path-like secret starting with / ignored" 1 $status
sponge_filter_secrets "source ~/secrets/token.txt"
check "path-like secret starting with ~ ignored" 1 $status
# Empty or unset variable -> handled without error, returns 1
set -gx DUMMY_API_KEY ""
sponge_filter_secrets "echo normal command"
check "empty sensitive var does not cause crash or false filter" 1 $status
# Clean up exported test variables
set -e MY_AWS_SECRET_ACCESS_KEY
set -e GITHUB_TOKEN
set -e KOPIA_PASSWORD
set -e SHORT_API_KEY
set -e SHORT_SECRET
set -e SECRET_FILE_PATH
set -e HOME_SECRET_PATH
set -e DUMMY_API_KEY
# =============================================================================
# 2. _fish_mkdir_p: Directory Creation with Feedback
# =============================================================================
section _fish_mkdir_p
set -l mkdir_sandbox (mktemp -d)
# Empty argument returns status 1
_fish_mkdir_p
check "empty argument returns 1" 1 $status
# Existing directory returns status 0
_fish_mkdir_p $mkdir_sandbox
check "existing directory returns 0" 0 $status
# --path: creates directory and prints single line
set -l out_path (_fish_mkdir_p --path $mkdir_sandbox/dirA/dirB)
check "--path returns 0" 0 $status
check "--path creates directory" true (test -d $mkdir_sandbox/dirA/dirB; and echo true; or echo false)
check "--path outputs 'Created directory:'" true (string match -q "*Created directory:*" -- "$out_path"; and echo true; or echo false)
# --silent: creates directory and suppresses stdout
set -l out_silent (_fish_mkdir_p --silent $mkdir_sandbox/dirC/dirD)
check "--silent returns 0" 0 $status
check "--silent creates directory" true (test -d $mkdir_sandbox/dirC/dirD; and echo true; or echo false)
check "--silent produces empty stdout" "" "$out_silent"
# -s short flag
set -l out_silent_short (_fish_mkdir_p -s $mkdir_sandbox/dirC/dirD/sub)
check "-s returns 0" 0 $status
check "-s creates directory" true (test -d $mkdir_sandbox/dirC/dirD/sub; and echo true; or echo false)
check "-s produces empty stdout" "" "$out_silent_short"
# --tree: creates directory and displays tree view with branch glyph
set -l out_tree (_fish_mkdir_p --tree $mkdir_sandbox/dirE/dirF)
check "--tree returns 0" 0 $status
check "--tree creates directory" true (test -d $mkdir_sandbox/dirE/dirF; and echo true; or echo false)
check "--tree outputs 'Created directories:'" true (string match -q "*Created directories:*" -- "$out_tree"; and echo true; or echo false)
check "--tree outputs branch glyph └──" true (string match -q "*└──*" -- "$out_tree"; and echo true; or echo false)
# Default mode without mode flag uses path mode
set -l out_default (_fish_mkdir_p $mkdir_sandbox/dirG/dirH)
check "default mode returns 0" 0 $status
check "default mode creates directory" true (test -d $mkdir_sandbox/dirG/dirH; and echo true; or echo false)
check "default mode outputs 'Created directory:'" true (string match -q "*Created directory:*" -- "$out_default"; and echo true; or echo false)
# Cleanup
rm -rf $mkdir_sandbox
# =============================================================================
# 3. _scrollback_prune_junk: History Log Pruning
# =============================================================================
section _scrollback_prune_junk
set -l prune_sandbox (mktemp -d)
# Create test files in prune sandbox
# Empty files (must be deleted)
touch $prune_sandbox/empty.log
touch $prune_sandbox/empty.txt
# 1-line noise files (must be deleted)
echo "lone command prompt" >$prune_sandbox/single_line.log
echo "[exited]" >$prune_sandbox/single_line.txt
printf "\n\n \n single line with surrounding whitespace \n\n" >$prune_sandbox/whitespace_single.log
# Kitty tab-rename UI noise captures (must be deleted)
printf "%s\n" "Prompt start" "Enter the new title for this tab below" Done >$prune_sandbox/scrollback_kitty.log
printf "%s\n" Header "Enter the new title for this tab below" Tail >$prune_sandbox/scrollback_rename.txt
# Valid multi-line log files (>1 meaningful lines) (MUST BE PRESERVED)
printf "%s\n" "Session started at 12:00" "Command: ls -la" "Output: total 0" >$prune_sandbox/valid_session.log
printf "%s\n" "Build step 1 passed" "Build step 2 passed" >$prune_sandbox/valid_session.txt
printf "%s\n" "scrollback line 1" "scrollback line 2" "scrollback line 3" >$prune_sandbox/scrollback_valid.log
# Run prune
_scrollback_prune_junk $prune_sandbox
# Assertions: empty files deleted
check "empty .log deleted" false (test -f $prune_sandbox/empty.log; and echo true; or echo false)
check "empty .txt deleted" false (test -f $prune_sandbox/empty.txt; and echo true; or echo false)
# Assertions: 1-line noise files deleted
check "single-line .log deleted" false (test -f $prune_sandbox/single_line.log; and echo true; or echo false)
check "single-line .txt deleted" false (test -f $prune_sandbox/single_line.txt; and echo true; or echo false)
check "whitespace single-line .log deleted" false (test -f $prune_sandbox/whitespace_single.log; and echo true; or echo false)
# Assertions: kitty tab rename prompt files deleted
check "kitty tab rename scrollback_*.log deleted" false (test -f $prune_sandbox/scrollback_kitty.log; and echo true; or echo false)
check "kitty tab rename scrollback_*.txt deleted" false (test -f $prune_sandbox/scrollback_rename.txt; and echo true; or echo false)
# Assertions: valid multi-line files preserved
check "valid multi-line .log preserved" true (test -f $prune_sandbox/valid_session.log; and echo true; or echo false)
check "valid multi-line .txt preserved" true (test -f $prune_sandbox/valid_session.txt; and echo true; or echo false)
check "valid multi-line scrollback_valid.log preserved" true (test -f $prune_sandbox/scrollback_valid.log; and echo true; or echo false)
# Assertions: non-existent directory handled safely
_scrollback_prune_junk $prune_sandbox/nonexistent
check "nonexistent directory returns 0" 0 $status
# Cleanup
rm -rf $prune_sandbox
# =============================================================================
# 4. sudo-toggle: Sudo NOPASSWD Security Toggle
# =============================================================================
section sudo-toggle
set -l sudo_sandbox (mktemp -d)
set -g _mock_sudoers_file "$sudo_sandbox/nofail-toggle"
# Mock sudo function to intercept stat, truncate, and tee for /etc/sudoers.d/nofail-toggle
function sudo
set -l cmd_args $argv
while test (count $cmd_args) -gt 0; and string match -qr '^-[a-zA-Z]' -- $cmd_args[1]
set -e cmd_args[1]
end
switch $cmd_args[1]
case stat
if test -f "$_mock_sudoers_file"
command stat -c %s "$_mock_sudoers_file"
else
return 1
end
case truncate
if test -f "$_mock_sudoers_file"
command truncate -s 0 "$_mock_sudoers_file"
end
case tee
command tee "$_mock_sudoers_file"
case '*'
return 1
end
end
# Case 1: When file is missing -> writes rule, outputs DISABLED (Bypass active)
rm -f "$_mock_sudoers_file"
set -l out_missing (sudo-toggle)
check "missing file: returns 0" 0 $status
check "missing file: displays DISABLED" true (string match -q "*🔓 Sudo security: DISABLED*" -- "$out_missing"; and echo true; or echo false)
check "missing file: creates sudoers file" true (test -f "$_mock_sudoers_file"; and echo true; or echo false)
check "missing file: writes content to file" true (test -s "$_mock_sudoers_file"; and echo true; or echo false)
# Case 2: When file has size > 0 (bypass active) -> truncates to 0, outputs ENABLED
set -l out_active (sudo-toggle)
check "active bypass: returns 0" 0 $status
check "active bypass: displays ENABLED" true (string match -q "*🔒 Sudo security: ENABLED*" -- "$out_active"; and echo true; or echo false)
check "active bypass: truncates file to 0 bytes" false (test -s "$_mock_sudoers_file"; and echo true; or echo false)
# Case 3: When file exists but has size 0 -> writes rule, outputs DISABLED
set -l out_empty (sudo-toggle)
check "empty file: returns 0" 0 $status
check "empty file: displays DISABLED" true (string match -q "*🔓 Sudo security: DISABLED*" -- "$out_empty"; and echo true; or echo false)
check "empty file: writes content to file" true (test -s "$_mock_sudoers_file"; and echo true; or echo false)
# Case 4: --help flag displays header documentation
set -l out_help (sudo-toggle --help)
check "sudo-toggle --help returns 0" 0 $status
check "sudo-toggle --help contains USAGE" true (string match -q "*USAGE*" -- "$out_help"; and echo true; or echo false)
# Cleanup
functions -e sudo
set -e _mock_sudoers_file
rm -rf $sudo_sandbox
# =============================================================================
# 5. spark: Sparkline Bar Chart Generation
# =============================================================================
section spark
# Number array 1 2 3 4 5 produces sparkline characters
set -l out_seq (spark 1 2 3 4 5)
check "spark 1 2 3 4 5 generates sparkline" "▁▃▄▆█" "$out_seq"
# Numbers via stdin
set -l out_stdin (printf "%s\n" 1 2 3 4 5 | spark)
check "spark via stdin generates sparkline" "▁▃▄▆█" "$out_stdin"
# Clamping with --min and --max
set -l out_clamped (spark --min=0 --max=10 0 5 10)
check "spark clamped with --min and --max" "▁▄█" "$out_clamped"
# Version flag
set -l out_ver (spark --version)
check "spark --version contains version 1.1.0" true (string match -q "*spark, version 1.1.0*" -- "$out_ver"; and echo true; or echo false)
set -l out_ver_s (spark -v)
check "spark -v contains version 1.1.0" true (string match -q "*spark, version 1.1.0*" -- "$out_ver_s"; and echo true; or echo false)
# Help flag
set -l out_spark_help (spark --help)
check "spark --help contains Usage:" true (string match -q "*Usage:*" -- "$out_spark_help"; and echo true; or echo false)
set -l out_spark_help_s (spark -h)
check "spark -h contains Usage:" true (string match -q "*Usage:*" -- "$out_spark_help_s"; and echo true; or echo false)
# =============================================================================
# 6. pkg: System Package Manager Abstraction
# =============================================================================
section pkg
# Test missing package manager detection
function _fish_deps_detect_pm
return 1
end
set -l err_no_pm (pkg somepkg 2>&1 >/dev/null)
check "missing package manager returns 1" 1 $status
check "missing package manager reports error on stderr" true (string match -q "*error: no supported package manager found*" -- "$err_no_pm"; and echo true; or echo false)
# Test pacman integration with mock detection and mock commands
function _fish_deps_detect_pm
echo pacman
end
set -g _pacman_log
function pacman
set -ga _pacman_log (string join -- " " $argv)
if test "$argv[1]" = -Qi
if test "$argv[2]" = installed-pkg
return 0
else
return 1
end
end
return 0
end
function sudo
$argv
end
# Auto mode with installed package -> invokes pacman -Qi and then removes (-Rns)
set -g _pacman_log
set -l out_installed (pkg installed-pkg)
check "pkg installed-pkg returns 0" 0 $status
check "pkg installed-pkg outputs Removing" true (string match -q "*Removing*installed-pkg*" -- "$out_installed"; and echo true; or echo false)
check "pkg installed-pkg runs pacman -Qi query" true (string match -q "*-Qi installed-pkg*" -- "$_pacman_log"; and echo true; or echo false)
check "pkg installed-pkg runs pacman -Rns removal" true (string match -q "*-Rns installed-pkg*" -- "$_pacman_log"; and echo true; or echo false)
# Auto mode with uninstalled package -> invokes pacman -Qi and then installs (-S)
set -g _pacman_log
set -l out_uninstalled (pkg uninstalled-pkg)
check "pkg uninstalled-pkg returns 0" 0 $status
check "pkg uninstalled-pkg outputs Installing" true (string match -q "*Installing*uninstalled-pkg*" -- "$out_uninstalled"; and echo true; or echo false)
check "pkg uninstalled-pkg runs pacman -Qi query" true (string match -q "*-Qi uninstalled-pkg*" -- "$_pacman_log"; and echo true; or echo false)
check "pkg uninstalled-pkg runs pacman -S installation" true (string match -q "*-S uninstalled-pkg*" -- "$_pacman_log"; and echo true; or echo false)
# Explicit install mode (-i) -> directly installs
set -g _pacman_log
set -l out_force_install (pkg -i new-pkg)
check "pkg -i returns 0" 0 $status
check "pkg -i outputs Installing" true (string match -q "*Installing*new-pkg*" -- "$out_force_install"; and echo true; or echo false)
check "pkg -i runs pacman -S" true (string match -q "*-S new-pkg*" -- "$_pacman_log"; and echo true; or echo false)
# Explicit uninstall mode (-u) -> directly removes
set -g _pacman_log
set -l out_force_uninstall (pkg -u old-pkg)
check "pkg -u returns 0" 0 $status
check "pkg -u outputs Removing" true (string match -q "*Removing*old-pkg*" -- "$out_force_uninstall"; and echo true; or echo false)
check "pkg -u runs pacman -Rns" true (string match -q "*-Rns old-pkg*" -- "$_pacman_log"; and echo true; or echo false)
# Help flag
set -l out_pkg_help (pkg --help)
check "pkg --help returns 0" 0 $status
check "pkg --help outputs Usage" true (string match -q "*Usage:*pkg*" -- "$out_pkg_help"; and echo true; or echo false)
# No arguments -> prints usage and returns 0
set -l out_pkg_empty (pkg)
check "pkg with no arguments returns 0" 0 $status
check "pkg with no arguments outputs Usage" true (string match -q "*Usage:*pkg*" -- "$out_pkg_empty"; and echo true; or echo false)
# Unknown flag -> returns 1 and prints error to stderr
set -l err_pkg_flag (pkg --unrecognized-option 2>&1 >/dev/null)
check "pkg unknown flag returns 1" 1 $status
check "pkg unknown flag outputs error on stderr" true (string match -q "*error:*unknown flag*--unrecognized-option*" -- "$err_pkg_flag"; and echo true; or echo false)
# Cleanup
functions -e _fish_deps_detect_pm pacman sudo
set -e _pacman_log
# =============================================================================
# Report
# =============================================================================
report
+4 -4
View File
@@ -245,10 +245,10 @@ section "op_enabled: always/* and AND, via a synthetic registry"
# this process reads them, so overriding them is free.
set -g __fish_config_op_registry_keys "syn_on:" "syn_off:" "syn_and:" "syn_bare:" "syn_multi:"
set -g __fish_config_op_registry_values \
"always/on" \
"always/off" \
always/on \
always/off \
"aliases/filesystem integrations/notifications" \
"aliases" \
aliases \
"always/off always/on"
set -e __fish_config_op_aliases __fish_config_op_integrations __fish_config_opinionated
@@ -312,7 +312,7 @@ section "op_enabled: C5 through the guard"
# The path production code actually takes, as opposed to calling the cascade
# directly. Same rule: unset means off and the master cannot enable it.
set -g __fish_config_op_registry_keys "syn_log:"
set -g __fish_config_op_registry_values "logging/terminal-capture"
set -g __fish_config_op_registry_values logging/terminal-capture
set -e __fish_config_op_logging __fish_config_op_logging_terminal_capture
__fish_config_op_enabled syn_log
+3 -3
View File
@@ -45,7 +45,7 @@ function test_help_renderer
'function fixturefn' \
' __fish_help_header (status current-function) $argv; and return 0' \
' echo RAN-BODY' \
'end' >$tmp/fixturefn.fish
end >$tmp/fixturefn.fish
set -l out (_help_probe $tmp 'fixturefn --help')
set -l code $status
@@ -106,14 +106,14 @@ function test_help_renderer_degrades_safely
'function headerless' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/headerless.fish
end >$tmp/headerless.fish
# A comment run carrying no `# LABEL` line at all.
printf '%s\n' \
'# just an ordinary comment, no labels here' \
'function malformed' \
' __fish_help_header (status current-function) $argv; and return 0' \
" touch $tmp/BODY-RAN" \
'end' >$tmp/malformed.fish
end >$tmp/malformed.fish
set -l failed 0
for fn in headerless malformed
+683
View File
@@ -0,0 +1,683 @@
#!/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)
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
+31
View File
@@ -0,0 +1,31 @@
#!/usr/bin/env fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# Isolated test suite for Python helper scripts network isolation and edge case mocking.
# Exercises scripts/sync-labels.py under offline conditions with full mock coverage.
#
# Runs isolated (no # MODE: marker, defaulting to isolated).
source (realpath (dirname (status filename)))/lib.fish
section "python network isolation: sync-labels.py"
set -l py_script $repo_root/tests/test_sync_labels.py
check "tests/test_sync_labels.py exists" true (test -f $py_script; and echo true; or echo false)
set -l py_lines (python3 $py_script -v 2>&1)
set -l py_status $status
for line in $py_lines
if string match -qr '^\s*(test_\w+)\s+\([^)]+\)\s+\.\.\.\s+(\w+)' -- $line
set -l match (string match -r '^\s*(test_\w+)\s+\([^)]+\)\s+\.\.\.\s+(\w+)' -- $line)
set -l tname $match[2]
set -l tres $match[3]
check "$tname" ok (string lower $tres)
end
end
check "test_sync_labels.py suite exited 0" 0 $py_status
report
+397
View File
@@ -0,0 +1,397 @@
#!/usr/bin/env fish
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
#
# MODE: isolated
source (realpath (dirname (status filename)))/lib.fish
set -p fish_function_path $repo_root/functions
set -gx TERM xterm-256color
set -g __fish_config_dir $repo_root 2>/dev/null; or true
# Ensure data/words is reachable via __fish_config_dir in isolated runs
if not test -d "$__fish_config_dir/data/words"
mkdir -p "$__fish_config_dir/data"
ln -sf "$repo_root/data/words" "$__fish_config_dir/data/words"
end
# -----------------------------------------------------------------------------
# rand_string: Components
# -----------------------------------------------------------------------------
section "rand_string: components"
check "literal component" FOO (rand_string literal=FOO)
check "string component" BAR (rand_string string=BAR)
set -l out_digits4 (rand_string digits=4)
check "digits=4 returns 4 digits" true (string match -qr '^\d{4}$' -- "$out_digits4"; and echo true; or echo false)
set -l out_digits1 (rand_string digits=1)
check "digits=1 returns 1 digit" true (string match -qr '^\d$' -- "$out_digits1"; and echo true; or echo false)
set -l out_digits_inv (rand_string digits=abc)
check "digits=non-numeric defaults to 1 digit" true (string match -qr '^\d$' -- "$out_digits_inv"; and echo true; or echo false)
set -l out_color (rand_string color)
check "category color generates non-empty word" true (test -n "$out_color"; and echo true; or echo false)
set -l out_animal (rand_string animal)
check "category animal generates non-empty word" true (test -n "$out_animal"; and echo true; or echo false)
set -l out_noun (rand_string noun)
check "category noun generates non-empty word" true (test -n "$out_noun"; and echo true; or echo false)
set -l out_verb (rand_string verb)
check "category verb generates non-empty word" true (test -n "$out_verb"; and echo true; or echo false)
set -l out_adj (rand_string adjective)
check "category adjective generates non-empty word" true (test -n "$out_adj"; and echo true; or echo false)
set -l out_name (rand_string name)
check "category name generates non-empty word" true (test -n "$out_name"; and echo true; or echo false)
# -----------------------------------------------------------------------------
# rand_string: Separators
# -----------------------------------------------------------------------------
section "rand_string: separators"
check "default separator is dash" A-B (rand_string literal=A literal=B)
check "--separator=underscore" A_B (rand_string --separator=underscore literal=A literal=B)
check "--separator=dot" "A.B" (rand_string --separator=dot literal=A literal=B)
check "--separator=none" AB (rand_string --separator=none literal=A literal=B)
check "--separator=empty" AB (rand_string --separator=empty literal=A literal=B)
check "-s dot" "A.B" (rand_string -s dot literal=A literal=B)
check "-s underscore" A_B (rand_string -s underscore literal=A literal=B)
check "-s none" AB (rand_string -s none literal=A literal=B)
check "custom literal separator ::" "A::B" (rand_string --separator=:: literal=A literal=B)
set -l out_multi (rand_string literal=test --separator=underscore literal=part digits=2)
check "multi-component with separators" true (string match -qr '^test_part_\d{2}$' -- "$out_multi"; and echo true; or echo false)
# -----------------------------------------------------------------------------
# rand_string: Casing
# -----------------------------------------------------------------------------
section "rand_string: casing"
check "--case=upper produces uppercase" true (string match -qr '^[A-Z]+$' -- (rand_string --case=upper color); and echo true; or echo false)
check "--case=lower produces lowercase" true (string match -qr '^[a-z]+$' -- (rand_string --case=lower color); and echo true; or echo false)
check "--case=title produces titlecase" true (string match -qr '^[A-Z][a-z]+$' -- (rand_string --case=title color); and echo true; or echo false)
check "-c upper flag" true (string match -qr '^[A-Z]+$' -- (rand_string -c upper color); and echo true; or echo false)
check "-c lower flag" true (string match -qr '^[a-z]+$' -- (rand_string -c lower color); and echo true; or echo false)
check "-c title flag" true (string match -qr '^[A-Z][a-z]+$' -- (rand_string -c title color); and echo true; or echo false)
check "literal preserves casing with --case=upper" foo (rand_string --case=upper literal=foo)
check "literal preserves casing with --case=lower" FOO (rand_string --case=lower literal=FOO)
check "literal preserves casing with --case=title" foo (rand_string --case=title literal=foo)
# -----------------------------------------------------------------------------
# rand_string: Errors and Help
# -----------------------------------------------------------------------------
section "rand_string: errors and help"
rand_string nonexistent_cat_xyz >/dev/null 2>&1
check "missing category returns 1" 1 $status
set -l err_msg (rand_string nonexistent_cat_xyz 2>&1 >/dev/null)
check "missing category reports error on stderr" true (string match -qr 'unknown category' -- "$err_msg"; and echo true; or echo false)
rand_string -h >/dev/null 2>&1
check "-h returns 0" 0 $status
set -l help_h (rand_string -h)
check "-h displays usage" true (string match -qr 'Usage:' -- "$help_h"; and echo true; or echo false)
rand_string --help >/dev/null 2>&1
check "--help returns 0" 0 $status
set -l help_long (rand_string --help)
check "--help displays usage" true (string match -qr 'Usage:' -- "$help_long"; and echo true; or echo false)
# -----------------------------------------------------------------------------
# Mock commandline infrastructure
# -----------------------------------------------------------------------------
set -g mock_cmd_buffer ""
set -g mock_cmd_cursor 0
set -g mock_cmd_token ""
set -g mock_cmd_inserted ""
set -g mock_cmd_in_search 0
function commandline
if test (count $argv) -eq 0
echo -n "$mock_cmd_buffer"
return 0
end
switch $argv[1]
case -C
if test (count $argv) -ge 2
set -g mock_cmd_cursor $argv[2]
else
echo -n "$mock_cmd_cursor"
end
return 0
case -r
if test (count $argv) -ge 2
set -g mock_cmd_buffer "$argv[2]"
else
set -g mock_cmd_buffer ""
end
return 0
case --
if test (count $argv) -ge 2
set -g mock_cmd_buffer "$argv[2]"
else
set -g mock_cmd_buffer ""
end
return 0
case -i --insert
if test (count $argv) -ge 2
set -g mock_cmd_inserted "$argv[2]"
set -g mock_cmd_buffer "$mock_cmd_buffer$argv[2]"
end
return 0
case --search-field
if contains -- --insert $argv
set -l idx (contains -i -- --insert $argv)
set -l val $argv[(math $idx + 1)]
set -g mock_cmd_inserted "$val"
set -g mock_cmd_buffer "$mock_cmd_buffer$val"
return 0
else
test "$mock_cmd_in_search" -eq 1
return $status
end
case -t --current-token
if test (count $argv) -ge 2
set -l old_tok "$mock_cmd_token"
set -g mock_cmd_token "$argv[2]"
if test -n "$old_tok"
set -g mock_cmd_buffer (string replace -- "$old_tok" "$argv[2]" "$mock_cmd_buffer")
else
set -g mock_cmd_buffer "$mock_cmd_buffer$argv[2]"
end
else
echo -n "$mock_cmd_token"
end
return 0
end
end
set -g _test_hist_files
function set_test_history --argument-names cmd
set -l session "test_hist_"(random)
set -l hist_dir ""
if test -n "$XDG_DATA_HOME"
set hist_dir "$XDG_DATA_HOME/fish"
else
set hist_dir "$HOME/.local/share/fish"
end
mkdir -p "$hist_dir"
set -l hist_file "$hist_dir/"$session"_history"
echo "- cmd: $cmd" >"$hist_file"
echo " when: "(date +%s) >>"$hist_file"
set -g fish_history "$session"
set -ga _test_hist_files "$hist_file"
end
function cleanup_test_history
if set -q _test_hist_files
for f in $_test_hist_files
if test -f "$f"
rm -f "$f"
end
end
set -e _test_hist_files
end
end
# -----------------------------------------------------------------------------
# _replace_command_token
# -----------------------------------------------------------------------------
section _replace_command_token
set mock_cmd_buffer "git status"
set mock_cmd_cursor -1
_replace_command_token
check "git status: buffer replaces command token" " status" "$mock_cmd_buffer"
check "git status: cursor placed at 0" 0 "$mock_cmd_cursor"
set mock_cmd_buffer "sudo rm -rf /tmp"
set mock_cmd_cursor -1
_replace_command_token
check "sudo rm -rf /tmp: buffer preserves sudo and replaces command" "sudo -rf /tmp" "$mock_cmd_buffer"
check "sudo rm -rf /tmp: cursor placed at 5" 5 "$mock_cmd_cursor"
set mock_cmd_buffer "sudo systemctl status nginx"
set mock_cmd_cursor -1
_replace_command_token
check "sudo systemctl status: buffer preserves sudo" "sudo status nginx" "$mock_cmd_buffer"
check "sudo systemctl status: cursor placed at 5" 5 "$mock_cmd_cursor"
set mock_cmd_buffer "python -m unittest"
set mock_cmd_cursor -1
_replace_command_token
check "python -m unittest: buffer replaces command token" " -m unittest" "$mock_cmd_buffer"
check "python -m unittest: cursor placed at 0" 0 "$mock_cmd_cursor"
set mock_cmd_buffer ls
set mock_cmd_cursor -1
_replace_command_token
check "single token ls: buffer becomes single space" " " "$mock_cmd_buffer"
check "single token ls: cursor placed at 0" 0 "$mock_cmd_cursor"
set mock_cmd_buffer "sudo reboot"
set mock_cmd_cursor -1
_replace_command_token
check "sudo reboot: buffer becomes sudo with two spaces" "sudo " "$mock_cmd_buffer"
check "sudo reboot: cursor placed at 5" 5 "$mock_cmd_cursor"
# -----------------------------------------------------------------------------
# __substitute_typo
# -----------------------------------------------------------------------------
section __substitute_typo
set_test_history 'git commit -m "feat"'
set mock_cmd_buffer "^feat^fix"
set mock_cmd_inserted ""
__substitute_typo
check "substitute ^feat^fix in git commit" 'git commit -m "fix"' "$mock_cmd_buffer"
set_test_history "docker run -d nginx"
set mock_cmd_buffer "^nginx^redis"
set mock_cmd_inserted ""
__substitute_typo
check "substitute ^nginx^redis in docker run" "docker run -d redis" "$mock_cmd_buffer"
set_test_history "echo foo foo"
set mock_cmd_buffer "^foo^bar"
set mock_cmd_inserted ""
__substitute_typo
check "substitute replaces all occurrences" "echo bar bar" "$mock_cmd_buffer"
set_test_history "git checkout main feat"
set mock_cmd_buffer "^feat^"
set mock_cmd_inserted ""
__substitute_typo
check "substitute ^feat^ with empty string deletes token" "git checkout main " "$mock_cmd_buffer"
set_test_history "git status"
set mock_cmd_buffer "^"
set mock_cmd_inserted ""
__substitute_typo
check "lone caret inserts literal caret" "^" "$mock_cmd_inserted"
check "lone caret buffer retains caret" "^^" "$mock_cmd_buffer"
set_test_history "git status"
set mock_cmd_buffer "git log"
set mock_cmd_inserted ""
__substitute_typo
check "non-matching buffer inserts literal caret" "^" "$mock_cmd_inserted"
cleanup_test_history
# -----------------------------------------------------------------------------
# _puffer_fish_expand_dot
# -----------------------------------------------------------------------------
section _puffer_fish_expand_dot
set mock_cmd_token ".."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token .. inserts /.." "/.." "$mock_cmd_inserted"
set mock_cmd_token "../.."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token ../.. inserts /.." "/.." "$mock_cmd_inserted"
set mock_cmd_token "../../.."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token ../../.. inserts /.." "/.." "$mock_cmd_inserted"
set mock_cmd_token "."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token . inserts ." "." "$mock_cmd_inserted"
set mock_cmd_token "..."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token ... inserts ." "." "$mock_cmd_inserted"
set mock_cmd_token foo
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 0
_puffer_fish_expand_dot
check "token foo inserts ." "." "$mock_cmd_inserted"
set mock_cmd_token ".."
set mock_cmd_inserted ""
set mock_cmd_buffer ""
set mock_cmd_in_search 1
_puffer_fish_expand_dot
check "search field mode inserts . even if token is .." "." "$mock_cmd_inserted"
# -----------------------------------------------------------------------------
# _puffer_fish_expand_bang
# -----------------------------------------------------------------------------
section _puffer_fish_expand_bang
set_test_history "cargo test --release"
set mock_cmd_token "!"
set mock_cmd_buffer "!"
set mock_cmd_inserted ""
set mock_cmd_in_search 0
_puffer_fish_expand_bang
check "token ! expands to history[1]" "cargo test --release" "$mock_cmd_token"
check "token ! updates buffer" "cargo test --release" "$mock_cmd_buffer"
set mock_cmd_token "!"
set mock_cmd_buffer "sudo !"
set mock_cmd_inserted ""
set mock_cmd_in_search 0
_puffer_fish_expand_bang
check "sudo ! expands token to history[1]" "sudo cargo test --release" "$mock_cmd_buffer"
set mock_cmd_token git
set mock_cmd_buffer git
set mock_cmd_inserted ""
set mock_cmd_in_search 0
_puffer_fish_expand_bang
check "non-bang token inserts literal !" "!" "$mock_cmd_inserted"
set mock_cmd_token ""
set mock_cmd_buffer ""
set mock_cmd_inserted ""
set mock_cmd_in_search 0
_puffer_fish_expand_bang
check "empty token inserts literal !" "!" "$mock_cmd_inserted"
set mock_cmd_token "!"
set mock_cmd_buffer ""
set mock_cmd_inserted ""
set mock_cmd_in_search 1
_puffer_fish_expand_bang
check "search field mode inserts literal !" "!" "$mock_cmd_inserted"
cleanup_test_history
# Unshadow commandline builtin
functions -e commandline
report
+429
View File
@@ -0,0 +1,429 @@
#!/usr/bin/env python3
# Copyright (C) 2026 Rootiest
# SPDX-License-Identifier: AGPL-3.0-or-later
"""Comprehensive unit tests for scripts/sync-labels.py with network isolation."""
import io
import json
import os
import socket
import sys
import tempfile
import unittest
import warnings
from pathlib import Path
from unittest.mock import MagicMock, patch, call
import urllib.error
import urllib.request
import importlib.util
# Load scripts/sync-labels.py via importlib (handling the hyphen in filename)
_script_path = Path(__file__).resolve().parent.parent / "scripts" / "sync-labels.py"
_spec = importlib.util.spec_from_file_location("sync_labels", _script_path)
sl = importlib.util.module_from_spec(_spec)
sys.modules["sync_labels"] = sl
_spec.loader.exec_module(sl)
def _make_http_error(url, code, msg, headers, body_bytes):
fp = io.BytesIO(body_bytes)
return urllib.error.HTTPError(url, code, msg, headers, fp)
class TestRequestNetworkDrop(unittest.TestCase):
"""Edge cases for complete network drop and connection failures."""
@patch("urllib.request.urlopen")
def test_request_connection_refused(self, mock_urlopen):
mock_urlopen.side_effect = urllib.error.URLError(
ConnectionRefusedError(111, "Connection refused")
)
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("Connection refused", str(ctx.exception))
@patch("urllib.request.urlopen")
def test_request_dns_resolution_failure(self, mock_urlopen):
mock_urlopen.side_effect = urllib.error.URLError(
socket.gaierror(-2, "Name or service not known")
)
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://nonexistent.invalid/test")
self.assertIn("Name or service not known", str(ctx.exception))
@patch("urllib.request.urlopen")
def test_request_network_unreachable(self, mock_urlopen):
mock_urlopen.side_effect = urllib.error.URLError(
OSError(101, "Network is unreachable")
)
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("Network is unreachable", str(ctx.exception))
class TestRequestTimeouts(unittest.TestCase):
"""Edge cases for slow or hung connections and timeouts."""
@patch("urllib.request.urlopen")
def test_request_socket_timeout(self, mock_urlopen):
mock_urlopen.side_effect = urllib.error.URLError(
socket.timeout("The read operation timed out")
)
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("timed out", str(ctx.exception))
@patch("urllib.request.urlopen")
def test_request_timeout_parameter_passed(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'{"ok": true}'
mock_resp.__enter__.return_value = mock_resp
mock_resp.__exit__.return_value = None
mock_urlopen.return_value = mock_resp
sl.request("https://api.github.com/test")
mock_urlopen.assert_called_once()
_, kwargs = mock_urlopen.call_args
self.assertEqual(kwargs.get("timeout"), sl.TIMEOUT)
class TestRequestHttpCodes(unittest.TestCase):
"""Edge cases for HTTP 4xx and 5xx responses, rate limiting, and errors."""
@patch("urllib.request.urlopen")
def test_request_http_401_unauthorized(self, mock_urlopen):
err = _make_http_error(
"https://api.github.com/test",
401,
"Unauthorized",
{},
b'{"message": "Bad credentials"}',
)
mock_urlopen.side_effect = err
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test", token="invalid")
self.assertIn("HTTP 401", str(ctx.exception))
self.assertIn("Bad credentials", str(ctx.exception))
err.close()
@patch("urllib.request.urlopen")
def test_request_http_403_rate_limit(self, mock_urlopen):
err = _make_http_error(
"https://api.github.com/test",
403,
"Forbidden",
{"X-RateLimit-Remaining": "0"},
b'{"message": "API rate limit exceeded for user"}',
)
mock_urlopen.side_effect = err
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("HTTP 403", str(ctx.exception))
self.assertIn("rate limit exceeded", str(ctx.exception))
err.close()
@patch("urllib.request.urlopen")
def test_request_http_404_not_found(self, mock_urlopen):
err = _make_http_error(
"https://api.github.com/test",
404,
"Not Found",
{},
b'{"message": "Not Found"}',
)
mock_urlopen.side_effect = err
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("HTTP 404", str(ctx.exception))
err.close()
@patch("urllib.request.urlopen")
def test_request_http_500_server_error(self, mock_urlopen):
err = _make_http_error(
"https://api.github.com/test",
500,
"Internal Server Error",
{},
b"Internal Server Error",
)
mock_urlopen.side_effect = err
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("HTTP 500", str(ctx.exception))
err.close()
@patch("urllib.request.urlopen")
def test_request_http_502_bad_gateway(self, mock_urlopen):
err = _make_http_error(
"https://api.github.com/test",
502,
"Bad Gateway",
{},
b"<html><body>502 Bad Gateway</body></html>",
)
mock_urlopen.side_effect = err
with self.assertRaises(sl.SyncError) as ctx:
sl.request("https://api.github.com/test")
self.assertIn("HTTP 502", str(ctx.exception))
err.close()
class TestRequestPayloads(unittest.TestCase):
"""Edge cases for payloads, parsing, and request formatting."""
@patch("urllib.request.urlopen")
def test_request_valid_json(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'[{"name": "Kind/Bug", "color": "ee0701"}]'
mock_resp.__enter__.return_value = mock_resp
mock_resp.__exit__.return_value = None
mock_urlopen.return_value = mock_resp
res = sl.request("https://api.github.com/test")
self.assertEqual(res, [{"name": "Kind/Bug", "color": "ee0701"}])
@patch("urllib.request.urlopen")
def test_request_empty_body_204(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b""
mock_resp.__enter__.return_value = mock_resp
mock_resp.__exit__.return_value = None
mock_urlopen.return_value = mock_resp
res = sl.request("https://api.github.com/test", method="DELETE")
self.assertIsNone(res)
@patch("urllib.request.urlopen")
def test_request_headers_and_auth(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b"{}"
mock_resp.__enter__.return_value = mock_resp
mock_resp.__exit__.return_value = None
mock_urlopen.return_value = mock_resp
sl.request(
"https://api.github.com/test",
token="secret_token",
method="POST",
payload={"name": "test"},
)
req = mock_urlopen.call_args[0][0]
self.assertEqual(req.get_header("Authorization"), "Bearer secret_token")
self.assertEqual(req.get_header("Accept"), "application/json")
self.assertEqual(req.get_header("Content-type"), "application/json")
self.assertEqual(req.get_method(), "POST")
self.assertEqual(req.data, b'{"name": "test"}')
@patch("urllib.request.urlopen")
def test_request_corrupted_json_payload(self, mock_urlopen):
mock_resp = MagicMock()
mock_resp.read.return_value = b'{"name": "broken", invalid_json'
mock_resp.__enter__.return_value = mock_resp
mock_resp.__exit__.return_value = None
mock_urlopen.return_value = mock_resp
with self.assertRaises(json.JSONDecodeError):
sl.request("https://api.github.com/test")
class TestPaginate(unittest.TestCase):
"""Edge cases for pagination across API responses."""
@patch("sync_labels.request")
def test_paginate_single_page(self, mock_req):
mock_req.return_value = [{"name": "A"}, {"name": "B"}]
res = sl.paginate("https://api.test?page={page}&per_page={per_page}", per_page=50)
self.assertEqual(len(res), 2)
mock_req.assert_called_once_with(
"https://api.test?page=1&per_page=50", token=None
)
@patch("sync_labels.request")
def test_paginate_multiple_pages(self, mock_req):
page1 = [{"name": f"item{i}"} for i in range(50)]
page2 = [{"name": f"item{i}"} for i in range(50, 70)]
mock_req.side_effect = [page1, page2]
res = sl.paginate("https://api.test?page={page}&per_page={per_page}", per_page=50)
self.assertEqual(len(res), 70)
self.assertEqual(mock_req.call_count, 2)
@patch("sync_labels.request")
def test_paginate_exact_page_boundary(self, mock_req):
page1 = [{"name": f"item{i}"} for i in range(50)]
page2 = []
mock_req.side_effect = [page1, page2]
res = sl.paginate("https://api.test?page={page}&per_page={per_page}", per_page=50)
self.assertEqual(len(res), 50)
self.assertEqual(mock_req.call_count, 2)
@patch("sync_labels.request")
def test_paginate_empty(self, mock_req):
mock_req.return_value = []
res = sl.paginate("https://api.test?page={page}&per_page={per_page}", per_page=50)
self.assertEqual(res, [])
mock_req.assert_called_once()
class TestLabelOperations(unittest.TestCase):
"""Edge cases for label CRUD operations and usage querying."""
@patch("sync_labels.request")
def test_usage_count_zero(self, mock_req):
mock_req.return_value = []
count = sl.usage_count("Unused Label", "token")
self.assertEqual(count, 0)
mock_req.assert_called_once_with(
f"{sl.GITHUB_API}/repos/{sl.GITHUB_REPO}/issues"
f"?labels=Unused%20Label&state=all&per_page=100",
token="token",
)
@patch("sync_labels.request")
def test_usage_count_active(self, mock_req):
mock_req.return_value = [{"number": 1}, {"number": 2}]
count = sl.usage_count("Priority/High", "token")
self.assertEqual(count, 2)
mock_req.assert_called_once_with(
f"{sl.GITHUB_API}/repos/{sl.GITHUB_REPO}/issues"
f"?labels=Priority%2FHigh&state=all&per_page=100",
token="token",
)
@patch("sync_labels.request")
def test_create_label(self, mock_req):
label = {"name": "Test/Label", "color": "123456", "description": "Desc"}
sl.create_label(label, "tok")
mock_req.assert_called_once_with(
f"{sl.GITHUB_API}/repos/{sl.GITHUB_REPO}/labels",
token="tok",
method="POST",
payload=label,
)
@patch("sync_labels.request")
def test_update_label(self, mock_req):
label = {"name": "Area/Prompt & Theme", "color": "abcdef", "description": "New"}
sl.update_label(label, "tok")
mock_req.assert_called_once_with(
f"{sl.GITHUB_API}/repos/{sl.GITHUB_REPO}/labels/Area%2FPrompt%20%26%20Theme",
token="tok",
method="PATCH",
payload={
"new_name": "Area/Prompt & Theme",
"color": "abcdef",
"description": "New",
},
)
@patch("sync_labels.request")
def test_delete_label(self, mock_req):
sl.delete_label("Reviewed/Won't Fix", "tok")
mock_req.assert_called_once_with(
f"{sl.GITHUB_API}/repos/{sl.GITHUB_REPO}/labels/Reviewed%2FWon%27t%20Fix",
token="tok",
method="DELETE",
)
class TestRunAndMainWorkflow(unittest.TestCase):
"""End-to-end execution testing dry-run, live execution, and error handling."""
def setUp(self):
self.env_patcher = patch.dict(os.environ, {}, clear=True)
self.env_patcher.start()
def tearDown(self):
self.env_patcher.stop()
def test_run_missing_token_not_dry_run_raises(self):
with self.assertRaises(sl.SyncError) as ctx:
sl.run(dry_run=False)
self.assertIn("GH_MIRROR_TOKEN is not set", str(ctx.exception))
@patch("sync_labels.fetch_gitea_labels")
def test_run_gitea_empty_raises(self, mock_gitea):
os.environ[sl.TOKEN_ENV] = "dummy_token"
mock_gitea.return_value = []
with self.assertRaises(sl.SyncError) as ctx:
sl.run(dry_run=False)
self.assertIn("Gitea returned no labels", str(ctx.exception))
@patch("sys.stdout", new_callable=io.StringIO)
@patch("sync_labels.delete_label")
@patch("sync_labels.update_label")
@patch("sync_labels.create_label")
@patch("sync_labels.usage_count")
@patch("sync_labels.fetch_github_labels")
@patch("sync_labels.fetch_gitea_labels")
def test_run_dry_run_makes_no_mutating_calls(
self, mock_gitea, mock_gh, mock_usage, mock_create, mock_update, mock_delete, mock_stdout
):
mock_gitea.return_value = [
{"name": "Keep", "color": "111111", "description": "Same"},
{"name": "New", "color": "222222", "description": "Created"},
{"name": "Change", "color": "333333", "description": "Updated"},
]
mock_gh.return_value = [
{"name": "Keep", "color": "111111", "description": "Same"},
{"name": "Change", "color": "000000", "description": "Old"},
{"name": "ExtraUnused", "color": "444444", "description": "Deleted"},
{"name": "ExtraUsed", "color": "555555", "description": "Kept"},
]
mock_usage.side_effect = lambda name, token: 1 if name == "ExtraUsed" else 0
# In dry run, token is not strictly required
status = sl.run(dry_run=True)
self.assertEqual(status, 0)
mock_create.assert_not_called()
mock_update.assert_not_called()
mock_delete.assert_not_called()
@patch("sys.stdout", new_callable=io.StringIO)
@patch("sync_labels.delete_label")
@patch("sync_labels.update_label")
@patch("sync_labels.create_label")
@patch("sync_labels.usage_count")
@patch("sync_labels.fetch_github_labels")
@patch("sync_labels.fetch_gitea_labels")
def test_run_live_executes_plan(
self, mock_gitea, mock_gh, mock_usage, mock_create, mock_update, mock_delete, mock_stdout
):
os.environ[sl.TOKEN_ENV] = "my_token"
mock_gitea.return_value = [
{"name": "Keep", "color": "111111", "description": "Same"},
{"name": "New", "color": "222222", "description": "Created"},
{"name": "Change", "color": "333333", "description": "Updated"},
]
mock_gh.return_value = [
{"name": "Keep", "color": "111111", "description": "Same"},
{"name": "Change", "color": "000000", "description": "Old"},
{"name": "ExtraUnused", "color": "444444", "description": "Deleted"},
{"name": "ExtraUsed", "color": "555555", "description": "Kept"},
]
mock_usage.side_effect = lambda name, token: 1 if name == "ExtraUsed" else 0
status = sl.run(dry_run=False)
self.assertEqual(status, 0)
mock_create.assert_called_once_with(
{"name": "New", "color": "222222", "description": "Created"}, "my_token"
)
mock_update.assert_called_once()
mock_delete.assert_called_once_with("ExtraUnused", "my_token")
@patch("sys.stdout", new_callable=io.StringIO)
def test_main_self_test(self, mock_stdout):
self.assertEqual(sl.main(["--self-test"]), 0)
@patch("sync_labels.run")
def test_main_sync_error_exit_code(self, mock_run):
mock_run.side_effect = sl.SyncError("Boom")
with patch("sys.stderr", new=io.StringIO()) as fake_err:
rc = sl.main(["--dry-run"])
self.assertEqual(rc, 1)
self.assertIn("error: Boom", fake_err.getvalue())
if __name__ == "__main__":
unittest.main()