Integrate the shared sudo lifecycle foundation

This commit is contained in:
Afonso Oliveira committed 2026-09-07 23:10:29 +01:00
commit 75e58f034f
26 files changed
+1596 -164

No files matched your search

+3 -2
View File
@@ -31,8 +31,9 @@ chmod +x "$mock_bin"/*
launch_log="$test_tmp/launch-log"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=true OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
grep -Fxq 'launch:-- 1password' "$launch_log" || fail "1Password launcher starts the installed app"
pass "1Password launcher starts the installed app"
grep -Fxq 'launch:-- 1password --force-device-scale-factor=1' "$launch_log" ||
fail "1Password launcher starts the installed app at a fixed scale factor"
pass "1Password launcher starts the installed app at a fixed scale factor"
PATH="$mock_bin:$PATH" OMARCHY_TEST_INSTALLED=false OMARCHY_TEST_LOG="$launch_log" \
bash "$ROOT/bin/omarchy-launch-1password"
+438 -103
View File
@@ -2,122 +2,457 @@
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
script="$ROOT/bin/omarchy-sudo-passwordless"
tmpfiles_file="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
command_path="$ROOT/bin/omarchy-sudo-passwordless"
security_library_path="$ROOT/bin/omarchy-security-functions"
tmpfiles_path="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
migration_path="$ROOT/migrations/1788163635.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
grant="$test_tmp/grant"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/gum" <<'SH'
#!/bin/bash
exit 0
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$TEST_CALLS"
[[ ${1:-} == "is-active" && ${TEST_TIMER_ACTIVE:-false} == "true" ]]
SH
cat >"$mock_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$TEST_CALLS"
case ${1:-} in
test)
[[ ${2:-} == "-f" && -f $TEST_GRANT ]]
;;
tee)
/usr/bin/tee "$TEST_GRANT"
;;
chmod)
/usr/bin/chmod "$2" "$TEST_GRANT"
;;
systemd-run)
[[ ${TEST_FAIL_SYSTEMD_RUN:-false} != "true" ]]
;;
rm)
/usr/bin/rm -f -- "$TEST_GRANT"
;;
systemctl)
exit 0
;;
*)
echo "unexpected sudo command: $*" >&2
exit 90
;;
esac
SH
chmod +x "$mock_bin/gum" "$mock_bin/sudo" "$mock_bin/systemctl"
run_command() {
TEST_CALLS="$calls" TEST_GRANT="$grant" PATH="$mock_bin:$PATH" USER=alice \
"$script" "$@"
function_prefix() {
printf 'source %q\n' "$security_library_path"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$command_path"
}
: >"$calls"
enable_output=$(run_command 15)
[[ -f $grant ]] || fail "successful timer setup leaves the passwordless sudo grant enabled"
[[ $(cat "$grant") == "alice ALL=(ALL) NOPASSWD: ALL" ]] ||
fail "the enabled grant belongs to the current user" "$(cat "$grant")"
grep -q '^sudo systemd-run --on-active=15m .* rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-alice$' "$calls" ||
fail "enabling arms the expiry timer" "$(cat "$calls")"
[[ $enable_output == *"automatically disable in 15 minutes"* ]] ||
fail "success is reported after the timer is armed" "$enable_output"
pass "enabling arms expiry before reporting success"
# Exercise the validation code itself. Leading zeroes remain numeric, but zero,
# negatives, oversized grants, and shell syntax are rejected.
(
source <(function_prefix)
for minutes in 1 15 1440 00015; do
valid_minutes "$minutes" || fail "passwordless sudo accepts bounded duration $minutes"
done
for minutes in 0 1441 -1 1m '1;id' '' 18446744073709551617; do
! valid_minutes "$minutes" || fail "passwordless sudo rejects invalid duration '$minutes'"
done
)
pass "passwordless sudo validates a bounded positive duration"
: >"$calls"
rm -f "$grant"
if failure_output=$(TEST_FAIL_SYSTEMD_RUN=true run_command 15 2>&1); then
fail "enabling fails when the expiry timer cannot be armed"
# The public entry point uses the kernel-backed numeric identity; $USER is
# never interpolated into a privileged filename or sudoers rule.
grep -F 'uid=$(/usr/bin/id -u)' "$command_path" >/dev/null ||
fail "passwordless sudo derives the caller from id -u"
! grep -Eq '\$\{?USER\}?' "$command_path" ||
fail "passwordless sudo does not trust USER for privileged policy"
grep -F '[[ ${SUDO_UID:-} =~ ^[0-9]+$ ]]' "$command_path" >/dev/null ||
fail "passwordless sudo validates sudo provenance"
pass "passwordless sudo derives and validates trusted account identity"
# Status inspection and the confirmation UI are mixed-trust: a normal sudo
# status call would publish a timestamp that a hostile prompt helper could use
# even when the user declines the grant. Exercise the public flow with a sudo
# model that publishes a token only when -N is missing.
grep -Fxq '#!/bin/bash -p' "$command_path" ||
fail "passwordless sudo no longer suppresses Bash startup injection"
public_sudo_stub="$test_tmp/public-sudo"
public_gum_stub="$test_tmp/public-gum"
public_token="$test_tmp/public-token"
public_exploit="$test_tmp/public-exploit"
cat >"$public_sudo_stub" <<'STUB'
#!/bin/bash
if [[ ${1:-} == -h ]]; then
echo 'usage: sudo [-ABbEHkNnPS] command'
exit 0
fi
[[ ! -e $grant ]] || fail "timer setup failure revokes the new passwordless sudo grant"
[[ $failure_output == *"Revoking access now"* ]] ||
fail "timer setup failure explains the fail-closed revocation" "$failure_output"
[[ $failure_output != *"Passwordless sudo has been ENABLED"* ]] ||
fail "timer setup failure does not report that passwordless sudo was enabled" "$failure_output"
pass "timer setup failure revokes a new grant"
: >"$calls"
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$grant"
if update_output=$(TEST_TIMER_ACTIVE=true TEST_FAIL_SYSTEMD_RUN=true run_command 30 2>&1); then
fail "updating fails when the replacement expiry timer cannot be armed"
if [[ ${1:-} == -k ]]; then
rm -f -- "$TEST_PUBLIC_TOKEN"
exit 0
fi
[[ ! -e $grant ]] || fail "timer update failure revokes the existing passwordless sudo grant"
[[ $update_output != *"timer updated"* ]] ||
fail "timer update failure does not report success" "$update_output"
pass "timer update failure revokes the existing grant"
no_update=0
if [[ ${1:-} == -N ]]; then no_update=1; shift; fi
[[ ${1:-} != -- ]] || shift
((no_update)) || : >"$TEST_PUBLIC_TOKEN"
case "${2:-}" in
__status) exit "${TEST_PUBLIC_STATUS:-3}" ;;
__enable|__disable) exit 0 ;;
*) exit 2 ;;
esac
STUB
cat >"$public_gum_stub" <<'STUB'
#!/bin/bash
[[ -z ${TEST_PUBLIC_GUM_LOG:-} ]] || : >"$TEST_PUBLIC_GUM_LOG"
[[ ! -e $TEST_PUBLIC_TOKEN ]] || : >"$TEST_PUBLIC_EXPLOIT"
exit 1
STUB
chmod 0755 "$public_sudo_stub" "$public_gum_stub"
public_flow="$test_tmp/passwordless-public-flow"
/usr/bin/sed "s#/usr/bin/sudo#$public_sudo_stub#g" "$security_library_path" >"$test_tmp/omarchy-security-functions"
/usr/bin/sed \
-e "s#/usr/bin/sudo#$public_sudo_stub#g" \
-e "s#/usr/bin/gum#$public_gum_stub#g" \
"$command_path" >"$public_flow"
chmod 0755 "$public_flow"
TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
/usr/bin/bash -p "$public_flow" 15 >/dev/null
[[ ! -e $public_token && ! -e $public_exploit ]] ||
fail "passwordless confirmation inherited a reusable status credential"
for status in 1 2; do
if TEST_PUBLIC_TOKEN="$public_token" TEST_PUBLIC_EXPLOIT="$public_exploit" \
TEST_PUBLIC_STATUS="$status" TEST_PUBLIC_GUM_LOG="$test_tmp/unsafe-status-confirmation" \
/usr/bin/bash -p "$public_flow" 15 >"$test_tmp/status-error.output" 2>&1; then
fail "passwordless sudo treats status/authorization failure $status as inactive"
fi
[[ ! -e $test_tmp/unsafe-status-confirmation ]] || fail "failed status inspection opens the enable prompt"
grep -q 'Could not safely inspect passwordless sudo' "$test_tmp/status-error.output" ||
fail "failed status inspection lacks recovery guidance"
done
mapfile -t tmpfiles_rules < <(grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_file")
(( ${#tmpfiles_rules[@]} == 1 )) ||
fail "passwordless sudo ships one tmpfiles rule" "${tmpfiles_rules[*]}"
startup_env="$test_tmp/passwordless-bash-env"
startup_marker="$test_tmp/passwordless-bash-env-ran"
cat >"$startup_env" <<'STUB'
: >"$TEST_STARTUP_MARKER"
set -o privileged
unset BASH_ENV
STUB
if BASH_ENV="$startup_env" TEST_STARTUP_MARKER="$startup_marker" \
/usr/bin/bash "$public_flow" -p >/dev/null 2>&1; then
fail "passwordless sudo accepted an unsafe interpreter with a decoy -p"
fi
[[ -e $startup_marker && ! -e $public_token && ! -e $public_exploit ]] ||
fail "unsafe passwordless startup reached its sudo workflow"
pass "passwordless confirmation uses a cold command-scoped credential boundary"
fake_root="$test_tmp/root"
# Source a path-rewritten copy so the real cleanup implementation can be
# exercised without touching /etc. Exact generated numeric rules are removed
# even after account deletion or a crash before state publication. Anything an
# administrator changed, and every symlink, is preserved.
fake_sudoers="$test_tmp/sudoers.d"
mkdir "$fake_sudoers"
rewritten="$test_tmp/passwordless-lib.sh"
function_prefix | sed "s#/etc/sudoers.d#$fake_sudoers#g" >"$rewritten"
(
source "$rewritten"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$fake_sudoers/99-omarchy-nopasswd-424242"
printf 'admin ALL=(ALL) NOPASSWD: /usr/bin/pacman\n' >"$fake_sudoers/99-omarchy-nopasswd-424243"
ln -s "$fake_sudoers/99-omarchy-nopasswd-424243" "$fake_sudoers/99-omarchy-nopasswd-424244"
remove_known_legacy_rules
)
[[ ! -e $fake_sudoers/99-omarchy-nopasswd-424242 ]] ||
fail "boot cleanup removes a state-less numeric orphan"
[[ -f $fake_sudoers/99-omarchy-nopasswd-424243 ]] ||
fail "boot cleanup preserves administrator-authored policy"
[[ -L $fake_sudoers/99-omarchy-nopasswd-424244 ]] ||
fail "boot cleanup refuses sudoers symlinks"
pass "boot cleanup removes crash/deleted-account orphans conservatively"
# A boot gate must not report success when deletion itself fails. Exercise the
# real cleanup and post-cleanup verification with a deterministic failing rm.
rm_failure_dir="$test_tmp/rm-failure-sudoers"
mkdir "$rm_failure_dir"
printf 'deleteduser ALL=(ALL) NOPASSWD: ALL\n' >"$rm_failure_dir/99-omarchy-nopasswd-424245"
failing_rm="$test_tmp/failing-rm"
cat >"$failing_rm" <<'FAILING_RM'
#!/bin/bash
exit 1
FAILING_RM
chmod +x "$failing_rm"
rm_failure_lib="$test_tmp/rm-failure-lib.sh"
function_prefix |
sed -e "s#/etc/sudoers.d#$rm_failure_dir#g" \
-e "s#/var/lib/omarchy/sudo-passwordless#$test_tmp/empty-state#g" \
-e "s#/usr/bin/rm#$failing_rm#g" >"$rm_failure_lib"
mkdir "$test_tmp/empty-state"
(
source "$rm_failure_lib"
! cleanup_all_locked
) || fail "boot cleanup fails when an Omarchy rule cannot be removed"
[[ -f $rm_failure_dir/99-omarchy-nopasswd-424245 ]] ||
fail "rm-failure fixture remains available for verification"
pass "boot cleanup fails closed when policy deletion fails"
# Reproduce the migration's real sudo provenance: sudo sets SUDO_UID. Rewrite
# only the read-only EUID probe so this unprivileged test can exercise the root
# dispatcher, then assert that cleanup (which can only revoke privilege) runs.
dispatch_lib="$test_tmp/dispatch-lib.sh"
function_prefix | sed 's/((EUID == 0))/((TEST_EUID == 0))/g' >"$dispatch_lib"
(
source "$dispatch_lib"
called=""
cleanup_all_locked() { called=cleanup; }
with_root_lock() { "$@"; }
TEST_EUID=0 SUDO_UID=1000 root_dispatch __cleanup-all
[[ $called == cleanup ]]
) || fail "migration cleanup dispatch accepts authenticated sudo provenance"
pass "migration can invoke fail-closed cleanup through sudo"
# A grant cannot be published until the static unit is verified/enabled, and a
# timer setup failure removes its pending state without calling publish_rule.
transaction_dir="$test_tmp/transaction"
mkdir "$transaction_dir"
transaction_lib="$test_tmp/transaction-lib.sh"
function_prefix | sed "s#/var/lib/omarchy/sudo-passwordless#$transaction_dir#g" >"$transaction_lib"
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 1; }
publish_rule() { return 99; }
! enable_locked 1000 15
)
(
source "$transaction_lib"
ACCOUNT_NAME=audituser
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
publish_rule() { printf published >"$transaction_dir/published"; }
cleanup_uid_locked() { : >"$transaction_dir/failed-timer-cleanup"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/pending && ! -e $transaction_dir/published &&
-e $transaction_dir/failed-timer-cleanup ]]
) || fail "passwordless sudo fails closed on prerequisite/timer failure"
pass "passwordless sudo publishes no rule after partial setup failure"
# Erik's predecessor fix revoked an already-active grant when an extension
# could not arm its replacement timer. Keep that fail-closed property while
# the new transaction deliberately leaves the old timer armed until the new
# one is verified.
replacement_state="$transaction_dir/1000.state"
replacement_rule="$transaction_dir/1000.rule"
replacement_stopped="$transaction_dir/old-timer-stopped"
old_timer=omarchy-nopasswd-expire-1000-aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
printf 'UID=1000\nUSER=audituser\nEXPIRES=2000000000\nTIMER=%s\n' "$old_timer" >"$replacement_state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$replacement_rule"
(
source "$transaction_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
state_file() { printf '%s' "$replacement_state"; }
rule_file() { printf '%s' "$replacement_rule"; }
prepare_state_file() { local pending="$transaction_dir/replacement-pending"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 1; }
stop_timer() { [[ $1 == "$old_timer" ]] && : >"$replacement_stopped"; }
! enable_locked 1000 30
[[ ! -e $replacement_state && ! -e $replacement_rule && -e $replacement_stopped ]]
) || fail "passwordless sudo leaves an existing grant live after replacement timer failure"
pass "replacement timer failure revokes the existing grant"
# Expiry is a wall-clock promise, so the transient timer must carry the exact
# absolute epoch recorded in root state. A monotonic-only --on-active timer
# pauses during suspend and can otherwise extend a short grant by hours.
timer_args="$test_tmp/timer-args"
calendar_systemd_run="$test_tmp/calendar-systemd-run"
calendar_systemctl="$test_tmp/calendar-systemctl"
cat >"$calendar_systemd_run" <<'STUB'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_TIMER_ARGS"
STUB
cat >"$calendar_systemctl" <<'STUB'
#!/bin/bash
exit 0
STUB
chmod 0755 "$calendar_systemd_run" "$calendar_systemctl"
calendar_lib="$test_tmp/calendar-lib.sh"
function_prefix |
sed -e "s#/usr/bin/systemd-run#$calendar_systemd_run#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$calendar_lib"
(
source "$calendar_lib"
TEST_TIMER_ARGS="$timer_args" start_expiry_timer 1000 2000000000 \
omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
) || fail "passwordless sudo cannot arm its absolute expiry timer"
grep -Fx -- '--on-calendar=@2000000000' "$timer_args" >/dev/null ||
fail "passwordless sudo timer does not advance across suspend"
pass "passwordless sudo arms the recorded absolute wall-clock expiry"
# A resumed machine can briefly observe the timer as active before systemd
# dispatches its overdue service. Status must independently enforce EXPIRES and
# synchronously remove policy instead of trusting timer activity alone.
expired_state="$test_tmp/expired-state"
expired_sudoers="$test_tmp/expired-sudoers"
mkdir "$expired_state" "$expired_sudoers"
expired_timer=omarchy-nopasswd-expire-1000-0123456789abcdef0123456789abcdef
printf 'UID=1000\nUSER=audituser\nEXPIRES=1\nTIMER=%s\n' "$expired_timer" >"$expired_state/1000.state"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$expired_sudoers/99-omarchy-nopasswd-1000"
expired_lib="$test_tmp/expired-lib.sh"
function_prefix |
sed -e "s#/var/lib/omarchy/sudo-passwordless#$expired_state#g" \
-e "s#/etc/sudoers.d#$expired_sudoers#g" \
-e "s#/usr/bin/systemctl#$calendar_systemctl#g" >"$expired_lib"
(
source "$expired_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
! status_locked 1000
) || fail "passwordless sudo accepts expired root state while its timer is active"
[[ ! -e $expired_state/1000.state && ! -e $expired_sudoers/99-omarchy-nopasswd-1000 ]] ||
fail "passwordless sudo does not synchronously revoke expired state"
pass "passwordless sudo enforces wall-clock expiry independently of timer dispatch"
# If the transient timer fires between its first active check and publication,
# the just-created rule must be synchronously revoked instead of surviving to
# reboot. Model that narrow transition with the real enable transaction.
inactive_systemctl="$test_tmp/inactive-systemctl"
cat >"$inactive_systemctl" <<'STUB'
#!/bin/bash
exit 1
STUB
chmod 0755 "$inactive_systemctl"
post_publish_lib="$test_tmp/post-publish-lib.sh"
sed "s#/usr/bin/systemctl#$inactive_systemctl#g" "$transaction_lib" >"$post_publish_lib"
(
source "$post_publish_lib"
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
verify_boot_cleanup() { return 0; }
read_state_timer() { return 1; }
prepare_state_file() { local pending="$transaction_dir/pending-after-arm"; : >"$pending"; printf %s "$pending"; }
start_expiry_timer() { return 0; }
publish_rule() { : >"$transaction_dir/published-after-arm"; }
cleanup_uid_locked() { rm -f "$transaction_dir/published-after-arm"; : >"$transaction_dir/revoked-after-arm"; }
! enable_locked 1000 15
[[ ! -e $transaction_dir/published-after-arm && -e $transaction_dir/revoked-after-arm ]]
) || fail "passwordless sudo leaves a grant when its armed timer expires before publication completes"
pass "timer expiry during publication revokes the grant synchronously"
# Follow the maintainer's package-owned tmpfiles design: one boot-only rule
# owns this filename namespace. A routine --remove leaves live grants alone;
# early boot removes them before a user can log in. The migration only revokes
# legacy runtime state and never writes static policy into /usr.
mapfile -t tmpfiles_rules < <(/usr/bin/grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_path")
(( ${#tmpfiles_rules[@]} == 1 )) || fail "passwordless sudo ships one boot cleanup rule"
[[ ${tmpfiles_rules[0]} == 'r! /etc/sudoers.d/99-omarchy-nopasswd-*' ]] ||
fail "passwordless sudo boot cleanup does not own the exact generated namespace"
fake_root="$test_tmp/tmpfiles-root"
sudoers_dir="$fake_root/etc/sudoers.d"
mkdir -p "$sudoers_dir"
grant_names=(alice buildbot-2 user.123 'service$')
for grant_name in "${grant_names[@]}"; do
touch "$sudoers_dir/99-omarchy-nopasswd-$grant_name"
for name in alice buildbot-2 424242; do
: >"$sudoers_dir/99-omarchy-nopasswd-$name"
done
touch "$sudoers_dir/omarchy-dns"
: >"$sudoers_dir/omarchy-dns"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[0]}"
[[ -e $sudoers_dir/99-omarchy-nopasswd-alice ]] || fail "non-boot tmpfiles run shortened a live grant"
/usr/bin/systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[0]}"
! find "$sudoers_dir" -name '99-omarchy-nopasswd-*' -print -quit | /usr/bin/grep -q . ||
fail "boot cleanup left a generated passwordless grant"
[[ -e $sudoers_dir/omarchy-dns ]] || fail "boot cleanup removed an unrelated sudoers rule"
/usr/bin/grep -Fx 'sudo /usr/bin/omarchy-sudo-passwordless __cleanup-all' "$migration_path" >/dev/null
! /usr/bin/grep -q 'omarchy-sudo-passwordless-cleanup.service' "$migration_path" ||
fail "migration retained a custom boot service instead of package-owned tmpfiles"
pass "package-owned boot cleanup is narrow, boot-only, and migration-safe"
systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[@]}"
[[ -f $sudoers_dir/99-omarchy-nopasswd-alice ]] ||
fail "boot-only cleanup leaves a live grant alone outside boot"
systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[@]}"
for grant_name in "${grant_names[@]}"; do
stale_grant="$sudoers_dir/99-omarchy-nopasswd-$grant_name"
[[ ! -e $stale_grant ]] || fail "boot cleanup removes every generated grant" "$stale_grant"
# Removing the settings package also removes the tmpfiles rule. Its package
# lifecycle must therefore revoke the same owned namespace synchronously, while
# preserving every unrelated sudoers file.
pkgs_candidates=(
"${OMARCHY_PKGS_PATH:-}"
"$ROOT/../omarchy-pkgs"
"$ROOT/../../omarchy-pkgs"
"$HOME/Work/omarchy/omarchy-pkgs"
"$HOME/Work/omacom/omarchy-pkgs"
)
pkgs_root=""
for candidate in "${pkgs_candidates[@]}"; do
if [[ -n $candidate && -d $candidate/pkgbuilds/omarchy-settings ]]; then
pkgs_root=$candidate/pkgbuilds
break
elif [[ -n $candidate && -d $candidate/omarchy-settings ]]; then
pkgs_root=$candidate
break
fi
done
[[ -f $sudoers_dir/omarchy-dns ]] || fail "boot cleanup preserves unrelated sudoers rules"
pass "systemd-tmpfiles removes generated grants only during boot"
[[ -n $pkgs_root ]] || fail "omarchy-pkgs checkout found for passwordless package-removal coverage"
for package_name in omarchy-settings omarchy-settings-dev; do
install_script="$pkgs_root/$package_name/$package_name.install"
transformed_install="$test_tmp/$package_name.install"
removal_root="$test_tmp/$package_name-remove"
removal_sudoers="$removal_root/etc/sudoers.d"
mkdir -p "$removal_sudoers" "$removal_root/run/lock" "$removal_root/etc/tmpfiles.d"
: >"$removal_sudoers/99-omarchy-nopasswd-1000"
: >"$removal_sudoers/99-omarchy-nopasswd-legacy-user"
: >"$removal_sudoers/omarchy-dns"
ln -s ../administrator/os-release "$removal_root/etc/os-release"
package_stat="$test_tmp/package-stat"
cat >"$package_stat" <<'STUB'
#!/bin/bash
if [[ $2 == '%u' ]]; then printf '0\n'; else /usr/bin/stat "$@"; fi
STUB
chmod +x "$package_stat"
sed -e "s#/etc/#$removal_root/etc/#g" \
-e "s#/run#$removal_root/run#g" \
-e "s#/usr/bin/stat#$package_stat#g" "$install_script" >"$transformed_install"
(
source "$transformed_install"
pre_remove
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
post_remove
) || fail "$package_name removal revokes active passwordless grants"
! find "$removal_sudoers" -name '99-omarchy-nopasswd-*' -print -quit | grep -q . ||
fail "$package_name removal leaves a passwordless grant behind"
[[ -e $removal_sudoers/omarchy-dns ]] ||
fail "$package_name removal deletes an unrelated sudoers policy"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal changes unrelated OS metadata"
: >"$removal_sudoers/99-omarchy-nopasswd-1001"
(
source "$transformed_install"
post_remove
) || fail "$package_name removal handles administrator OS selector state"
[[ $(readlink "$removal_root/etc/os-release") == ../administrator/os-release ]] ||
fail "$package_name removal overwrites an administrator OS selector"
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1001 ]] ||
fail "$package_name removal grant cleanup depends on OS selector state"
(
source "$transformed_install"
_etc_overrides_apply() { :; }
if post_install; then exit 1; fi
[[ -f $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_root/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
post_install
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
: >"$removal_sudoers/99-omarchy-nopasswd-1002"
pre_upgrade
[[ ! -e $removal_sudoers/99-omarchy-nopasswd-1002 ]]
post_upgrade
[[ ! -e $removal_root/run/omarchy-sudo-passwordless-package-removing ]]
) || fail "$package_name restores grant availability only after boot cleanup is installed"
done
pass "settings package transitions revoke grants and preserve unrelated configuration"
# Exercise the production flock wrapper under contention. mkdir is an atomic
# overlap detector; all workers must enter and leave the protected region.
lock_dir="$test_tmp/lock-runtime"
mkdir "$lock_dir"
lock_lib="$test_tmp/lock-lib.sh"
function_prefix |
sed -e "s#/run/omarchy/sudo-passwordless#$lock_dir#g" \
-e "s#/run/lock/omarchy-sudo-passwordless.lock#$test_tmp/passwordless.lock#g" \
-e 's#/usr/bin/chown root:root "$LOCK_FILE"#/usr/bin/true#' >"$lock_lib"
worker="$test_tmp/worker.sh"
cat >"$worker" <<'WORKER'
#!/bin/bash
set -euo pipefail
source "$LOCK_LIB"
prepare_root_state() { :; }
critical() {
mkdir "$LOCK_SENTINEL"
sleep 0.03
rmdir "$LOCK_SENTINEL"
printf x >>"$LOCK_RESULTS"
}
with_root_lock critical
WORKER
chmod +x "$worker"
for _ in {1..8}; do
LOCK_LIB="$lock_lib" LOCK_SENTINEL="$test_tmp/held" LOCK_RESULTS="$test_tmp/results" bash "$worker" &
done
wait
[[ $(wc -c <"$test_tmp/results") == 8 ]] || fail "concurrent passwordless operations serialize"
pass "passwordless sudo serializes concurrent operations"
# Same-boot expiry calls the fixed installed cleanup command, and cleanup
# removes policy before touching a timer so timer failures cannot extend it.
grep -F '"$INSTALLED_SELF" __expire "$uid" "$timer"' "$command_path" >/dev/null
cleanup_body=$(awk '/^cleanup_uid_locked\(\) \{/ { in_body=1 } in_body { print } in_body && /^}/ { exit }' "$command_path")
rm_line=$(grep -n '/usr/bin/rm -f' <<<"$cleanup_body" | head -1 | cut -d: -f1)
stop_line=$(grep -n 'stop_timer' <<<"$cleanup_body" | tail -1 | cut -d: -f1)
((rm_line < stop_line)) || fail "expiry removes sudo policy before timer cleanup"
pass "same-boot expiration is fixed-target and fail closed"
@@ -0,0 +1,235 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
children=()
cleanup() {
local status=$?
trap - EXIT
if (( ${#children[@]} )); then
kill "${children[@]}" 2>/dev/null || true
wait "${children[@]}" 2>/dev/null || true
fi
rm -rf "$test_tmp"
exit "$status"
}
trap cleanup EXIT
# All policy, state, locks and command mutations stay in this private fixture.
# Native visudo validates inert fragments; no test installs host sudo policy.
mkdir -p "$test_tmp/bin" "$test_tmp/state" "$test_tmp/etc/sudoers.d" "$test_tmp/etc/tmpfiles.d" "$test_tmp/run/lock" "$test_tmp/hooks"
export TEST_GRANT_ROOT="$test_tmp"
cat >"$test_tmp/bin/stat" <<'STUB'
#!/bin/bash
case $2 in
'%u') printf '0\n' ;;
'%a') if [[ -d ${@: -1} ]]; then printf '755\n'; else printf '644\n'; fi ;;
'%u %a') if [[ -d ${@: -1} ]]; then printf '0 755\n'; else printf '0 644\n'; fi ;;
*) exec /usr/bin/stat "$@" ;;
esac
STUB
cat >"$test_tmp/bin/install" <<'STUB'
#!/bin/bash
args=()
while (($#)); do
case $1 in -o|-g) shift 2 ;; *) args+=("$1"); shift ;; esac
done
exec /usr/bin/install "${args[@]}"
STUB
cat >"$test_tmp/bin/rm" <<'STUB'
#!/bin/bash
for path in "$@"; do
if [[ ${TEST_FAIL_TEMP_CLEANUP:-0} == 1 && $path == "$TEST_GRANT_ROOT/state/".sudoers.* ]]; then exit 1; fi
if [[ ${TEST_FAIL_RULE_DELETE:-0} == 1 && $path == "$TEST_GRANT_ROOT/etc/sudoers.d/"* ]]; then exit 1; fi
done
exec /usr/bin/rm "$@"
STUB
cat >"$test_tmp/bin/systemctl" <<'STUB'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_GRANT_ROOT/systemctl.log"
exit 0
STUB
chmod +x "$test_tmp/bin/"*
library="$test_tmp/grant-functions.sh"
{
printf 'source %q\n' "$ROOT/bin/omarchy-security-functions"
awk '/^set -euo pipefail$/ { functions=1 } /^case "\$\{1:-\}" in$/ { exit } functions { print }' "$ROOT/bin/omarchy-sudo-passwordless"
} | sed \
-e "s|/var/lib/omarchy/sudo-passwordless|$test_tmp/state|g" \
-e "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|g" \
-e "s|/etc/tmpfiles.d|$test_tmp/etc/tmpfiles.d|g" \
-e "s|/usr/share/libalpm/hooks|$test_tmp/hooks|g" \
-e "s|/run/lock/omarchy-sudo-passwordless.lock|$test_tmp/run/lock/omarchy-sudo-passwordless.lock|g" \
-e "s|/run/omarchy-sudo-passwordless-package-removing|$test_tmp/run/omarchy-sudo-passwordless-package-removing|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/install|$test_tmp/bin/install|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" \
-e "s|/usr/bin/systemctl|$test_tmp/bin/systemctl|g" \
-e 's|/usr/bin/chown|/usr/bin/true|g' >"$library"
cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/hooks/"
printf 'r! /etc/sudoers.d/99-omarchy-nopasswd-*\n' >"$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
# The expected policy text is mapped along with its filename in this fixture.
sed -i "s|/etc/sudoers.d|$test_tmp/etc/sudoers.d|" "$test_tmp/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
(
source "$library"
for name in 'buildbot$' audituser aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
valid_account_name "$name" || fail "supported account name rejected: $name"
printf '%s ALL=(ALL) NOPASSWD: ALL\n' "$name" >"$test_tmp/name-policy"
/usr/sbin/visudo -cf "$test_tmp/name-policy" >/dev/null
done
for name in 'a$b' '$' aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa; do
! valid_account_name "$name" || fail "invalid account name accepted"
done
! valid_uid 18446744073709551617 || fail "overflowed UID accepted"
printf 'buildbot$ ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot$"
remove_known_legacy_rules
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-buildbot\$ ]]
) || fail "supported account names and legacy cleanup disagree"
pass "provisioning-compatible names validate as sudoers and clean up correctly"
transaction_setup() {
resolve_account() { ACCOUNT_NAME=audituser; ACCOUNT_UID=1000; }
prepare_root_state() { :; }
start_expiry_timer() { printf '%s\n' "$3" >>"$test_tmp/armed"; }
stop_timer() { printf '%s\n' "$1" >>"$test_tmp/stopped"; }
}
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) && ! -e $(state_file 1000) && -s $test_tmp/stopped ]]
) || fail "post-publication cleanup failure did not revoke before timer cleanup"
pass "failed temporary cleanup after publication revokes the live policy"
rm -f "$test_tmp/stopped"
(
source "$library"
transaction_setup
TEST_FAIL_TEMP_CLEANUP=1 TEST_FAIL_RULE_DELETE=1 enable_locked 1000 15 && exit 1
[[ -f $(rule_file 1000) && -f $(state_file 1000) && ! -e $test_tmp/stopped ]]
if TEST_FAIL_RULE_DELETE=1 revoke_inactive_grant 1000; then exit 1; else status=$?; fi
(( status == 2 ))
) || fail "failed policy revocation disarmed expiry or claimed inactive status"
pass "failed revocation preserves expiry jobs and returns a distinct error"
(
source "$library"
transaction_setup
current_timer=$(read_state_timer 1000)
expire_locked 1000 omarchy-nopasswd-expire-1000-ffffffffffffffffffffffffffffffff
[[ -f $(rule_file 1000) ]]
expire_locked 1000
[[ -f $(rule_file 1000) ]]
expire_locked 1000 "$current_timer"
[[ ! -e $(rule_file 1000) ]]
) || fail "a predecessor timer invalidates its replacement"
pass "old and legacy timer callbacks preserve a newer valid grant"
(
source "$library"
transaction_setup
start_expiry_timer() {
: >"$REMOVAL_BLOCKER"
return 0
}
enable_locked 1000 15 && exit 1
[[ ! -e $(rule_file 1000) ]]
) || fail "publication ignores a lost package prerequisite"
rm "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
pass "grant publication rechecks package availability after timer setup"
pkgs_path=${OMARCHY_PKGS_PATH:-$ROOT/../omarchy-pkgs}
[[ ! -d $pkgs_path/pkgbuilds ]] || pkgs_path=$pkgs_path/pkgbuilds
package_script="$pkgs_path/omarchy-settings/omarchy-settings.install"
[[ -f $package_script ]] || fail "package checkout is required for shared lifecycle coverage"
sed -e "s|/etc/|$test_tmp/etc/|g" \
-e "s|/run|$test_tmp/run|g" \
-e "s|/usr/bin/stat|$test_tmp/bin/stat|g" \
-e "s|/usr/bin/rm|$test_tmp/bin/rm|g" "$package_script" >"$test_tmp/package.install"
worker="$test_tmp/publisher.sh"
{
printf '#!/bin/bash\nset -euo pipefail\nsource %q\n' "$library"
declare -f transaction_setup
printf 'test_tmp=%q\ntransaction_setup\n' "$test_tmp"
cat <<'WORKER'
publish_rule() {
: >"$test_tmp/publisher.entered"
while [[ ! -e $test_tmp/publisher.release ]]; do sleep 0.02; done
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$(rule_file "$1")"
}
with_root_lock enable_locked 1000 15
WORKER
} >"$worker"
bash "$worker" >"$test_tmp/publisher.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/publisher.entered ]] || break
sleep 0.02
done
[[ -e $test_tmp/publisher.entered ]] || fail "grant publisher did not enter the shared lock"
bash -euo pipefail -c 'source "$1"; : >"$2"; pre_remove; post_remove' bash \
"$test_tmp/package.install" "$test_tmp/removal.started" >"$test_tmp/removal.output" 2>&1 &
children+=("$!")
for ((attempt = 0; attempt < 250; attempt++)); do
[[ ! -e $test_tmp/removal.started ]] || break
sleep 0.02
done
[[ -e $test_tmp/removal.started ]] || fail "package removal did not start"
touch "$test_tmp/publisher.release"
for child in "${children[@]}"; do wait "$child" || fail "shared lifecycle worker failed"; done
children=()
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000 ]] || fail "removal left a concurrently published grant"
[[ -f $test_tmp/run/omarchy-sudo-passwordless-package-removing ]] || fail "removal did not block later publication"
(
source "$library"
transaction_setup
! with_root_lock enable_locked 1000 15
) || fail "a publisher can create a grant after package removal begins"
pass "package removal shares the grant lock and blocks later publication"
printf 'audituser ALL=(ALL) NOPASSWD: ALL\n' >"$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000"
if TEST_FAIL_RULE_DELETE=1 bash -euo pipefail -c 'source "$1"; post_remove' bash "$test_tmp/package.install" >"$test_tmp/removal-failure.output" 2>&1; then
fail "package removal hid a failed policy deletion"
fi
grep -q 'Administrator cleanup is required' "$test_tmp/removal-failure.output" || fail "package deletion failure lacks recovery guidance"
pass "package removal reports cleanup failures instead of successful revocation"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
record=$(read_state_record 1000)
expiry=${record#*$'\t'}
expiry=${expiry%%$'\t'*}
deadline=$(/usr/bin/date -u -d "@$expiry" +%Y%m%d%H%M%SZ)
[[ $(cat "$(rule_file 1000)") == "audituser ALL=(ALL) NOTAFTER=$deadline NOPASSWD: ALL" ]]
/usr/sbin/visudo -cf "$(rule_file 1000)" >/dev/null
classify_generated_rule "$(rule_file 1000)"
rm -f "$(state_file 1000)"
remove_known_legacy_rules
[[ ! -e $(rule_file 1000) ]]
) || fail "native sudo deadline or state-independent bounded rule cleanup is incorrect"
pass "sudo policy contains the same deadline and bounded orphan rules are recognized"
(
source "$library"
transaction_setup
rm -f "$REMOVAL_BLOCKER"
enable_locked 1000 5
if TEST_FAIL_RULE_DELETE=1 package_removing_locked; then exit 1; fi
[[ -f $REMOVAL_BLOCKER && -f $(rule_file 1000) ]]
! enable_locked 1000 5
package_removing_locked
[[ ! -e $(rule_file 1000) ]]
rm -f "$REMOVAL_BLOCKER" "$PACKAGE_HOOK"
! enable_locked 1000 5
) || fail "pre-transaction revocation error or missing hook does not prevent new grants"
pass "package hook fails closed and grants require its installed policy"
+81
View File
@@ -0,0 +1,81 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
test_home="$test_tmp/home"
mock_bin="$test_tmp/bin"
mkdir -p "$mock_bin"
# Run the real theme refresh, renderer and publisher in a throwaway home.
# Only package installation, the T3 CLI and desktop launch are stubbed.
cat >"$mock_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
[[ $1 == "t3code-bin" ]]
SH
cat >"$mock_bin/t3" <<'SH'
#!/bin/bash
[[ $1 == "theme" && $2 == "set" && $3 == "omarchy" && $4 == "--base-dir" && $5 == "$T3CODE_HOME" ]] || exit 1
palette="$T3CODE_HOME/userdata/themes/omarchy.json"
if [[ ! -f $palette ]] || ! jq -e '.name == "Omarchy" and (.accent | test("^#[0-9a-fA-F]{6}$"))' "$palette" >/dev/null; then
echo "No published Omarchy theme"
exit 1
fi
if [[ ${OMARCHY_TEST_T3_FAIL:-0} == "1" ]]; then
echo "T3 theme selection failed"
exit 1
fi
echo "T3 selected Omarchy"
SH
cat >"$mock_bin/setsid" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$mock_bin"/*
run_installer() {
HOME="$test_home" T3CODE_HOME="$test_tmp/custom t3 home" \
OMARCHY_PATH="$ROOT" PATH="$mock_bin:$ROOT/bin:$PATH" \
OMARCHY_THEME_HEADLESS=1 XDG_RUNTIME_DIR="$test_tmp" \
"$ROOT/bin/omarchy-install-ai-t3-code" >"$test_tmp/output" 2>&1
}
state="$test_home/.local/state/omarchy/current"
palette="$test_tmp/custom t3 home/userdata/themes/omarchy.json"
settings="$test_tmp/custom t3 home/userdata/settings.json"
mkdir -p "$state/theme"
echo 'tokyo-night' >"$state/theme.name"
cp "$ROOT/themes/tokyo-night/colors.toml" "$state/theme/colors.toml"
run_installer || fail "installing after an update succeeds" "$(cat "$test_tmp/output")"
[[ -f $state/theme/t3code.json && -f $palette ]] || fail "the missing palette is rendered and published"
cmp -s "$state/theme/t3code.json" "$palette" || fail "T3 receives the current theme's palette"
grep -q 'T3 selected Omarchy' "$test_tmp/output" || fail "the published palette is selected"
grep -q 'Opening T3 Code' "$test_tmp/output" || fail "the themed app opens"
[[ ! -e $test_home/.t3 ]] || fail "the custom T3 home is respected"
pass "an update with no staged T3 palette renders, publishes and selects it before launch"
# Preserve an already rendered palette, including a user-supplied color.
jq '.accent = "#abcdef"' "$state/theme/t3code.json" >"$test_tmp/palette.json"
cp "$test_tmp/palette.json" "$state/theme/t3code.json"
run_installer || fail "installing with an existing palette succeeds" "$(cat "$test_tmp/output")"
cmp -s "$test_tmp/palette.json" "$palette" || fail "an existing palette is published without re-staging"
pass "an existing palette is used without refreshing the current theme"
printf '{"defaultTheme":"dark","keep":"existing settings"}\n' >"$settings"
cp "$settings" "$test_tmp/settings-before.json"
if OMARCHY_TEST_T3_FAIL=1 run_installer; then
fail "a failed T3 theme selection fails the installer"
fi
grep -q 'T3 theme selection failed' "$test_tmp/output" || fail "the CLI failure is visible"
if grep -Eq 'Opening T3 Code|T3 Code has been installed' "$test_tmp/output"; then
fail "a failed selection does not report successful setup"
fi
cmp -s "$test_tmp/settings-before.json" "$settings" || fail "a failed selection does not rewrite settings"
pass "a CLI failure remains visible and leaves the existing settings intact"
+1 -1
View File
@@ -207,7 +207,7 @@ pass "a theme name cannot climb out of the theme directories"
# generates. Every generated theme file is either denied to an installed theme or
# recorded here as carrying colour, so a new template fails until it is placed.
denied=(alacritty.toml foot.ini ghostty.conf kitty.conf gum_env.lua hyprland.lua neovim.lua vscode.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml vscode-theme.json)
colour_only=(btop.theme chromium.theme claude.json helix.toml hermes.yaml hyprland-preview-share-picker.css keyboard.rgb obsidian.css pi.json shell.toml t3code.json vscode-theme.json)
for tpl in "$ROOT"/default/themed/*.tpl; do
generated=$(basename "$tpl" .tpl)
+33
View File
@@ -51,3 +51,36 @@ pass "bar on is idempotent"
HOME="$test_home" omarchy-toggle-bar off
[[ ! -f $bar_flag ]] || fail "bar off disables bar-off toggle"
pass "bar off disables bar-off toggle"
# The gaps half of full screen copies a flag file in and reloads Hyprland, so
# give it this checkout to copy from and a hyprctl that answers without a
# compositor.
export OMARCHY_PATH="$ROOT"
stub_bin="$TMPDIR/bin"
mkdir -p "$stub_bin"
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/hyprctl"
chmod +x "$stub_bin/hyprctl"
export PATH="$stub_bin:$PATH"
gaps_flag="$test_home/.local/state/omarchy/toggles/hypr/window-no-gaps.lua"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle hides the bar and the gaps together"
pass "fullscreen toggle hides the bar and the gaps together"
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen toggle restores the bar and the gaps together"
pass "fullscreen toggle restores the bar and the gaps together"
HOME="$test_home" omarchy-toggle-bar on
HOME="$test_home" omarchy-toggle-fullscreen-desktop
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen toggle pulls a half-hidden desktop into full screen"
pass "fullscreen toggle pulls a half-hidden desktop into full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop off
[[ ! -f $bar_flag && ! -f $gaps_flag ]] || fail "fullscreen off leaves full screen"
pass "fullscreen off leaves full screen"
HOME="$test_home" omarchy-toggle-fullscreen-desktop on
[[ -f $bar_flag && -f $gaps_flag ]] || fail "fullscreen on enters full screen"
pass "fullscreen on enters full screen"