Merge remote-tracking branch 'origin/quattro' into fix-snapshot-locate-and-timeline-leak
# Conflicts: # docs/file-layout.md
This commit is contained in:
@@ -16,7 +16,7 @@ wait_until "omarchy-shell responds to ping" 60 omarchy-shell shell ping
|
||||
plugins=$(omarchy-shell shell listPlugins)
|
||||
for plugin in \
|
||||
omarchy.audio omarchy.background omarchy.bar omarchy.bluetooth \
|
||||
omarchy.clipboard omarchy.emojis omarchy.launcher omarchy.menu \
|
||||
omarchy.clipboard omarchy.emojis omarchy.menu \
|
||||
omarchy.monitor omarchy.network omarchy.notifications omarchy.power \
|
||||
omarchy.reminders omarchy.weather; do
|
||||
[[ $plugins == *"$plugin"* ]] || fail "shell plugin is loaded: $plugin" "loaded plugins: $plugins"
|
||||
|
||||
@@ -94,23 +94,24 @@ screenshot "success-notification-popup"
|
||||
omarchy-shell notifications dismissAll >/dev/null
|
||||
wait_until "notification popup closes" 15 layer_absent "omarchy-notifications"
|
||||
|
||||
# The launcher does the full loop: open, search by typing, launch the top hit.
|
||||
# The menu's Apps submenu does the full launcher loop: open, search by
|
||||
# typing, launch the top hit.
|
||||
if window_present "(?i)omawrite" >/dev/null 2>&1; then
|
||||
fail "launcher test starts with no Omawrite window" "an Omawrite window is already open"
|
||||
fail "app launch test starts with no Omawrite window" "an Omawrite window is already open"
|
||||
fi
|
||||
|
||||
omarchy-shell shell summon omarchy.launcher >/dev/null
|
||||
wait_until "launcher opens" 15 layer_present "omarchy-launcher"
|
||||
omarchy-menu summon apps >/dev/null
|
||||
wait_until "apps menu opens" 15 layer_present "omarchy-menu"
|
||||
sleep 1
|
||||
screenshot "success-launcher-open"
|
||||
screenshot "success-apps-menu-open"
|
||||
|
||||
wtype "omawrite"
|
||||
sleep 1
|
||||
screenshot "success-launcher-search"
|
||||
screenshot "success-apps-menu-search"
|
||||
wtype -k Return
|
||||
|
||||
wait_until "launcher launches the top search hit" 60 window_present "(?i)omawrite"
|
||||
wait_until "launcher closes after launching" 15 layer_absent "omarchy-launcher"
|
||||
wait_until "apps menu launches the top search hit" 60 window_present "(?i)omawrite"
|
||||
wait_until "apps menu closes after launching" 15 layer_absent "omarchy-menu"
|
||||
|
||||
close_windows "(?i)omawrite"
|
||||
wait_until "Omawrite window closes" 30 window_absent "(?i)omawrite"
|
||||
|
||||
@@ -423,6 +423,28 @@ for nvim_legacy_target in \
|
||||
done
|
||||
pass "nvim theme migration relinks current theme"
|
||||
|
||||
for nvim_legacy_target in \
|
||||
"../../../omarchy/current/theme/neovim.lua" \
|
||||
"../../../../.config/omarchy/current/theme/neovim.lua" \
|
||||
"~/.config/omarchy/current/theme/neovim.lua" \
|
||||
"$NVIM_MIGRATION_TMPDIR/.config/omarchy/current/theme/neovim.lua"; do
|
||||
ln -sfn "$nvim_legacy_target" "$nvim_theme_link"
|
||||
HOME="$NVIM_MIGRATION_TMPDIR" bash -euo pipefail "$ROOT/migrations/1785002349.sh" >/dev/null
|
||||
nvim_actual_target=$(readlink "$nvim_theme_link")
|
||||
nvim_resolved_target=$(readlink -f "$nvim_theme_link")
|
||||
[[ $nvim_actual_target == $nvim_expected_target ]] || fail "nvim theme repair migration updates $nvim_legacy_target"
|
||||
[[ $nvim_resolved_target == $nvim_expected_resolved_target ]] || fail "nvim theme repair migration points to current theme"
|
||||
done
|
||||
|
||||
HOME="$NVIM_MIGRATION_TMPDIR" bash -euo pipefail "$ROOT/migrations/1785002349.sh" >/dev/null
|
||||
[[ $(readlink "$nvim_theme_link") == $nvim_expected_target ]] || fail "nvim theme repair migration is idempotent"
|
||||
|
||||
touch "$NVIM_MIGRATION_TMPDIR/custom-theme.lua"
|
||||
ln -sfn "../../../../custom-theme.lua" "$nvim_theme_link"
|
||||
HOME="$NVIM_MIGRATION_TMPDIR" bash -euo pipefail "$ROOT/migrations/1785002349.sh" >/dev/null
|
||||
[[ $(readlink "$nvim_theme_link") == "../../../../custom-theme.lua" ]] || fail "nvim theme repair migration leaves custom symlinks alone"
|
||||
pass "nvim theme repair migration relinks every legacy spelling"
|
||||
|
||||
HYPR_MIGRATION_TMPDIR=$(mktemp -d)
|
||||
mkdir -p "$HYPR_MIGRATION_TMPDIR/.config/hypr"
|
||||
cat >"$HYPR_MIGRATION_TMPDIR/.config/hypr/hyprland.lua" <<'LUA'
|
||||
|
||||
@@ -0,0 +1,130 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const search = requireFromRoot('shell/services/AppSearch.js')
|
||||
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||
const appLibraryQml = fs.readFileSync(path.join(root, 'shell/services/AppLibrary.qml'), 'utf8')
|
||||
|
||||
const entries = [
|
||||
{
|
||||
name: 'Google Contacts',
|
||||
genericName: 'Address Book',
|
||||
comment: 'Manage contacts',
|
||||
keywords: ['contacts', 'address book', 'people'],
|
||||
id: 'google-contacts.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Calculator',
|
||||
genericName: 'Calculator',
|
||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||
id: 'org.gnome.Calculator.desktop'
|
||||
},
|
||||
{
|
||||
name: 'OBS Studio',
|
||||
genericName: 'Streaming/Recording Software',
|
||||
comment: 'Free and Open Source Streaming/Recording Software',
|
||||
keywords: ['streaming', 'recording', 'capture'],
|
||||
id: 'com.obsproject.Studio.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Aether',
|
||||
genericName: '',
|
||||
comment: 'Minimal internet radio player',
|
||||
keywords: ['audio', 'music', 'radio'],
|
||||
id: 'io.github.taqi.aether.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Xournal++',
|
||||
genericName: 'Notetaking',
|
||||
comment: 'Take handwritten notes',
|
||||
keywords: ['notes', 'pdf', 'annotation'],
|
||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||
},
|
||||
{
|
||||
name: 'RustDesk',
|
||||
genericName: 'Remote Desktop',
|
||||
comment: 'Remote desktop control',
|
||||
keywords: ['remote', 'desktop', 'control'],
|
||||
id: 'com.rustdesk.RustDesk.desktop'
|
||||
}
|
||||
]
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
assert(
|
||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||
'calculator does not match contact as a loose subsequence'
|
||||
)
|
||||
|
||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
||||
|
||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
||||
|
||||
// The menu's Apps submenu is the launcher now: app rows launch and uninstall
|
||||
// through the shared app library instead of running commands themselves.
|
||||
const activateMatch = menuQml.match(/function activateIndex\(index, fromPointer\) \{([\s\S]*?)\n \}/)
|
||||
assert(activateMatch, 'menu activateIndex function exists')
|
||||
assert(
|
||||
activateMatch[1].includes('root.appLibrary.launch('),
|
||||
'menu routes app launch through the shared app library'
|
||||
)
|
||||
assert(
|
||||
!activateMatch[1].includes('entry.execute()'),
|
||||
'menu does not execute desktop entries directly'
|
||||
)
|
||||
|
||||
const confirmDeleteMatch = menuQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
||||
assert(confirmDeleteMatch, 'menu confirmDelete function exists')
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.appLibrary.remove('),
|
||||
'menu delete routes through the shared app library'
|
||||
)
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.cancel()'),
|
||||
'menu delete closes the menu after confirmation'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function remove\(desktopId, name\) \{[\s\S]*?omarchy-remove-launcher-entry[\s\S]*?\n \}/.test(appLibraryQml),
|
||||
'app library remove runs the remover through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function launch\(desktopId, name\) \{[\s\S]*?gtk-launch[\s\S]*?\n \}/.test(appLibraryQml) &&
|
||||
appLibraryQml.includes('Util.execDetached("gtk-launch "'),
|
||||
'app library runs desktop entry launch through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(appLibraryQml),
|
||||
'app library fallback icon index includes device icons'
|
||||
)
|
||||
|
||||
assert(
|
||||
/if \(active === "apps"\) \{[\s\S]*?rows\.sort\(function\(a, b\)/.test(menuQml),
|
||||
'apps menu enforces alphabetical display order after provider refreshes'
|
||||
)
|
||||
|
||||
const iconSourceMatch = appLibraryQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
||||
assert(iconSourceMatch, 'app library iconSource function exists')
|
||||
assert(
|
||||
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
||||
'app library prefers indexed app icons over ambiguous themed icons'
|
||||
)
|
||||
|
||||
const openMatch = menuQml.match(/function openExistingMenu\(initialMenu\) \{([\s\S]*?)\n \}/)
|
||||
assert(openMatch, 'menu openExistingMenu function exists')
|
||||
assert(
|
||||
openMatch[1].includes('root.appLibrary.refreshIcons()'),
|
||||
'menu refreshes the shared icon index when opened'
|
||||
)
|
||||
JS
|
||||
@@ -12,7 +12,9 @@ cleanup() {
|
||||
kill "$QS_PID" 2>/dev/null || true
|
||||
wait "$QS_PID" 2>/dev/null || true
|
||||
fi
|
||||
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
|
||||
rm -rf "$TMPDIR"
|
||||
fi
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
|
||||
@@ -37,9 +37,16 @@ for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
write_stub omarchy-state '#!/bin/bash
|
||||
printf "state" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
write_stub omarchy-update '#!/bin/bash
|
||||
printf "update" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\tOMARCHY_PATH=%s" "$OMARCHY_PATH" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
'
|
||||
|
||||
@@ -47,22 +54,9 @@ write_stub gum '#!/bin/bash
|
||||
printf "gum" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
printf "\n" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
if [[ $1 == "input" ]]; then
|
||||
printf "%s\n" "${OMARCHY_TEST_GUM_INPUT:-$HOME/Work/omarchy}"
|
||||
fi
|
||||
exit 0
|
||||
'
|
||||
|
||||
write_stub omarchy-cmd-missing '#!/bin/bash
|
||||
[[ $1 == "git" ]] && exit 1
|
||||
exit 0
|
||||
'
|
||||
|
||||
write_stub omarchy-cmd-present '#!/bin/bash
|
||||
[[ $1 == "omarchy-dev-unlink" || $1 == "gum" || $1 == "git" ]] && exit 0
|
||||
exit 1
|
||||
'
|
||||
|
||||
write_stub git '#!/bin/bash
|
||||
printf "git" >>"$OMARCHY_CHANNEL_TEST_LOG"
|
||||
for arg in "$@"; do printf "\t%s" "$arg" >>"$OMARCHY_CHANNEL_TEST_LOG"; done
|
||||
@@ -96,9 +90,7 @@ esac
|
||||
run_channel() {
|
||||
: >"$log_file"
|
||||
OMARCHY_CHANNEL_TEST_LOG="$log_file" \
|
||||
OMARCHY_DEV_PATH="${OMARCHY_DEV_PATH:-}" \
|
||||
OMARCHY_TEST_GUM_INPUT="${OMARCHY_TEST_GUM_INPUT:-}" \
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_PATH="${OMARCHY_TEST_PATH:-/usr/share/omarchy}" \
|
||||
HOME="$test_tmp/home" \
|
||||
PATH="$stub_bin:$ROOT/bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-channel-set" "$@"
|
||||
@@ -112,49 +104,35 @@ assert_log_line() {
|
||||
pass "$description"
|
||||
}
|
||||
|
||||
assert_numbered_log_line() {
|
||||
local number="$1"
|
||||
local expected="$2"
|
||||
local description="$3"
|
||||
local actual=""
|
||||
|
||||
actual=$(sed -n "${number}p" "$log_file")
|
||||
[[ $actual == $expected ]] || fail "$description" "$(cat "$log_file")"
|
||||
pass "$description"
|
||||
}
|
||||
|
||||
run_channel stable
|
||||
assert_log_line $'refresh\tstable' "stable refreshes the stable pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "stable installs stable Omarchy packages"
|
||||
assert_log_line 'unlink' "stable restores the package-backed Omarchy path"
|
||||
assert_log_line $'update\t-y' "stable runs the normal update pipeline"
|
||||
assert_log_line $'unlink\t--no-reboot' "stable restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "stable runs the normal update pipeline from the package-backed path"
|
||||
if grep -q $'^state\tset\treboot-required$' "$log_file"; then
|
||||
fail "stable does not require reboot when already package-backed" "$(cat "$log_file")"
|
||||
fi
|
||||
pass "stable does not require reboot when already package-backed"
|
||||
|
||||
run_channel rc
|
||||
assert_log_line $'refresh\trc' "rc refreshes the rc pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy\tomarchy-settings' "rc installs rc Omarchy packages"
|
||||
assert_log_line 'unlink' "rc restores the package-backed Omarchy path"
|
||||
assert_log_line $'update\t-y' "rc runs the normal update pipeline"
|
||||
assert_log_line $'unlink\t--no-reboot' "rc restores the package-backed Omarchy path without an early reboot prompt"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "rc runs the normal update pipeline from the package-backed path"
|
||||
|
||||
run_channel edge
|
||||
OMARCHY_TEST_PATH="$ROOT" run_channel edge
|
||||
assert_log_line $'refresh\tedge' "edge refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "edge installs development Omarchy packages"
|
||||
assert_log_line 'unlink' "edge remains package-backed"
|
||||
assert_log_line $'update\t-y' "edge runs the normal update pipeline"
|
||||
assert_log_line $'unlink\t--no-reboot' "edge unlinks dev without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "edge marks reboot required when leaving dev"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH=/usr/share/omarchy' "edge runs the normal update pipeline from the package-backed path"
|
||||
[[ $(grep -E '^(unlink|state|update)' "$log_file") == $'unlink\t--no-reboot\nstate\tset\treboot-required\nupdate\t-y\tOMARCHY_PATH=/usr/share/omarchy' ]] ||
|
||||
fail "edge defers the reboot prompt until the update restart stage" "$(cat "$log_file")"
|
||||
pass "edge defers the reboot prompt until the update restart stage"
|
||||
|
||||
checkout="$test_tmp/dev-checkout"
|
||||
default_checkout="$test_tmp/home/Work/omarchy"
|
||||
OMARCHY_TEST_GUM_INPUT="$checkout" run_channel dev
|
||||
assert_numbered_log_line 1 $'gum\tconfirm\t--default=false\tEnable Dev anyway?' "dev warns before changing packages"
|
||||
assert_numbered_log_line 2 $'gum\tinput\t--value\t'"$default_checkout"$'\t--placeholder\t'"$default_checkout"$'\t--header\tWhere should Dev checkout live? Existing non-checkout paths will not be overwritten.' "dev prompts for the checkout path before changing packages"
|
||||
assert_log_line $'gum\tconfirm\t--default=false\tEnable Dev anyway?' "dev asks for confirmation"
|
||||
assert_log_line $'refresh\tedge' "dev refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'git\tclone\t--branch\tquattro\t--single-branch\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the quattro checkout"
|
||||
assert_log_line $'link\t'"$checkout" "dev links the source checkout"
|
||||
|
||||
occupied_checkout="$test_tmp/occupied"
|
||||
mkdir -p "$occupied_checkout"
|
||||
if OMARCHY_TEST_GUM_INPUT="$occupied_checkout" run_channel dev >"$test_tmp/occupied.out" 2>"$test_tmp/occupied.err"; then
|
||||
checkout="$test_tmp/home/omarchy"
|
||||
mkdir -p "$checkout"
|
||||
if run_channel dev >"$test_tmp/occupied.out" 2>"$test_tmp/occupied.err"; then
|
||||
fail "dev refuses to use an occupied non-checkout path"
|
||||
fi
|
||||
|
||||
@@ -164,6 +142,30 @@ if grep -Fx $'refresh\tedge' "$log_file" >/dev/null; then
|
||||
fi
|
||||
pass "dev refuses occupied non-checkout paths before package changes"
|
||||
|
||||
rmdir "$checkout"
|
||||
run_channel dev
|
||||
assert_log_line $'gum\tconfirm\t--default=false\tSwitch to dev channel?' "dev asks for confirmation"
|
||||
assert_log_line $'refresh\tedge' "dev refreshes the edge pacman channel"
|
||||
assert_log_line $'sudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev' "dev installs development Omarchy packages"
|
||||
assert_log_line $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout" "dev clones the source checkout to ~/omarchy"
|
||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "dev links ~/omarchy without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "dev defers the reboot prompt to the update pipeline"
|
||||
assert_log_line $'update\t-y\tOMARCHY_PATH='"$checkout" "dev runs the normal update pipeline from the source checkout"
|
||||
[[ $(grep -E '^(git|link|state|refresh|sudo|update)' "$log_file") == $'git\tclone\thttps://github.com/basecamp/omarchy.git\t'"$checkout"$'\nlink\t'"$checkout"$'\t--no-reboot\nstate\tset\treboot-required\nrefresh\tedge\nsudo\tenv\tOMARCHY_UPDATE_PACMAN=1\tpacman\t-S\t--needed\t--noconfirm\t--ask\t4\tomarchy-dev\tomarchy-settings-dev\nupdate\t-y\tOMARCHY_PATH='"$checkout" ]] ||
|
||||
fail "dev activates the checkout before changing or updating packages" "$(cat "$log_file")"
|
||||
pass "dev activates the checkout before changing or updating packages"
|
||||
|
||||
OMARCHY_TEST_PATH="$checkout" run_channel stable
|
||||
assert_log_line $'unlink\t--no-reboot' "switching from dev to stable unlinks without an early reboot prompt"
|
||||
assert_log_line $'state\tset\treboot-required' "switching from dev to stable marks reboot required"
|
||||
|
||||
run_channel dev
|
||||
if grep -q $'^git\tclone\t' "$log_file"; then
|
||||
fail "dev reuses an existing checkout" "$(cat "$log_file")"
|
||||
fi
|
||||
assert_log_line $'link\t'"$checkout"$'\t--no-reboot' "switching back to dev links ~/omarchy"
|
||||
pass "switching back to dev reuses the existing ~/omarchy checkout"
|
||||
|
||||
current_channel() {
|
||||
OMARCHY_TEST_VERSION_CHANNEL="$1" \
|
||||
OMARCHY_TEST_PACKAGES="$2" \
|
||||
@@ -182,4 +184,4 @@ pass "current channel detects rc"
|
||||
pass "current channel detects package-backed edge"
|
||||
|
||||
[[ $(current_channel edge dev "$test_tmp/dev-checkout") == "dev" ]] || fail "current channel detects dev from OMARCHY_PATH"
|
||||
pass "current channel detects dev from OMARCHY_PATH"
|
||||
pass "current channel honors a dev link outside ~/omarchy"
|
||||
|
||||
@@ -94,12 +94,29 @@ import sys
|
||||
from pathlib import Path
|
||||
|
||||
root = Path(os.environ["ROOT"])
|
||||
home = Path.home()
|
||||
pkgs_candidates = [
|
||||
root.parent / "omarchy-pkgs/pkgbuilds",
|
||||
root.parent / "omarchy/omarchy-pkgs/pkgbuilds",
|
||||
root.parent.parent / "omarchy-pkgs/pkgbuilds",
|
||||
root.parent / "omacom/omarchy-pkgs/pkgbuilds",
|
||||
root.parent.parent / "omacom/omarchy-pkgs/pkgbuilds",
|
||||
home / "Work/omacom/omarchy-pkgs/pkgbuilds",
|
||||
]
|
||||
pkgs_root = next((path for path in pkgs_candidates if path.exists()), pkgs_candidates[0])
|
||||
# Checkouts differ per machine, so allow an explicit pointer at the sibling repo.
|
||||
# Accepts either the omarchy-pkgs checkout or its pkgbuilds/ directory.
|
||||
override = os.environ.get("OMARCHY_PKGS_PATH")
|
||||
if override:
|
||||
pkgs_candidates = [Path(override) / "pkgbuilds", Path(override)] + pkgs_candidates
|
||||
pkgs_root = next((path for path in pkgs_candidates if path.exists()), None)
|
||||
if pkgs_root is None:
|
||||
print("not ok - omarchy-pkgs checkout found for PKGBUILD coverage", file=sys.stderr)
|
||||
print(
|
||||
"looked in:\n " + "\n ".join(str(path) for path in pkgs_candidates) +
|
||||
"\nset OMARCHY_PKGS_PATH to the omarchy-pkgs checkout",
|
||||
file=sys.stderr,
|
||||
)
|
||||
sys.exit(1)
|
||||
settings_pkgbuild_path = pkgs_root / "omarchy-settings/PKGBUILD"
|
||||
omarchy_pkgbuild_path = pkgs_root / "omarchy/PKGBUILD"
|
||||
if not settings_pkgbuild_path.exists():
|
||||
@@ -120,8 +137,8 @@ package_defaults = [
|
||||
("default/systemd/user/bt-agent.service", "/usr/lib/systemd/user/bt-agent.service", "systemd/user/bt-agent.service"),
|
||||
("default/systemd/user/omarchy-sleep-lock.service", "/usr/lib/systemd/user/omarchy-sleep-lock.service", "systemd/user/omarchy-sleep-lock.service"),
|
||||
("default/systemd/user/omarchy-recover-internal-monitor.service", "/usr/lib/systemd/user/omarchy-recover-internal-monitor.service", "systemd/user/omarchy-recover-internal-monitor.service"),
|
||||
("default/systemd/user/omarchy-update-user-notify.service", "/usr/lib/systemd/user/omarchy-update-user-notify.service", "systemd/user/omarchy-update-user-notify.service"),
|
||||
("default/systemd/user/omarchy-update-user-notify.path", "/usr/lib/systemd/user/omarchy-update-user-notify.path", "systemd/user/omarchy-update-user-notify.path"),
|
||||
("default/systemd/user/omarchy-migrate-notify.service", "/usr/lib/systemd/user/omarchy-migrate-notify.service", "systemd/user/omarchy-migrate-notify.service"),
|
||||
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
|
||||
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
|
||||
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
|
||||
]
|
||||
@@ -134,6 +151,16 @@ for source, destination, legacy in package_defaults:
|
||||
if destination and (source not in pkgbuild or destination not in pkgbuild):
|
||||
errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}")
|
||||
|
||||
# Existing users have an absolute wants symlink to the old unit path, and the
|
||||
# migration that repoints it only runs for users who run an update -- the
|
||||
# opposite of who the notifier is for. Dropping this alias strands them.
|
||||
notify_alias = 'ln -sfn omarchy-migrate-notify.service "$pkgdir/usr/lib/systemd/user/omarchy-update-user-notify.service"'
|
||||
if notify_alias not in pkgbuild:
|
||||
errors.append(
|
||||
"PKGBUILD does not ship the omarchy-update-user-notify.service compatibility "
|
||||
"alias, so users who have not run migration 1785095882 lose the login notifier"
|
||||
)
|
||||
|
||||
alpm_hooks = [
|
||||
"00-omarchy-update-guard.hook",
|
||||
"10-omarchy-hyprland-reload-pause.hook",
|
||||
@@ -249,6 +276,27 @@ jq -e '
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config moves existing widgets without duplicates"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window right
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces"]) and
|
||||
(.bar.layout.right | ids == ["omarchy.tray", "omarchy.active-window", "omarchy.microphone", "omarchy.tailscale", "omarchy.bluetooth"])
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "bar plugin move accepts a positional target section"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window left
|
||||
jq -e '
|
||||
def ids: map(.id // .);
|
||||
(.bar.layout.left | ids == ["omarchy.menu", "omarchy.workspaces", "omarchy.active-window"]) and
|
||||
(.bar.layout.right | ids == ["omarchy.tray", "omarchy.microphone", "omarchy.tailscale", "omarchy.bluetooth"])
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "bar plugin move can restore a widget with positional syntax"
|
||||
|
||||
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin move omarchy.active-window left --section right 2>/dev/null; then
|
||||
fail "bar plugin move accepted positional and flagged target sections"
|
||||
fi
|
||||
pass "bar plugin move rejects conflicting target section syntax"
|
||||
|
||||
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin add local.nonexistent-widget 2>/dev/null; then
|
||||
fail "bar plugin add accepted an unknown widget"
|
||||
fi
|
||||
@@ -289,6 +337,28 @@ jq -e '
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "shell config removes widgets with remove alias"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth enabled false --json
|
||||
jq -e '
|
||||
any(.bar.layout.right[]; .id == "omarchy.bluetooth" and .enabled == false)
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "bar plugin set accepts false JSON values"
|
||||
|
||||
HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth optional null --json
|
||||
jq -e '
|
||||
any(.bar.layout.right[]; .id == "omarchy.bluetooth" and has("optional") and .optional == null)
|
||||
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
|
||||
pass "bar plugin set accepts null JSON values"
|
||||
|
||||
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth broken '{' --json 2>/dev/null; then
|
||||
fail "bar plugin set accepted malformed JSON"
|
||||
fi
|
||||
pass "bar plugin set rejects malformed JSON"
|
||||
|
||||
if HOME="$TMPDIR/home" OMARCHY_PATH="$ROOT" omarchy-bar-plugin set omarchy.bluetooth broken 'false null' --json 2>/dev/null; then
|
||||
fail "bar plugin set accepted multiple JSON values"
|
||||
fi
|
||||
pass "bar plugin set rejects multiple JSON values"
|
||||
|
||||
mock_bin="$TMPDIR/mock-bin"
|
||||
mkdir -p "$mock_bin"
|
||||
|
||||
|
||||
@@ -0,0 +1,82 @@
|
||||
#!/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
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
log_file="$test_tmp/dev-unlink.log"
|
||||
conf_file="$test_tmp/omarchy.conf"
|
||||
mkdir -p "$stub_bin" "$test_tmp/home"
|
||||
|
||||
cat >"$stub_bin/sudo" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'sudo' >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
for arg in "$@"; do
|
||||
printf '\t%s' "$arg" >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
done
|
||||
printf '\n' >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
|
||||
if [[ $1 == "tee" ]]; then
|
||||
cat >"$OMARCHY_DEV_UNLINK_TEST_CONF"
|
||||
fi
|
||||
SH
|
||||
chmod +x "$stub_bin/sudo"
|
||||
|
||||
cat >"$stub_bin/gum" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'gum' >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
for arg in "$@"; do
|
||||
printf '\t%s' "$arg" >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
done
|
||||
printf '\n' >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/gum"
|
||||
|
||||
cat >"$stub_bin/omarchy-system-reboot" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'reboot\n' >>"$OMARCHY_DEV_UNLINK_TEST_LOG"
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-system-reboot"
|
||||
|
||||
run_unlink() {
|
||||
HOME="$test_tmp/home" \
|
||||
OMARCHY_DEV_UNLINK_TEST_LOG="$log_file" \
|
||||
OMARCHY_DEV_UNLINK_TEST_CONF="$conf_file" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-dev-unlink" "$@"
|
||||
}
|
||||
|
||||
: >"$log_file"
|
||||
run_unlink --no-reboot
|
||||
|
||||
grep -Fx $'sudo\ttee\t/etc/omarchy.conf' "$log_file" >/dev/null ||
|
||||
fail "dev unlink writes the package path without rebooting" "$(cat "$log_file")"
|
||||
[[ $(<"$conf_file") == 'export OMARCHY_PATH="/usr/share/omarchy"' ]] ||
|
||||
fail "dev unlink writes the package path guard" "$(<"$conf_file")"
|
||||
if grep -Eq '^(gum|reboot)' "$log_file"; then
|
||||
fail "dev unlink --no-reboot skips the reboot prompt" "$(cat "$log_file")"
|
||||
fi
|
||||
pass "dev unlink --no-reboot skips the reboot prompt"
|
||||
|
||||
: >"$log_file"
|
||||
run_unlink
|
||||
|
||||
grep -Fx $'gum\tconfirm\tReboot now to activate?' "$log_file" >/dev/null ||
|
||||
fail "interactive dev unlink still prompts for reboot" "$(cat "$log_file")"
|
||||
grep -Fx 'reboot' "$log_file" >/dev/null ||
|
||||
fail "interactive dev unlink still reboots after confirmation" "$(cat "$log_file")"
|
||||
pass "interactive dev unlink keeps its reboot prompt"
|
||||
|
||||
if run_unlink --invalid >"$test_tmp/invalid.out" 2>"$test_tmp/invalid.err"; then
|
||||
fail "dev unlink rejects unknown arguments"
|
||||
fi
|
||||
grep -F 'Usage: omarchy dev unlink [--no-reboot]' "$test_tmp/invalid.err" >/dev/null ||
|
||||
fail "dev unlink explains valid arguments" "$(cat "$test_tmp/invalid.err")"
|
||||
pass "dev unlink rejects unknown arguments"
|
||||
@@ -0,0 +1,86 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
# The hook guards on the real /etc/pam.d path, which can't be mocked via PATH.
|
||||
if [[ -f /etc/pam.d/omarchy-lock-fingerprint ]]; then
|
||||
pass "fingerprint invitation test skipped: host already has fingerprint auth configured"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
test_home=$(mktemp -d)
|
||||
test_bin=$(mktemp -d)
|
||||
log_file=$(mktemp)
|
||||
hw_marker=$(mktemp -u)
|
||||
hook_path="$test_home/.config/omarchy/hooks/post-update.d/setup-fingerprint.hook"
|
||||
|
||||
cleanup() {
|
||||
rm -rf "$test_home" "$test_bin"
|
||||
rm -f "$log_file" "$hw_marker"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
mkdir -p "$(dirname "$hook_path")"
|
||||
|
||||
cat >"$test_bin/omarchy-hw-fingerprint" <<'EOF'
|
||||
#!/bin/bash
|
||||
[[ -f $TEST_HW_MARKER ]]
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-hw-fingerprint"
|
||||
|
||||
cat >"$test_bin/omarchy-notification-send" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo notification >>"$TEST_LOG"
|
||||
echo action
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-notification-send"
|
||||
|
||||
cat >"$test_bin/omarchy-launch-floating-terminal-with-presentation" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo launch >>"$TEST_LOG"
|
||||
EOF
|
||||
chmod +x "$test_bin/omarchy-launch-floating-terminal-with-presentation"
|
||||
|
||||
cat >"$test_bin/systemd-run" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "systemd-run:$*" >>"$TEST_LOG"
|
||||
while (($# > 0)); do
|
||||
case $1 in
|
||||
-p) shift 2 ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
exec "$@"
|
||||
EOF
|
||||
chmod +x "$test_bin/systemd-run"
|
||||
|
||||
run_invitation_hook() {
|
||||
cp "$ROOT/install/user/first-run/setup-fingerprint.hook" "$hook_path"
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" TEST_HW_MARKER="$hw_marker" bash "$hook_path"
|
||||
}
|
||||
|
||||
run_invitation_hook
|
||||
|
||||
[[ ! -f $test_home/.local/state/omarchy/done/fingerprint-setup-invitation ]] || fail "fingerprint invitation stays pending without a reader"
|
||||
[[ ! -s $log_file ]] || fail "fingerprint invitation does nothing without a reader"
|
||||
|
||||
touch "$hw_marker"
|
||||
run_invitation_hook
|
||||
|
||||
[[ -f $test_home/.local/state/omarchy/done/fingerprint-setup-invitation ]] || fail "fingerprint invitation records completion"
|
||||
[[ -f $hook_path ]] || fail "fingerprint invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "fingerprint invitation uses durable user services"
|
||||
grep -q -- '--user --collect --quiet --service-type=exec --unit=omarchy-fingerprint-setup-invitation' "$log_file" || fail "fingerprint invitation configures its user service"
|
||||
# KillMode=process keeps the launcher's setsid child alive once the short-lived
|
||||
# main process exits, otherwise the setup terminal never appears.
|
||||
grep -q -- '--user --collect --quiet -p KillMode=process --unit=omarchy-setup-security-fingerprint ' "$log_file" || fail "fingerprint invitation outlives its launcher unit"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "fingerprint invitation sends one notification"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "fingerprint invitation handles the notification action"
|
||||
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" TEST_HW_MARKER="$hw_marker" bash "$hook_path"
|
||||
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "completed fingerprint invitation does not schedule again"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "completed fingerprint invitation hook does not notify again"
|
||||
|
||||
pass "fingerprint invitation waits for a reader and only runs once"
|
||||
@@ -110,24 +110,13 @@ ShellRoot {
|
||||
|
||||
Item { id: host }
|
||||
|
||||
QtObject {
|
||||
id: mockNotificationService
|
||||
property bool doNotDisturb: false
|
||||
property ListModel pendingModel: ListModel {}
|
||||
property ListModel pastModel: ListModel {}
|
||||
function setDoNotDisturb(value) { doNotDisturb = !!value }
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockShell
|
||||
property var bar: fakeBar
|
||||
property var barConfig: ({ position: "top" })
|
||||
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
||||
function firstPartyServiceFor(id) {
|
||||
if (id === "omarchy.notifications") return mockNotificationService
|
||||
return null
|
||||
}
|
||||
function serviceFor(id) { return firstPartyServiceFor(id) }
|
||||
function firstPartyServiceFor(id) { return null }
|
||||
function serviceFor(id) { return null }
|
||||
function summon(id, payloadJson) { return true }
|
||||
function hide(id) { return true }
|
||||
function toggle(id, payloadJson) { return true }
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
import QtQuick
|
||||
import Quickshell
|
||||
import qs.Commons
|
||||
|
||||
ShellRoot {
|
||||
id: root
|
||||
|
||||
readonly property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
|
||||
readonly property string rootPath: Quickshell.env("OMARCHY_PATH")
|
||||
property var failures: []
|
||||
|
||||
function fail(message) {
|
||||
failures.push(String(message))
|
||||
}
|
||||
|
||||
function assertTrue(condition, message) {
|
||||
if (!condition) fail(message)
|
||||
}
|
||||
|
||||
function shellQuote(value) {
|
||||
return "'" + String(value).replace(/'/g, "'\\''") + "'"
|
||||
}
|
||||
|
||||
function writeResult() {
|
||||
var payload = JSON.stringify({
|
||||
ok: failures.length === 0,
|
||||
failures: failures
|
||||
})
|
||||
|
||||
if (resultPath) {
|
||||
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
|
||||
}
|
||||
}
|
||||
|
||||
Item { id: host; width: 800; height: 600 }
|
||||
|
||||
TextMetrics {
|
||||
id: probe
|
||||
font.family: Style.font.family
|
||||
}
|
||||
|
||||
Timer {
|
||||
interval: 1
|
||||
running: true
|
||||
repeat: false
|
||||
onTriggered: {
|
||||
try {
|
||||
var component = Qt.createComponent("file://" + root.rootPath + "/shell/plugins/lock/LockView.qml", Component.PreferSynchronous)
|
||||
if (component.status !== Component.Ready) {
|
||||
root.fail("LockView failed to load: " + component.errorString())
|
||||
return
|
||||
}
|
||||
|
||||
var view = component.createObject(host, { width: 800, height: 600, loadBackground: false })
|
||||
if (!view) {
|
||||
root.fail("LockView failed to instantiate: " + component.errorString())
|
||||
return
|
||||
}
|
||||
|
||||
var indicator = view.children ? findByObjectName(view, "fingerprintIndicator") : null
|
||||
root.assertTrue(indicator !== null, "fingerprint indicator exists in the lock view")
|
||||
|
||||
if (indicator) {
|
||||
view.fingerprintConfigured = false
|
||||
root.assertTrue(!indicator.visible, "fingerprint indicator is hidden when no sensor is configured")
|
||||
|
||||
view.fingerprintConfigured = true
|
||||
root.assertTrue(indicator.visible, "fingerprint indicator is shown when a sensor is configured")
|
||||
|
||||
// The field reserves space for the icon so a long password can never
|
||||
// slide underneath it. The reserve must exceed the icon's own width
|
||||
// (leaving a gap), and the shrunk dots must fit the reserved-clear
|
||||
// area even at extreme lengths.
|
||||
root.assertTrue(view.fingerprintReserve > indicator.width,
|
||||
"reserved space exceeds the icon width, got reserve " + view.fingerprintReserve + " vs icon " + indicator.width)
|
||||
|
||||
view.passwordText = "x".repeat(80)
|
||||
var clearWidth = view.fieldWidth - 2 * view.fingerprintReserve
|
||||
probe.font.pixelSize = Math.max(1, Math.floor(view.passwordDotFontSize * view.passwordDotScale))
|
||||
probe.font.letterSpacing = view.passwordDotLetterSpacing * view.passwordDotScale
|
||||
probe.text = "●".repeat(80)
|
||||
root.assertTrue(probe.advanceWidth <= clearWidth,
|
||||
"80 dots stay clear of the fingerprint icon, need " + probe.advanceWidth + "px of " + clearWidth)
|
||||
|
||||
view.fingerprintConfigured = false
|
||||
root.assertTrue(view.fingerprintReserve === 0, "no space is reserved when no sensor is configured")
|
||||
}
|
||||
|
||||
view.destroy()
|
||||
} catch (error) {
|
||||
root.fail("lock fingerprint indicator fixture threw: " + error)
|
||||
} finally {
|
||||
root.writeResult()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findByObjectName(node, name) {
|
||||
if (!node) return null
|
||||
if (node.objectName === name) return node
|
||||
var kids = node.children || []
|
||||
for (var i = 0; i < kids.length; i++) {
|
||||
var found = findByObjectName(kids[i], name)
|
||||
if (found) return found
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
@@ -137,24 +137,13 @@ ShellRoot {
|
||||
function rescan() {}
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockNotificationService
|
||||
property bool doNotDisturb: false
|
||||
property ListModel pendingModel: ListModel {}
|
||||
property ListModel pastModel: ListModel {}
|
||||
function setDoNotDisturb(value) { doNotDisturb = !!value }
|
||||
}
|
||||
|
||||
QtObject {
|
||||
id: mockShell
|
||||
property var bar: fakeBar
|
||||
property var barConfig: ({ position: "top" })
|
||||
property var shellConfig: ({ version: 1, idle: {}, plugins: [], bar: { layout: { left: [], center: [], right: [] } } })
|
||||
function firstPartyServiceFor(id) {
|
||||
if (id === "omarchy.notifications") return mockNotificationService
|
||||
return null
|
||||
}
|
||||
function serviceFor(id) { return firstPartyServiceFor(id) }
|
||||
function firstPartyServiceFor(id) { return null }
|
||||
function serviceFor(id) { return null }
|
||||
function summon(id, payloadJson) { return true }
|
||||
function hide(id) { return true }
|
||||
function toggle(id, payloadJson) { return true }
|
||||
|
||||
@@ -105,6 +105,20 @@ grep -Fq $'SUPER + RETURN Terminal' <<<"$fresh_output" || fail "default applicat
|
||||
grep -Fq $'SUPER + SHIFT + A ChatGPT' <<<"$fresh_output" || fail "default application bindings include preinstalled web apps"
|
||||
pass "default application bindings load from package defaults"
|
||||
|
||||
grep -F 'hl.dsp.send_key_state({ mods = mods, key = key, state = "down" })' "$ROOT/default/hypr/bindings/clipboard.lua" >/dev/null ||
|
||||
fail "universal clipboard shortcuts send explicit mods to the focused surface"
|
||||
pass "universal clipboard shortcuts send explicit mods to the focused surface"
|
||||
|
||||
if grep -E 'send_key_state\(\{[^}]*window' "$ROOT/default/hypr/bindings/clipboard.lua" >/dev/null; then
|
||||
fail "universal clipboard shortcuts do not target only normal windows"
|
||||
fi
|
||||
pass "universal clipboard shortcuts do not exclude layer-shell fields"
|
||||
|
||||
if grep -F 'wtype -M' "$ROOT/default/hypr/bindings/clipboard.lua" >/dev/null; then
|
||||
fail "universal clipboard shortcuts avoid the virtual keyboard so held SUPER cannot merge in"
|
||||
fi
|
||||
pass "universal clipboard shortcuts avoid virtual keyboard modifier merging"
|
||||
|
||||
removed_home="$tmpdir/removed-home"
|
||||
mkdir -p "$removed_home/.local/state/omarchy"
|
||||
touch "$removed_home/.local/state/omarchy/preinstalls-removed"
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command lua
|
||||
|
||||
resolved_input() {
|
||||
OMARCHY_PATH="$ROOT" OMARCHY_VCONSOLE="${1-}" lua <<'LUA'
|
||||
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
|
||||
|
||||
local vconsole = os.getenv("OMARCHY_VCONSOLE")
|
||||
local real_open = io.open
|
||||
|
||||
io.open = function(path, mode)
|
||||
if path ~= "/etc/vconsole.conf" then
|
||||
return real_open(path, mode)
|
||||
end
|
||||
|
||||
if not vconsole then
|
||||
return nil
|
||||
end
|
||||
|
||||
local file = io.tmpfile()
|
||||
file:write(vconsole)
|
||||
file:seek("set")
|
||||
return file
|
||||
end
|
||||
|
||||
hl = {
|
||||
config = function(config)
|
||||
local input = config.input
|
||||
print(("[%s] [%s] [%s]"):format(input.kb_layout, input.kb_variant, input.kb_options))
|
||||
end,
|
||||
}
|
||||
|
||||
o = { window = function() end }
|
||||
|
||||
require("default.hypr.input")
|
||||
LUA
|
||||
}
|
||||
|
||||
assert_input() {
|
||||
local description="$1"
|
||||
local expected="$2"
|
||||
local actual
|
||||
|
||||
if (( $# > 2 )); then
|
||||
actual=$(resolved_input "$3")
|
||||
else
|
||||
actual=$(resolved_input)
|
||||
fi
|
||||
|
||||
[[ $actual == "$expected" ]] ||
|
||||
fail "$description" "expected: $expected"$'\n'"actual: $actual"
|
||||
pass "$description"
|
||||
}
|
||||
|
||||
base_options="compose:caps,shift:both_capslock"
|
||||
toggle_options="$base_options,grp:alts_toggle"
|
||||
|
||||
assert_input "missing vconsole.conf falls back to us" "[us] [] [$base_options]"
|
||||
assert_input "us layout passes through" "[us] [intl] [$base_options]" 'XKBLAYOUT=us
|
||||
XKBVARIANT=intl
|
||||
'
|
||||
assert_input "latin layouts are left alone" "[de] [nodeadkeys] [$base_options]" 'XKBLAYOUT=de
|
||||
XKBVARIANT=nodeadkeys
|
||||
'
|
||||
assert_input "non-latin layout gains us in front" "[us,ara] [,] [$toggle_options]" 'XKBLAYOUT=ara
|
||||
'
|
||||
assert_input "prepended us keeps variants aligned" "[us,ru] [,phonetic] [$toggle_options]" 'XKBLAYOUT=ru
|
||||
XKBVARIANT=phonetic
|
||||
'
|
||||
assert_input "non-latin layout in front gains us even when us trails" "[us,il,us] [,] [$toggle_options]" 'XKBLAYOUT=il,us
|
||||
'
|
||||
|
||||
hooks_conf="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
|
||||
input_lua="$ROOT/default/hypr/input.lua"
|
||||
|
||||
hooks_layouts=$(awk -F')' '/\) ;;$/ { gsub(/[[:space:]|]+/, "\n", $1); print $1 }' "$hooks_conf" | grep '^[a-z]\+$' | sort)
|
||||
lua_layouts=$(sed -n '/^local non_latin_layouts =/,+1p' "$input_lua" | grep -o '"[^"]*"' | tr -d '"' | tr ' ' '\n' | grep '^[a-z]\+$' | sort)
|
||||
|
||||
[[ -n $hooks_layouts ]] || fail "non-latin layout list is readable from omarchy_hooks.conf"
|
||||
[[ $hooks_layouts == "$lua_layouts" ]] ||
|
||||
fail "non-latin layout lists stay in sync" "$(diff <(echo "$hooks_layouts") <(echo "$lua_layouts"))"
|
||||
pass "non-latin layout lists stay in sync with the initramfs hook"
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command lua
|
||||
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
stub_dir="$tmpdir/bin"
|
||||
home_dir="$tmpdir/home"
|
||||
log_file="$tmpdir/hyprctl.log"
|
||||
mkdir -p "$stub_dir" "$home_dir"
|
||||
|
||||
cat >"$stub_dir/hyprctl" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $1 == "activeworkspace" && -n $HYPRCTL_BROKEN ]]; then
|
||||
printf '{}\n'
|
||||
elif [[ $1 == "activeworkspace" ]]; then
|
||||
printf '{"id":3,"tiledLayout":"dwindle"}\n'
|
||||
else
|
||||
printf '%s\n' "$*" >>"$HYPRCTL_LOG"
|
||||
fi
|
||||
EOF
|
||||
chmod +x "$stub_dir/hyprctl"
|
||||
|
||||
cat >"$stub_dir/omarchy-notification-send" <<'EOF'
|
||||
#!/bin/bash
|
||||
:
|
||||
EOF
|
||||
chmod +x "$stub_dir/omarchy-notification-send"
|
||||
|
||||
HOME="$home_dir" HYPRCTL_LOG="$log_file" PATH="$stub_dir:$PATH" \
|
||||
"$ROOT/bin/omarchy-hyprland-workspace-layout-toggle"
|
||||
|
||||
layout_file="$home_dir/.local/state/omarchy/workspace-layouts/3.lua"
|
||||
[[ -f $layout_file ]] || fail "workspace layout toggle saves a workspace rule"
|
||||
grep -Fx 'hl.workspace_rule({ workspace = "3", layout = "scrolling" })' "$layout_file" >/dev/null ||
|
||||
fail "workspace layout toggle saves the selected layout"
|
||||
grep -Fx 'eval hl.workspace_rule({ workspace = "3", layout = "scrolling" })' "$log_file" >/dev/null ||
|
||||
fail "workspace layout toggle applies the selected layout immediately"
|
||||
pass "workspace layout toggle persists and applies the selected layout"
|
||||
|
||||
if HOME="$home_dir" HYPRCTL_LOG="$log_file" HYPRCTL_BROKEN=1 PATH="$stub_dir:$PATH" \
|
||||
"$ROOT/bin/omarchy-hyprland-workspace-layout-toggle" 2>/dev/null; then
|
||||
fail "workspace layout toggle exits nonzero without a workspace id"
|
||||
fi
|
||||
[[ -f "$home_dir/.local/state/omarchy/workspace-layouts/null.lua" ]] &&
|
||||
fail "workspace layout toggle does not persist a rule without a workspace id"
|
||||
pass "workspace layout toggle ignores broken hyprctl output"
|
||||
|
||||
HOME="$home_dir" OMARCHY_PATH="$ROOT" lua <<'LUA'
|
||||
local rules = {}
|
||||
|
||||
hl = {
|
||||
workspace_rule = function(rule)
|
||||
table.insert(rules, rule)
|
||||
end,
|
||||
}
|
||||
|
||||
dofile(os.getenv("OMARCHY_PATH") .. "/default/hypr/bootstrap.lua")
|
||||
require("default.hypr.workspace-layouts")
|
||||
|
||||
assert(#rules == 1)
|
||||
assert(rules[1].workspace == "3")
|
||||
assert(rules[1].layout == "scrolling")
|
||||
LUA
|
||||
pass "saved workspace layouts load into Hyprland configuration"
|
||||
@@ -1,145 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
const search = requireFromRoot('shell/plugins/launcher/LauncherSearch.js')
|
||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
||||
|
||||
const entries = [
|
||||
{
|
||||
name: 'Google Contacts',
|
||||
genericName: 'Address Book',
|
||||
comment: 'Manage contacts',
|
||||
keywords: ['contacts', 'address book', 'people'],
|
||||
id: 'google-contacts.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Calculator',
|
||||
genericName: 'Calculator',
|
||||
comment: 'Perform arithmetic, scientific or financial calculations',
|
||||
keywords: ['calculation', 'arithmetic', 'scientific', 'financial'],
|
||||
id: 'org.gnome.Calculator.desktop'
|
||||
},
|
||||
{
|
||||
name: 'OBS Studio',
|
||||
genericName: 'Streaming/Recording Software',
|
||||
comment: 'Free and Open Source Streaming/Recording Software',
|
||||
keywords: ['streaming', 'recording', 'capture'],
|
||||
id: 'com.obsproject.Studio.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Aether',
|
||||
genericName: '',
|
||||
comment: 'Minimal internet radio player',
|
||||
keywords: ['audio', 'music', 'radio'],
|
||||
id: 'io.github.taqi.aether.desktop'
|
||||
},
|
||||
{
|
||||
name: 'Xournal++',
|
||||
genericName: 'Notetaking',
|
||||
comment: 'Take handwritten notes',
|
||||
keywords: ['notes', 'pdf', 'annotation'],
|
||||
id: 'com.github.xournalpp.xournalpp.desktop'
|
||||
},
|
||||
{
|
||||
name: 'RustDesk',
|
||||
genericName: 'Remote Desktop',
|
||||
comment: 'Remote desktop control',
|
||||
keywords: ['remote', 'desktop', 'control'],
|
||||
id: 'com.rustdesk.RustDesk.desktop'
|
||||
}
|
||||
]
|
||||
|
||||
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
|
||||
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
|
||||
|
||||
assert(
|
||||
search.fuzzyScore(entries[1], 'contact') < 0,
|
||||
'calculator does not match contact as a loose subsequence'
|
||||
)
|
||||
|
||||
const acronymMatches = search.sortedEntries(entries, 'gc').map(row => search.entryName(row.entry))
|
||||
assertEqual(acronymMatches[0], 'Google Contacts', 'short acronym matching still works')
|
||||
|
||||
const directMatches = search.sortedEntries(entries, 'obs').map(row => search.entryName(row.entry))
|
||||
assertEqual(directMatches[0], 'OBS Studio', 'direct app-name matching still works')
|
||||
|
||||
assert(
|
||||
/function select\(delta\)[\s\S]*root\.disarmHover\(\)[\s\S]*root\.selectedIndex =/.test(launcherQml),
|
||||
'launcher keyboard navigation disarms stale hover before moving selection'
|
||||
)
|
||||
assert(
|
||||
/PointerMoveGate\s*\{[\s\S]*id: pointerGate[\s\S]*referenceItem: card[\s\S]*\}/.test(launcherQml),
|
||||
'launcher uses shared pointer movement gate in card coordinates'
|
||||
)
|
||||
assert(
|
||||
/function disarmHover\(\)[\s\S]*pointerGate\.reset\(\)/.test(launcherQml),
|
||||
'launcher resets pointer movement gate when hover is disarmed'
|
||||
)
|
||||
const openMatch = launcherQml.match(/function open\(payloadJson\) \{([\s\S]*?)\n \}/)
|
||||
assert(openMatch, 'launcher open function exists')
|
||||
assert(
|
||||
openMatch[1].indexOf('root.disarmHover()') < openMatch[1].indexOf('root.opened = true')
|
||||
&& !openMatch[1].includes('pointerGate.allowInitialSample()'),
|
||||
'launcher ignores a stale hidden-pointer position when becoming visible'
|
||||
)
|
||||
assert(
|
||||
/function selectFromPointer\(index, item, mouse\)[\s\S]*pointerGate\.moved\(item, mouse\)[\s\S]*root\.selectedIndex = index/.test(launcherQml),
|
||||
'launcher only selects from pointer after real movement'
|
||||
)
|
||||
assert(
|
||||
/onPositionChanged: function\(mouse\) \{\s*root\.selectFromPointer\(row\.index, row, mouse\)\s*\}/.test(launcherQml),
|
||||
'launcher row hover routes through pointer movement gate'
|
||||
)
|
||||
assert(
|
||||
/onEntered: root\.selectFromPointer\(row\.index, row, \{\s*x: mouseArea\.mouseX,\s*y: mouseArea\.mouseY\s*\}\)/.test(launcherQml),
|
||||
'launcher samples pointer movement immediately when entering a row'
|
||||
)
|
||||
assert(
|
||||
!/onContainsMouseChanged:[\s\S]*root\.selectedIndex/.test(launcherQml),
|
||||
'launcher does not select rows from containsMouse'
|
||||
)
|
||||
|
||||
const confirmDeleteMatch = launcherQml.match(/function confirmDelete\(\) \{([\s\S]*?)\n \}/)
|
||||
assert(confirmDeleteMatch, 'launcher confirmDelete function exists')
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('root.dismiss()'),
|
||||
'launcher delete closes launcher after confirmation'
|
||||
)
|
||||
assert(
|
||||
confirmDeleteMatch[1].includes('Util.execDetached(command)'),
|
||||
'launcher delete runs remover through the shell'
|
||||
)
|
||||
|
||||
const activateMatch = launcherQml.match(/function activateIndex\(index\) \{([\s\S]*?)\n \}/)
|
||||
assert(activateMatch, 'launcher activateIndex function exists')
|
||||
assert(
|
||||
!activateMatch[1].includes('entry.execute()'),
|
||||
'launcher does not execute desktop entries directly'
|
||||
)
|
||||
assert(
|
||||
activateMatch[1].includes('gtk-launch') && activateMatch[1].includes('Util.execDetached'),
|
||||
'launcher runs desktop entry launch through the shell'
|
||||
)
|
||||
|
||||
assert(
|
||||
/function iconIndexScanCommand\(\)[\s\S]*-path "\*\/apps\/\*" -o -path "\*\/devices\/\*"/.test(launcherQml),
|
||||
'launcher fallback icon index includes device icons'
|
||||
)
|
||||
|
||||
const iconSourceMatch = launcherQml.match(/function iconSource\(icon\) \{([\s\S]*?)\n \}/)
|
||||
assert(iconSourceMatch, 'launcher iconSource function exists')
|
||||
assert(
|
||||
iconSourceMatch[1].indexOf('root.iconIndex[value]') < iconSourceMatch[1].indexOf('Quickshell.iconPath(value, true)'),
|
||||
'launcher prefers indexed app icons over ambiguous themed icons'
|
||||
)
|
||||
|
||||
assert(
|
||||
openMatch[1].includes('if (!iconIndexScan.running) iconIndexScan.running = true'),
|
||||
'launcher refreshes its icon index when opened'
|
||||
)
|
||||
JS
|
||||
Executable
+93
@@ -0,0 +1,93 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
lid_close="$ROOT/bin/omarchy-system-lid-close"
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# closed/docked are the two facts logind uses to decide whether a lid close
|
||||
# suspends, so each scenario pins them and records what the lid handler did.
|
||||
setup_scenario() {
|
||||
scenario_dir="$tmpdir/$1"
|
||||
mock_bin="$scenario_dir/bin"
|
||||
call_log="$scenario_dir/calls"
|
||||
mkdir -p "$mock_bin"
|
||||
: >"$call_log"
|
||||
|
||||
local closed="$2" docked="$3"
|
||||
|
||||
cat >"$mock_bin/omarchy-hw-laptop-closed" <<SH
|
||||
#!/bin/bash
|
||||
exit $closed
|
||||
SH
|
||||
cat >"$mock_bin/omarchy-hw-external-monitors" <<SH
|
||||
#!/bin/bash
|
||||
exit $docked
|
||||
SH
|
||||
for command in omarchy-system-lock omarchy-hyprland-monitor-clamshell; do
|
||||
cat >"$mock_bin/$command" <<SH
|
||||
#!/bin/bash
|
||||
echo $command >>"\$CALL_LOG"
|
||||
SH
|
||||
done
|
||||
chmod +x "$mock_bin"/*
|
||||
}
|
||||
|
||||
run_lid_close() {
|
||||
CALL_LOG="$call_log" PATH="$mock_bin:$PATH" "$lid_close"
|
||||
mapfile -t calls <"$call_log"
|
||||
}
|
||||
|
||||
# An undocked lid close is about to suspend, and logind's inhibitor window is a
|
||||
# timer rather than a promise, so the lock has to start now instead of waiting
|
||||
# for PrepareForSleep.
|
||||
setup_scenario undocked 0 1
|
||||
run_lid_close
|
||||
|
||||
[[ ${calls[0]} == "omarchy-system-lock" ]] ||
|
||||
fail "undocked lid close locks before anything else" "calls: ${calls[*]}"
|
||||
pass "undocked lid close locks before anything else"
|
||||
|
||||
[[ ${calls[1]} == "omarchy-hyprland-monitor-clamshell" ]] ||
|
||||
fail "undocked lid close still reconciles displays" "calls: ${calls[*]}"
|
||||
pass "undocked lid close still reconciles displays"
|
||||
|
||||
# A docked lid close is clamshell mode: logind leaves the machine awake and the
|
||||
# session stays in use on the external display, so locking it would be wrong.
|
||||
setup_scenario docked 0 0
|
||||
run_lid_close
|
||||
|
||||
[[ ${calls[*]} != *omarchy-system-lock* ]] ||
|
||||
fail "docked lid close does not lock the session" "calls: ${calls[*]}"
|
||||
pass "docked lid close does not lock the session"
|
||||
|
||||
[[ ${calls[0]} == "omarchy-hyprland-monitor-clamshell" ]] ||
|
||||
fail "docked lid close reconciles displays" "calls: ${calls[*]}"
|
||||
pass "docked lid close reconciles displays"
|
||||
|
||||
# Hyprland can replay a switch binding when the lid is already open, and an
|
||||
# open lid must never lock the machine the user is sitting at.
|
||||
setup_scenario open 1 1
|
||||
run_lid_close
|
||||
|
||||
[[ ${calls[*]} != *omarchy-system-lock* ]] ||
|
||||
fail "an open lid never locks the session" "calls: ${calls[*]}"
|
||||
pass "an open lid never locks the session"
|
||||
|
||||
# The lid handler runs from a Hyprland binding, so a lock that hangs or fails
|
||||
# must not stop the display reconciliation behind it.
|
||||
setup_scenario failing_lock 0 1
|
||||
cat >"$mock_bin/omarchy-system-lock" <<'SH'
|
||||
#!/bin/bash
|
||||
echo omarchy-system-lock >>"$CALL_LOG"
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-system-lock"
|
||||
run_lid_close
|
||||
|
||||
[[ ${calls[1]} == "omarchy-hyprland-monitor-clamshell" ]] ||
|
||||
fail "a failing lock still reconciles displays" "calls: ${calls[*]}"
|
||||
pass "a failing lock still reconciles displays"
|
||||
+71
@@ -0,0 +1,71 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
TMPDIR=""
|
||||
QS_PID=""
|
||||
|
||||
cleanup() {
|
||||
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
|
||||
kill "$QS_PID" 2>/dev/null || true
|
||||
wait "$QS_PID" 2>/dev/null || true
|
||||
fi
|
||||
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
|
||||
pass "no Wayland compositor; skipping lock fingerprint indicator test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if ! command -v quickshell >/dev/null 2>&1; then
|
||||
pass "quickshell not installed; skipping lock fingerprint indicator test"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
require_command jq
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
result="$TMPDIR/result.json"
|
||||
log="$TMPDIR/quickshell.log"
|
||||
config_dir="$TMPDIR/lock-fingerprint-indicator"
|
||||
mkdir -p "$config_dir" "$TMPDIR/home"
|
||||
cp "$SHELL_TEST_DIR/fixtures/lock-fingerprint-indicator/shell.qml" "$config_dir/shell.qml"
|
||||
ln -s "$ROOT/shell/Ui" "$config_dir/Ui"
|
||||
ln -s "$ROOT/shell/Commons" "$config_dir/Commons"
|
||||
|
||||
OMARCHY_PATH="$ROOT" \
|
||||
OMARCHY_QML_TEST_RESULT="$result" \
|
||||
HOME="$TMPDIR/home" \
|
||||
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
|
||||
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
|
||||
PATH="$ROOT/bin:$PATH" \
|
||||
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
|
||||
QS_PID=$!
|
||||
|
||||
for _ in {1..80}; do
|
||||
[[ -s $result ]] && break
|
||||
if ! kill -0 "$QS_PID" 2>/dev/null; then
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "lock fingerprint indicator quickshell exited before writing result"
|
||||
fi
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
[[ -s $result ]] || {
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "lock fingerprint indicator test timed out"
|
||||
}
|
||||
|
||||
if ! jq -e '.ok == true' "$result" >/dev/null; then
|
||||
printf 'Lock fingerprint indicator result:\n' >&2
|
||||
jq . "$result" >&2
|
||||
printf 'Lock fingerprint indicator log:\n' >&2
|
||||
sed -n '1,220p' "$log" >&2
|
||||
fail "fingerprint indicator tracks the configured sensor"
|
||||
fi
|
||||
|
||||
pass "fingerprint indicator tracks the configured sensor"
|
||||
+120
-1
@@ -92,6 +92,8 @@ assertDeepEqual(
|
||||
kind: 'action',
|
||||
icon: '',
|
||||
iconFont: '',
|
||||
appIcon: '',
|
||||
appId: '',
|
||||
label: 'Theme picker',
|
||||
target: 'style.theme',
|
||||
detail: 'Style',
|
||||
@@ -107,6 +109,36 @@ assertDeepEqual(
|
||||
|
||||
const defaultItems = menu.parseMenuJsonc(defaultMenuJsonc)
|
||||
const defaultById = Object.fromEntries(defaultItems.map(item => [item.id, item]))
|
||||
|
||||
// Needs the real menu: app rows sort after all menu items, and only at that
|
||||
// item count does the order tiebreak alone bury an installed app.
|
||||
const rankBase = menu.mergeMenuSources(defaultItems, [])
|
||||
const ranked = menu.mergeAppRows(rankBase.items, rankBase.itemOrder, [
|
||||
{ id: 'apps.brave', parent: 'apps', kind: 'app', label: 'Brave', description: '', aliases: [] },
|
||||
{ id: 'apps.fontforge', parent: 'apps', kind: 'app', label: 'FontForge', description: '', aliases: [] }
|
||||
])
|
||||
const rankScore = (id, query) => menu.searchScore(ranked.items, ranked.items[id], query)
|
||||
assert(
|
||||
['install.browser.brave', 'remove.browser.brave', 'setup.default.browser.brave'].every(
|
||||
id => rankScore('apps.brave', 'brave') < rankScore(id, 'brave')
|
||||
),
|
||||
'menu ranks an installed app above menu entries matching the query equally well'
|
||||
)
|
||||
assert(
|
||||
rankScore('style.font', 'font') < rankScore('apps.fontforge', 'font'),
|
||||
'menu keeps a better-matching menu entry above a weaker app match'
|
||||
)
|
||||
const triggerItems = defaultItems.filter(item => item.parent === 'trigger')
|
||||
assertEqual(
|
||||
triggerItems[0].id,
|
||||
'trigger.emoji',
|
||||
'menu lists Emoji first under Trigger'
|
||||
)
|
||||
assertEqual(
|
||||
defaultById['trigger.emoji'].action,
|
||||
'omarchy-menu-emoji',
|
||||
'menu opens the emoji picker from Trigger'
|
||||
)
|
||||
assert(
|
||||
defaultById['update.omarchy'].icon === '\ue900',
|
||||
'menu update Omarchy entry uses the Omarchy glyph'
|
||||
@@ -120,7 +152,7 @@ assert(
|
||||
'menu keeps Input as a direct config action'
|
||||
)
|
||||
assert(
|
||||
defaultById['setup.direct-boot'].action.includes('omarchy-config-direct-boot'),
|
||||
defaultById['setup.direct-boot'].action.includes('omarchy-setup-direct-boot'),
|
||||
'menu places Direct Boot directly under Setup'
|
||||
)
|
||||
assertEqual(
|
||||
@@ -159,6 +191,11 @@ assertEqual(
|
||||
'omarchy-hw-laptop',
|
||||
'menu only shows Mirror Display on laptops'
|
||||
)
|
||||
assertEqual(
|
||||
defaultById['trigger.capture.screenrecord.webcam'].when,
|
||||
'omarchy-hw-webcam',
|
||||
'menu only shows webcam screen recording when a webcam is available'
|
||||
)
|
||||
assert(
|
||||
/font\.family: row\.iconFont\.length > 0 \? row\.iconFont : root\.fontFamily/.test(menuQml),
|
||||
'menu rows support per-icon font families'
|
||||
@@ -188,6 +225,88 @@ assert(
|
||||
/function disarmPointer\(\)[\s\S]*pointerGate\.reset\(\)/.test(menuQml),
|
||||
'menu resets pointer movement gate when pointer selection is disarmed'
|
||||
)
|
||||
// App rows are rebuilt from scratch on every desktop-entry rescan. The merge
|
||||
// must be idempotent and must never carry an orphan id forward, or a single
|
||||
// lost write turns into an app listed twice (and thrice, and so on).
|
||||
const nonAppItems = {
|
||||
root: { id: 'root', kind: 'menu', label: 'Go' },
|
||||
apps: { id: 'apps', kind: 'menu', label: 'Apps', provider: 'apps' }
|
||||
}
|
||||
const nonAppOrder = ['root', 'apps']
|
||||
const appRowsFor = ids => ids.map(id => ({ id: `apps.${id}`, kind: 'app', parent: 'apps', label: id, appId: id }))
|
||||
|
||||
const firstMerge = menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['alacritty', 'youtube']))
|
||||
assert(
|
||||
firstMerge.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube',
|
||||
'app merge appends app rows after the static menu items'
|
||||
)
|
||||
|
||||
const secondMerge = menu.mergeAppRows(firstMerge.items, firstMerge.itemOrder, appRowsFor(['alacritty', 'youtube']))
|
||||
assert(
|
||||
secondMerge.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube',
|
||||
'repeating the app merge with the same entries does not duplicate rows'
|
||||
)
|
||||
|
||||
assert(
|
||||
menu.mergeAppRows(secondMerge.items, secondMerge.itemOrder, appRowsFor(['alacritty'])).itemOrder.join(',')
|
||||
=== 'root,apps,apps.alacritty',
|
||||
'app merge drops rows for entries that went away'
|
||||
)
|
||||
|
||||
assert(
|
||||
menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['youtube', 'youtube'])).itemOrder.join(',')
|
||||
=== 'root,apps,apps.youtube',
|
||||
'app merge lists an app once even when two desktop entries share an id'
|
||||
)
|
||||
|
||||
const orphanedItems = {}
|
||||
for (const key in firstMerge.items) orphanedItems[key] = firstMerge.items[key]
|
||||
delete orphanedItems['apps.youtube']
|
||||
const healed = menu.mergeAppRows(orphanedItems, firstMerge.itemOrder, appRowsFor(['alacritty', 'youtube']))
|
||||
assert(
|
||||
healed.itemOrder.join(',') === 'root,apps,apps.alacritty,apps.youtube'
|
||||
&& !!healed.items['apps.youtube'],
|
||||
'app merge heals an order entry whose item went missing instead of duplicating it'
|
||||
)
|
||||
|
||||
assert(
|
||||
!firstMerge.items['apps.youtube'].hasOwnProperty('__probe')
|
||||
&& (() => {
|
||||
const before = Object.keys(nonAppItems).length
|
||||
menu.mergeAppRows(nonAppItems, nonAppOrder, appRowsFor(['gimp']))
|
||||
return Object.keys(nonAppItems).length === before
|
||||
})(),
|
||||
'app merge leaves the map it was handed untouched'
|
||||
)
|
||||
|
||||
const providerRowsFor = values => values.map(value => ({ id: `style.font.${value}`, kind: 'action', parent: 'style.font', label: value }))
|
||||
const firstProviderMerge = menu.mergeRowsById(nonAppItems, nonAppOrder, providerRowsFor(['mono', 'serif']))
|
||||
assert(
|
||||
firstProviderMerge.itemOrder.join(',') === 'root,apps,style.font.mono,style.font.serif',
|
||||
'provider merge appends its rows'
|
||||
)
|
||||
assert(
|
||||
menu.mergeRowsById(firstProviderMerge.items, firstProviderMerge.itemOrder, providerRowsFor(['mono', 'serif']))
|
||||
.itemOrder.join(',') === 'root,apps,style.font.mono,style.font.serif',
|
||||
'repeating a provider merge does not duplicate rows'
|
||||
)
|
||||
|
||||
// The maps live in QML `var` properties, where an in-place write is
|
||||
// occasionally dropped by the engine, so both merges must hand back fresh
|
||||
// objects for the caller to assign in one shot.
|
||||
assert(
|
||||
/var merged = MenuModel\.mergeAppRows\(root\.items, root\.itemOrder, appRows\)\s*\n\s*root\.items = merged\.items\s*\n\s*root\.itemOrder = merged\.itemOrder/.test(menuQml),
|
||||
'menu assigns the rebuilt app item map instead of mutating it in place'
|
||||
)
|
||||
assert(
|
||||
/var merged = MenuModel\.mergeRowsById\(root\.items, root\.itemOrder, providerRows\)\s*\n\s*root\.items = merged\.items\s*\n\s*root\.itemOrder = merged\.itemOrder/.test(menuQml),
|
||||
'menu assigns the rebuilt provider item map instead of mutating it in place'
|
||||
)
|
||||
assert(
|
||||
!/root\.items\[[^\]]+\] =/.test(menuQml) && !/delete root\.items\[/.test(menuQml),
|
||||
'menu never writes into the item map held by the var property'
|
||||
)
|
||||
|
||||
for (const functionName of ['openExistingMenu', 'openDmenu']) {
|
||||
const openMatch = menuQml.match(new RegExp(`function ${functionName}\\([^)]*\\) \\{([\\s\\S]*?)\\n \\}`))
|
||||
assert(openMatch, `menu ${functionName} function exists`)
|
||||
|
||||
@@ -33,6 +33,12 @@ bash -c "$command"
|
||||
SH
|
||||
chmod +x "$stub_bin/systemd-run"
|
||||
|
||||
cat >"$stub_bin/omarchy-notification-wait" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$stub_bin/omarchy-notification-wait"
|
||||
|
||||
cat >"$stub_bin/omarchy-notification-send" <<'SH'
|
||||
#!/bin/bash
|
||||
printf '%s\n' "$@" >"$OMARCHY_TEST_NOTIFY_ARGS"
|
||||
|
||||
@@ -65,6 +65,13 @@ local omarchy_monitor_scale = "auto"
|
||||
LUA
|
||||
}
|
||||
|
||||
write_internal_monitor_config() {
|
||||
cat >"$monitor_lua" <<'LUA'
|
||||
hl.monitor({ output = "eDP-1", mode = "preferred", position = "0x0", scale = 1.25 })
|
||||
hl.monitor({ output = "", mode = "preferred", position = "auto-right", scale = 1 })
|
||||
LUA
|
||||
}
|
||||
|
||||
run_clamshell() {
|
||||
HOME="$home_dir" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
@@ -102,3 +109,10 @@ OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell
|
||||
grep -F 'scale = 1.6' "$eval_log" >/dev/null || fail "clamshell recovery uses remembered internal scale"
|
||||
! grep -F 'scale = "auto"' "$eval_log" >/dev/null || fail "clamshell recovery avoids auto after disabled internal display"
|
||||
pass "clamshell recovery uses remembered internal scale"
|
||||
|
||||
write_internal_monitor_config
|
||||
: >"$eval_log"
|
||||
OMARCHY_TEST_INTERNAL_DISABLED=true run_clamshell
|
||||
grep -F 'position = "0x0"' "$eval_log" >/dev/null || fail "clamshell recovery uses configured internal position"
|
||||
grep -F 'scale = 1.25' "$eval_log" >/dev/null || fail "clamshell recovery uses configured internal scale"
|
||||
pass "clamshell recovery uses configured internal monitor rule"
|
||||
|
||||
@@ -7,12 +7,13 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
monitor_watch="$ROOT/bin/omarchy-hyprland-monitor-watch"
|
||||
monitor_internal="$ROOT/bin/omarchy-hyprland-monitor-internal"
|
||||
monitor_mirror="$ROOT/bin/omarchy-hyprland-monitor-internal-mirror"
|
||||
monitor_laptop="$ROOT/bin/omarchy-hyprland-monitor-laptop"
|
||||
monitor_external_active="$ROOT/bin/omarchy-hyprland-monitor-external-active"
|
||||
sleep_lock="$ROOT/bin/omarchy-system-sleep-lock"
|
||||
system_wake="$ROOT/bin/omarchy-system-wake"
|
||||
clamshell="$ROOT/bin/omarchy-hyprland-monitor-clamshell"
|
||||
lock_service="$ROOT/shell/plugins/lock/Service.qml"
|
||||
hw_clamshell="$ROOT/bin/omarchy-hw-clamshell"
|
||||
hw_laptop_closed="$ROOT/bin/omarchy-hw-laptop-closed"
|
||||
utilities="$ROOT/default/hypr/bindings/utilities.lua"
|
||||
|
||||
grep -F 'sleep "$delay"' "$monitor_watch" >/dev/null
|
||||
@@ -35,8 +36,8 @@ grep -F 'sync_poll_state' "$monitor_watch" >/dev/null
|
||||
grep -F 'done < <(socat' "$monitor_watch" >/dev/null
|
||||
pass "clamshell poll only runs on a docked laptop, not desktops or undocked laptops"
|
||||
|
||||
grep -F '/proc/acpi/button/lid/*/state' "$hw_clamshell" >/dev/null
|
||||
grep -F 'omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null
|
||||
grep -F 'omarchy-hw-laptop-closed && omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null
|
||||
grep -F '/proc/acpi/button/lid/*/state' "$hw_laptop_closed" >/dev/null
|
||||
pass "clamshell helper detects closed-lid external monitor state"
|
||||
|
||||
grep -F 'hyprctl monitors -j' "$monitor_external_active" >/dev/null
|
||||
@@ -60,7 +61,8 @@ grep -F 'omarchy-hw-clamshell' "$clamshell" >/dev/null
|
||||
pass "clamshell monitor sync disables laptop output and force-recovers it"
|
||||
|
||||
grep -F "hyprctl dispatch 'hl.dsp.dpms({ action = \"enable\" })' >/dev/null 2>&1 || true" "$monitor_internal" >/dev/null
|
||||
grep -F 'hyprctl monitors all -j' "$monitor_internal" >/dev/null
|
||||
grep -F 'omarchy-hyprland-monitor-laptop' "$monitor_internal" >/dev/null
|
||||
grep -F 'hyprctl monitors all -j' "$monitor_laptop" >/dev/null
|
||||
grep -F 'omarchy-hyprland-monitor-external-active' "$monitor_internal" >/dev/null
|
||||
grep -F 'wake' "$monitor_internal" >/dev/null
|
||||
grep -F 'omarchy-hyprland-toggle-enabled $TOGGLE || return 0' "$monitor_internal" >/dev/null
|
||||
@@ -70,13 +72,9 @@ pass "internal monitor recovery only wakes displays when it re-enables one"
|
||||
grep -F 'omarchy-hyprland-monitor-external-active' "$monitor_mirror" >/dev/null
|
||||
pass "internal mirror helper recovers when no active external display remains"
|
||||
|
||||
grep -F 'switch:on:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
|
||||
grep -F 'switch:on:Lid Switch", nil, "omarchy-system-lid-close"' "$utilities" >/dev/null
|
||||
grep -F 'switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
|
||||
pass "lid switch bindings reconcile clamshell display state"
|
||||
|
||||
grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$sleep_lock" >/dev/null
|
||||
grep -F '(( attempt % 5 == 0 )) && sync_clamshell' "$sleep_lock" >/dev/null
|
||||
pass "sleep lock syncs clamshell display state while waiting for secure lock"
|
||||
pass "lid switch bindings lock on close and reconcile clamshell display state"
|
||||
|
||||
grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$system_wake" >/dev/null
|
||||
pass "system wake resyncs clamshell display state"
|
||||
|
||||
@@ -74,14 +74,42 @@ scale=$(OMARCHY_TEST_MONITOR_SCALE=3 run_scaling)
|
||||
[[ $scale == "3" ]] || fail "monitor scaling reports explicit 3x scale" "actual: $scale"
|
||||
pass "monitor scaling reports explicit 3x scale"
|
||||
|
||||
# 1280x800 (QEMU virtio-gpu) can't divide cleanly by 3; expect a snap up to 3.2.
|
||||
scale=$(OMARCHY_TEST_MONITOR_SCALE=3.2 run_scaling)
|
||||
[[ $scale == "3.2" ]] || fail "monitor scaling reports the actual non-preset scale" "actual: $scale"
|
||||
pass "monitor scaling reports the actual non-preset scale"
|
||||
|
||||
# 1280x800 approximates the 3x preset as 3.2x.
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=2 OMARCHY_TEST_MONITOR_WIDTH=1280 OMARCHY_TEST_MONITOR_HEIGHT=800 run_scaling 3
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling snaps unclean 3x up to 3.2x"
|
||||
grep -Fx 'local omarchy_monitor_scale = 3.2' "$monitor_lua" >/dev/null || fail "monitor scaling persists snapped 3.2x"
|
||||
pass "monitor scaling snaps unclean 3x up to 3.2x"
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling approximates explicit 3x as 3.2x"
|
||||
grep -Fx 'local omarchy_monitor_scale = 3.2' "$monitor_lua" >/dev/null ||
|
||||
fail "monitor scaling persists approximated 3.2x"
|
||||
pass "monitor scaling approximates explicit 3x as 3.2x"
|
||||
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=2 OMARCHY_TEST_MONITOR_WIDTH=1280 OMARCHY_TEST_MONITOR_HEIGHT=800 run_scaling up
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling up snaps unclean preset to 3.2x"
|
||||
pass "monitor scaling up snaps unclean preset to 3.2x"
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling up reaches approximated 3.2x"
|
||||
pass "monitor scaling up reaches approximated 3.2x"
|
||||
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=4 OMARCHY_TEST_MONITOR_WIDTH=1280 OMARCHY_TEST_MONITOR_HEIGHT=800 run_scaling down
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling down reaches approximated 3.2x"
|
||||
pass "monitor scaling down reaches approximated 3.2x"
|
||||
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=2 OMARCHY_TEST_MONITOR_WIDTH=6016 OMARCHY_TEST_MONITOR_HEIGHT=3384 run_scaling 1.25
|
||||
grep -F 'scale = 1.33333' "$eval_out" >/dev/null || fail "monitor scaling approximates explicit 1.25x"
|
||||
pass "monitor scaling approximates explicit 1.25x"
|
||||
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=2 OMARCHY_TEST_MONITOR_WIDTH=1280 OMARCHY_TEST_MONITOR_HEIGHT=800 run_scaling 3.2
|
||||
grep -F 'scale = 3.2' "$eval_out" >/dev/null || fail "monitor scaling accepts displayed approximate values"
|
||||
pass "monitor scaling accepts displayed approximate values"
|
||||
|
||||
# On a mode where both 3x and 4x resolve to 4x, the duplicate is one step.
|
||||
write_monitor_config
|
||||
OMARCHY_TEST_MONITOR_SCALE=4 OMARCHY_TEST_MONITOR_WIDTH=1280 OMARCHY_TEST_MONITOR_HEIGHT=804 run_scaling down
|
||||
grep -F 'scale = 2' "$eval_out" >/dev/null || fail "monitor scaling down skips duplicate 4x approximation"
|
||||
grep -Fx 'local omarchy_monitor_scale = 2' "$monitor_lua" >/dev/null ||
|
||||
fail "monitor scaling down persists 2x after skipping duplicate approximation"
|
||||
pass "monitor scaling down skips duplicate approximation"
|
||||
|
||||
@@ -14,21 +14,60 @@ assertEqual(monitor.clampBrightness('nope'), 1, 'monitor rejects invalid brightn
|
||||
|
||||
assertEqual(monitor.normalizeScale('1.250'), '1.25', 'monitor normalizes fractional scale')
|
||||
assertEqual(monitor.normalizeScale('nope'), '', 'monitor rejects invalid scale')
|
||||
assertEqual(monitor.cleanScale(3, 1280, 800), '3.2', 'monitor matches clean VM scale')
|
||||
assertEqual(monitor.cleanScale(1.25, 1280, 800), '1.25', 'monitor preserves an already clean scale')
|
||||
assertEqual(monitor.cleanScale(1.25, 6016, 3384), '1.33', 'monitor matches clean physical display scale')
|
||||
assertEqual(monitor.cleanScale(1.6, 0, 800), '', 'monitor rejects a missing display mode')
|
||||
assertEqual(
|
||||
monitor.matchingScaleIndex(['1', '1.25', '1.6', '2', '3', '4'], 3.2, 1280, 800),
|
||||
4,
|
||||
'monitor selects an approximated VM scale'
|
||||
)
|
||||
assertEqual(
|
||||
monitor.matchingScaleIndex(['1', '1.25', '1.6', '2', '3', '4'], 4, 4, 4),
|
||||
5,
|
||||
'monitor selects an exact preset'
|
||||
)
|
||||
assertDeepEqual(
|
||||
monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 1280, 800),
|
||||
['1', '1.25', '1.6', '2', '3', '4'],
|
||||
'monitor keeps distinct approximated VM scales'
|
||||
)
|
||||
assertDeepEqual(
|
||||
monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 6016, 3384),
|
||||
['1', '1.25', '1.6', '2', '3', '4'],
|
||||
'monitor keeps distinct approximated physical display scales'
|
||||
)
|
||||
assertDeepEqual(
|
||||
monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 1280, 804),
|
||||
['1', '1.25', '2', '4'],
|
||||
'monitor collapses presets with duplicate effective scales'
|
||||
)
|
||||
assertDeepEqual(
|
||||
monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 5968, 3230),
|
||||
['1', '2'],
|
||||
'monitor hides presets the current mode cannot reach'
|
||||
)
|
||||
assertDeepEqual(
|
||||
monitor.availableScales(['1', '1.25', '1.6', '2', '3', '4'], 0, 0),
|
||||
['1', '1.25', '1.6', '2', '3', '4'],
|
||||
'monitor keeps presets until display dimensions are known'
|
||||
)
|
||||
|
||||
assertEqual(monitor.brightnessName(96), 'Sun blast', 'monitor names very bright displays')
|
||||
assertEqual(monitor.brightnessName(12), 'Candlelit', 'monitor names dim displays')
|
||||
|
||||
assertDeepEqual(
|
||||
monitor.parseDisplays(JSON.stringify([
|
||||
{ name: 'eDP-1', enabled: true },
|
||||
{ name: 'HDMI-A-1', enabled: false },
|
||||
{ name: 'DP-1', enabled: true }
|
||||
{ name: 'eDP-1', enabled: true, focused: false, width: 1920, height: 1080 },
|
||||
{ name: 'HDMI-A-1', enabled: false, focused: false, width: 0, height: 0 },
|
||||
{ name: 'DP-1', enabled: true, focused: true, width: 1280, height: 800 }
|
||||
])),
|
||||
{
|
||||
displays: [
|
||||
{ name: 'eDP-1', enabled: true },
|
||||
{ name: 'HDMI-A-1', enabled: false },
|
||||
{ name: 'DP-1', enabled: true }
|
||||
{ name: 'eDP-1', enabled: true, focused: false, width: 1920, height: 1080 },
|
||||
{ name: 'HDMI-A-1', enabled: false, focused: false, width: 0, height: 0 },
|
||||
{ name: 'DP-1', enabled: true, focused: true, width: 1280, height: 800 }
|
||||
],
|
||||
enabledDisplayCount: 2
|
||||
},
|
||||
|
||||
@@ -188,7 +188,6 @@ assertEqual(notifications.imageExtension('/tmp/no-extension'), 'png', 'notificat
|
||||
assertEqual(notifications.imageExtension('/tmp/archive.reallylong'), 'png', 'notifications reject suspicious image extensions')
|
||||
|
||||
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
|
||||
const barWidgetQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/BarWidget.qml'), 'utf8')
|
||||
assert(
|
||||
/readonly property int historyReplayLimit: 5/.test(serviceQml),
|
||||
'notifications service limits history replay to five rows'
|
||||
@@ -197,8 +196,4 @@ assert(
|
||||
/function showHistory\(\): string \{\s*return service\.showRecentHistory\(\)\s*\}/.test(serviceQml),
|
||||
'notifications history IPC replays recent notifications'
|
||||
)
|
||||
assert(
|
||||
!barWidgetQml.includes('historyOpenRequested'),
|
||||
'notifications history IPC does not depend on the bar widget'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -11,6 +11,7 @@ assertEqual(osd.iconFor('', 0), osd.iconFor('muted', 50), 'osd falls back to mut
|
||||
assertEqual(osd.iconFor('volume-high', 1), osd.iconFor('', 100), 'osd maps high volume aliases')
|
||||
assertEqual(osd.iconFor('logout', 50), '', 'osd maps logout icon')
|
||||
assertEqual(osd.iconFor('custom-symbol', 50), 'custom-symbol', 'osd preserves unknown explicit icons')
|
||||
assertEqual(osd.widestIcon, osd.iconFor('volume-high', 100), 'osd sizes the icon column to a glyph it can show')
|
||||
|
||||
assertDeepEqual(
|
||||
osd.stateForShow('volume', '', '75', '100', '', '800'),
|
||||
@@ -21,8 +22,7 @@ assertDeepEqual(
|
||||
value: 75,
|
||||
message: '75%',
|
||||
icon: osd.iconFor('volume', 75),
|
||||
duration: 800,
|
||||
fit: false
|
||||
duration: 800
|
||||
},
|
||||
'osd builds progress state'
|
||||
)
|
||||
@@ -36,24 +36,8 @@ assertDeepEqual(
|
||||
value: 0,
|
||||
message: 'Paused',
|
||||
icon: osd.iconFor('media-pause', -1),
|
||||
duration: 1200,
|
||||
fit: false
|
||||
duration: 1200
|
||||
},
|
||||
'osd builds message state'
|
||||
)
|
||||
|
||||
assertDeepEqual(
|
||||
osd.stateForShow('shutdown', 'Shutting down…', '', '100', '', '5000', '1'),
|
||||
{
|
||||
iconKey: 'shutdown',
|
||||
maxValue: 100,
|
||||
hasProgress: false,
|
||||
value: 0,
|
||||
message: 'Shutting down…',
|
||||
icon: osd.iconFor('shutdown', -1),
|
||||
duration: 5000,
|
||||
fit: true
|
||||
},
|
||||
'osd parses fit flag'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -23,19 +23,27 @@ assertEqual(
|
||||
)
|
||||
|
||||
assert(
|
||||
polkit.fingerprintFirstFromPamConfig(`
|
||||
polkit.fingerprintConfiguredFromPamConfig(`
|
||||
# comment
|
||||
auth sufficient pam_fprintd.so
|
||||
auth include system-auth
|
||||
`),
|
||||
'polkit detects fingerprint-first PAM config'
|
||||
'polkit detects fingerprint in a PAM config'
|
||||
)
|
||||
assert(
|
||||
!polkit.fingerprintFirstFromPamConfig(`
|
||||
polkit.fingerprintConfiguredFromPamConfig(`
|
||||
auth [success=1 default=ignore] pam_exec.so quiet /usr/bin/omarchy-hw-laptop-closed
|
||||
auth sufficient pam_fprintd.so
|
||||
auth required pam_unix.so
|
||||
`),
|
||||
'polkit detects fingerprint even behind a clamshell gate'
|
||||
)
|
||||
assert(
|
||||
!polkit.fingerprintConfiguredFromPamConfig(`
|
||||
account include system-auth
|
||||
auth include system-auth
|
||||
auth sufficient pam_fprintd.so
|
||||
auth required pam_unix.so
|
||||
`),
|
||||
'polkit detects password-first PAM config'
|
||||
'polkit reports no fingerprint when pam_fprintd is absent'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -70,7 +70,9 @@ printf '%s\n' "$*" >>"$OMARCHY_TEST_IPC_LOG"
|
||||
|
||||
case "$*" in
|
||||
*'shell ping')
|
||||
grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null && printf 'ok\n'
|
||||
[[ $* == *"-p $OMARCHY_TEST_SESSION_PATH/shell"* ]] &&
|
||||
grep -Fx '303' "$OMARCHY_TEST_QS_STATE" >/dev/null &&
|
||||
printf 'ok\n'
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
@@ -107,14 +109,27 @@ if [[ ${1:-} == "-j" && ${2:-} == "monitors" ]]; then
|
||||
fi
|
||||
elif [[ ${1:-} == "dispatch" && ${2:-} == hl.dsp.exec_cmd* ]]; then
|
||||
printf '%s\n' "${2:-}" >>"$OMARCHY_TEST_DISPATCH_LOG"
|
||||
env -u OMARCHY_TEST_TRANSIENT_ENV quickshell -n -p "$OMARCHY_PATH/shell"
|
||||
OMARCHY_PATH="$OMARCHY_TEST_SESSION_PATH" \
|
||||
env -u OMARCHY_TEST_TRANSIENT_ENV quickshell -n -p "$OMARCHY_TEST_SESSION_PATH/shell"
|
||||
printf 'ok\n'
|
||||
elif [[ ${1:-} == "dispatch" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
SH
|
||||
|
||||
chmod +x "$restart_bin/qs" "$restart_bin/quickshell" "$restart_bin/hyprctl"
|
||||
cat >"$restart_bin/systemctl" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
if [[ ${1:-} == "--user" && ${2:-} == "show-environment" ]]; then
|
||||
printf 'OMARCHY_PATH=%s\n' "$OMARCHY_TEST_SESSION_PATH"
|
||||
elif [[ ${1:-} == "--user" && ${2:-} == "try-restart" ]]; then
|
||||
exit 0
|
||||
else
|
||||
exit 1
|
||||
fi
|
||||
SH
|
||||
|
||||
chmod +x "$restart_bin/qs" "$restart_bin/quickshell" "$restart_bin/hyprctl" "$restart_bin/systemctl"
|
||||
|
||||
sleep 30 &
|
||||
restart_pid_one=$!
|
||||
@@ -122,14 +137,19 @@ sleep 30 &
|
||||
restart_pid_two=$!
|
||||
printf '%s\n%s\n' "$restart_pid_one" "$restart_pid_two" >"$restart_state"
|
||||
|
||||
caller_root="$test_tmp/caller-root"
|
||||
mkdir -p "$caller_root/shell"
|
||||
touch "$caller_root/shell/shell.qml"
|
||||
|
||||
PATH="$restart_bin:$PATH" \
|
||||
OMARCHY_PATH="$restart_root" \
|
||||
OMARCHY_PATH="$caller_root" \
|
||||
XDG_RUNTIME_DIR="$runtime_dir" \
|
||||
OMARCHY_TEST_QS_STATE="$restart_state" \
|
||||
OMARCHY_TEST_QS_LOG="$restart_log" \
|
||||
OMARCHY_TEST_QS_ENV_LOG="$restart_env_log" \
|
||||
OMARCHY_TEST_DISPATCH_LOG="$dispatch_log" \
|
||||
OMARCHY_TEST_IPC_LOG="$ipc_log" \
|
||||
OMARCHY_TEST_SESSION_PATH="$restart_root" \
|
||||
OMARCHY_TEST_TRANSIENT_ENV=leaked \
|
||||
timeout 5 "$ROOT/bin/omarchy-restart-shell"
|
||||
|
||||
@@ -145,10 +165,11 @@ restart_pid_one=""
|
||||
restart_pid_two=""
|
||||
[[ $(<"$restart_state") == 303 ]] || fail "restart leaves exactly one fresh shell instance"
|
||||
[[ $(grep -c '^-n -p ' "$restart_log") == 1 ]] || fail "restart launches one fresh shell process"
|
||||
grep -F "kill -p $restart_root/shell --any-display" "$restart_log" >/dev/null || fail "restart stops the shell from the session checkout"
|
||||
[[ $(<"$restart_env_log") == "unset" ]] || fail "restart uses the Hyprland session environment for the fresh shell"
|
||||
grep -F 'hl.dsp.exec_cmd("quickshell -n -p $OMARCHY_PATH/shell")' "$dispatch_log" >/dev/null || fail "restart launches the fresh shell through Hyprland"
|
||||
grep -F 'shell ping' "$ipc_log" >/dev/null || fail "restart waits for fresh shell IPC readiness"
|
||||
pass "restart replaces duplicate shell instances"
|
||||
grep -F "ipc -n -p $restart_root/shell call -- shell ping" "$ipc_log" >/dev/null || fail "restart checks readiness in the session checkout"
|
||||
pass "restart replaces duplicate shell instances from the session checkout"
|
||||
|
||||
: >"$restart_log"
|
||||
printf '404\n' >"$restart_state"
|
||||
@@ -161,6 +182,7 @@ locked_error=$(PATH="$restart_bin:$PATH" \
|
||||
OMARCHY_TEST_QS_LOG="$restart_log" \
|
||||
OMARCHY_TEST_DISPATCH_LOG="$dispatch_log" \
|
||||
OMARCHY_TEST_IPC_LOG="$ipc_log" \
|
||||
OMARCHY_TEST_SESSION_PATH="$restart_root" \
|
||||
"$ROOT/bin/omarchy-restart-shell" 2>&1) && fail "restart refuses while the shell lock is active"
|
||||
|
||||
[[ $locked_error == "Refusing to restart Omarchy shell while the session is locked." ]] || fail "locked restart explains why it was refused" "$locked_error"
|
||||
|
||||
@@ -5,7 +5,6 @@ run_node_test <<'JS'
|
||||
const fs = require('fs')
|
||||
|
||||
const menuQml = fs.readFileSync(path.join(root, 'shell/plugins/menu/Menu.qml'), 'utf8')
|
||||
const launcherQml = fs.readFileSync(path.join(root, 'shell/plugins/launcher/Launcher.qml'), 'utf8')
|
||||
|
||||
assert(
|
||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(menuQml)
|
||||
@@ -18,14 +17,4 @@ assert(
|
||||
'Menu row content does not depend on current selected border state'
|
||||
)
|
||||
|
||||
assert(
|
||||
/rowReservedBorderLeft:\s*Border\.left\(selectedBorderSpec\)/.test(launcherQml)
|
||||
&& /rowReservedBorderRight:\s*Border\.right\(selectedBorderSpec\)/.test(launcherQml),
|
||||
'Launcher rows reserve selected border insets'
|
||||
)
|
||||
|
||||
assert(
|
||||
!/anchors\.(left|right)Margin:[^\n]*\brow\.border(Left|Right)\b/.test(launcherQml),
|
||||
'Launcher row content does not depend on current selected border state'
|
||||
)
|
||||
JS
|
||||
|
||||
@@ -127,8 +127,8 @@ jq -e '
|
||||
}
|
||||
pass "shell IPC returns effective shell config"
|
||||
|
||||
[[ $(shell_ipc shell summon omarchy.launcher '{"query":"term"}') == "ok" ]] || fail_with_log "shell IPC summons launcher overlay"
|
||||
shell_ipc_quiet shell hide omarchy.launcher >/dev/null
|
||||
[[ $(shell_ipc shell summon omarchy.menu '{"menu":"apps"}') == "ok" ]] || fail_with_log "shell IPC summons menu apps overlay"
|
||||
shell_ipc_quiet shell hide omarchy.menu >/dev/null
|
||||
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"
|
||||
pass "shell IPC summon and hide contract works"
|
||||
|
||||
|
||||
@@ -13,6 +13,8 @@ mkdir -p "$stub_bin"
|
||||
cat >"$stub_bin/v4l2-ctl" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
[[ ${OMARCHY_TEST_NO_WEBCAM:-false} == "true" ]] && exit 0
|
||||
|
||||
printf '%s\n' "Built-in Webcam: Integrated Camera"
|
||||
printf '\t%s\n' "/dev/video0"
|
||||
printf '\t%s\n' "/dev/video1"
|
||||
@@ -43,10 +45,24 @@ SH
|
||||
chmod +x "$stub_bin"/*
|
||||
|
||||
export PATH="$stub_bin:$ROOT/bin:$PATH"
|
||||
# The resize helper anchors to a region file here, so keep it out of the real one
|
||||
export XDG_RUNTIME_DIR="$tmp_dir"
|
||||
export OMARCHY_TEST_MENU_ARGS="$tmp_dir/menu-args"
|
||||
export OMARCHY_TEST_RECORDER_ARGS="$tmp_dir/recorder-args"
|
||||
export OMARCHY_TEST_NOTIFICATION_ARGS="$tmp_dir/notification-args"
|
||||
|
||||
if "$ROOT/bin/omarchy-hw-webcam"; then
|
||||
pass "webcam hardware detection succeeds when a video device is available"
|
||||
else
|
||||
fail "webcam hardware detection succeeds when a video device is available"
|
||||
fi
|
||||
|
||||
if OMARCHY_TEST_NO_WEBCAM=true "$ROOT/bin/omarchy-hw-webcam"; then
|
||||
fail "webcam hardware detection fails when no video device is available"
|
||||
else
|
||||
pass "webcam hardware detection fails when no video device is available"
|
||||
fi
|
||||
|
||||
"$ROOT/bin/omarchy-capture-screenrecording-with-webcam"
|
||||
|
||||
expected_menu_args="$tmp_dir/expected-menu-args"
|
||||
@@ -139,6 +155,59 @@ if [[ -s $OMARCHY_TEST_HYPRCTL_ARGS ]]; then
|
||||
fi
|
||||
pass "webcam resize ignores other windows"
|
||||
|
||||
region_file="$XDG_RUNTIME_DIR/omarchy-screenrecord-region"
|
||||
|
||||
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||
echo "800x600+100+100" >"$region_file"
|
||||
"$ROOT/bin/omarchy-capture-webcam-resize" reset
|
||||
|
||||
printf '%s\n' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 133, y = 150 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 727, y = 510 })' >"$expected_hyprctl_args"
|
||||
|
||||
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||
fail "webcam anchors to the recorded region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||
fi
|
||||
pass "webcam anchors to the recorded region"
|
||||
|
||||
printf '%s\n' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 178, y = 200 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 2342, y = 460 })' >"$expected_hyprctl_args"
|
||||
|
||||
for region in "not-a-region" ""; do
|
||||
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||
printf '%s' "$region" >"$region_file"
|
||||
"$ROOT/bin/omarchy-capture-webcam-resize" reset
|
||||
|
||||
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||
fail "webcam falls back to the monitor for an unusable region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||
fi
|
||||
done
|
||||
pass "webcam falls back to the monitor for an unusable region"
|
||||
|
||||
# A region too narrow for presets scaled from its height shrinks the whole
|
||||
# ladder, so the three sizes stay distinct and each one fits inside the margins
|
||||
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
|
||||
echo "200x1200+0+0" >"$region_file"
|
||||
for size in small medium large; do
|
||||
"$ROOT/bin/omarchy-capture-webcam-resize" "$size"
|
||||
done
|
||||
|
||||
printf '%s\n' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 64, y = 72 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 96, y = 1088 })' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 89, y = 100 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 71, y = 1060 })' \
|
||||
'dispatch hl.dsp.window.resize({ window = "address:0xabc", x = 120, y = 135 })' \
|
||||
'dispatch hl.dsp.window.move({ window = "address:0xabc", x = 40, y = 1025 })' >"$expected_hyprctl_args"
|
||||
|
||||
if ! cmp -s "$OMARCHY_TEST_HYPRCTL_ARGS" "$expected_hyprctl_args"; then
|
||||
fail "webcam sizes stay distinct and inside a narrow region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
|
||||
fi
|
||||
pass "webcam sizes stay distinct and inside a narrow region"
|
||||
|
||||
rm -f "$region_file"
|
||||
|
||||
grep -F 'o.bind("SUPER + ALT + code:34", "Make webcam overlay smaller", "omarchy-capture-webcam-resize smaller")' \
|
||||
"$ROOT/default/hypr/bindings/utilities.lua" >/dev/null || fail "webcam smaller hotkey is configured"
|
||||
grep -F 'o.bind("SUPER + ALT + code:35", "Make webcam overlay larger", "omarchy-capture-webcam-resize larger")' \
|
||||
|
||||
Executable
+30
@@ -0,0 +1,30 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
mkdir -p "$test_dir/bin" "$test_dir/run"
|
||||
touch "$test_dir/run/wayland-1" "$test_dir/run/wayland-1.lock"
|
||||
|
||||
cat >"$test_dir/bin/qs" <<'STUB'
|
||||
#!/bin/bash
|
||||
echo "display=[$WAYLAND_DISPLAY]"
|
||||
STUB
|
||||
chmod +x "$test_dir/bin/qs"
|
||||
|
||||
export PATH="$test_dir/bin:$PATH"
|
||||
export OMARCHY_PATH="$ROOT"
|
||||
export XDG_RUNTIME_DIR="$test_dir/run"
|
||||
|
||||
# tmux hooks run without WAYLAND_DISPLAY, and qs matches instances by display.
|
||||
output=$(env -u WAYLAND_DISPLAY "$ROOT/bin/omarchy-shell" omarchy.indicators refresh)
|
||||
[[ $output == "display=[wayland-1]" ]] || fail "shell ipc recovers a missing display" "$output"
|
||||
pass "shell ipc recovers a missing display"
|
||||
|
||||
output=$(WAYLAND_DISPLAY=wayland-9 "$ROOT/bin/omarchy-shell" omarchy.indicators refresh)
|
||||
[[ $output == "display=[wayland-9]" ]] || fail "shell ipc keeps an existing display" "$output"
|
||||
pass "shell ipc keeps an existing display"
|
||||
Executable
+336
@@ -0,0 +1,336 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
|
||||
|
||||
sleep_lock="$ROOT/bin/omarchy-system-sleep-lock"
|
||||
tmpdir=$(mktemp -d)
|
||||
trap 'rm -rf "$tmpdir"' EXIT
|
||||
|
||||
# Each scenario gets its own mock PATH and call log, then runs the sleep lock
|
||||
# with a short budget so a stalled shell cannot slow the suite down.
|
||||
setup_scenario() {
|
||||
scenario_dir="$tmpdir/$1"
|
||||
mock_bin="$scenario_dir/bin"
|
||||
call_log="$scenario_dir/calls"
|
||||
state_dir="$scenario_dir/state"
|
||||
notify_log="$scenario_dir/notifications"
|
||||
journal_log="$scenario_dir/journal"
|
||||
mkdir -p "$mock_bin" "$state_dir"
|
||||
: >"$notify_log"
|
||||
: >"$journal_log"
|
||||
|
||||
# The budget is derived from logind, so pin the window rather than letting the
|
||||
# host's own configuration decide what these scenarios are testing.
|
||||
mock_logind_window 5000000
|
||||
|
||||
# Capture the desktop warning instead of firing a real one at whoever is
|
||||
# running the suite.
|
||||
cat >"$mock_bin/omarchy-notification-send" <<SH
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "\$*" >>"$notify_log"
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-notification-send"
|
||||
}
|
||||
|
||||
mock_logind_window() {
|
||||
cat >"$mock_bin/busctl" <<SH
|
||||
#!/bin/bash
|
||||
|
||||
printf 't %s\n' $1
|
||||
SH
|
||||
chmod +x "$mock_bin/busctl"
|
||||
}
|
||||
|
||||
mock_clamshell() {
|
||||
cat >"$mock_bin/omarchy-hyprland-monitor-clamshell" <<SH
|
||||
#!/bin/bash
|
||||
|
||||
echo clamshell >>"\$CALL_LOG"
|
||||
sleep ${1:-0}
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-hyprland-monitor-clamshell"
|
||||
}
|
||||
|
||||
# Called with no budget to exercise the value derived from logind's window.
|
||||
run_sleep_lock() {
|
||||
local args=()
|
||||
[[ -n ${1:-} ]] && args=("$1")
|
||||
|
||||
start_us=${EPOCHREALTIME//[!0-9]/}
|
||||
set +e
|
||||
CALL_LOG="$call_log" STATE_DIR="$state_dir" PATH="$mock_bin:$PATH" \
|
||||
"$sleep_lock" "${args[@]}" 2>"$journal_log"
|
||||
exit_status=$?
|
||||
set -e
|
||||
elapsed_us=$((10#${EPOCHREALTIME//[!0-9]/} - 10#$start_us))
|
||||
|
||||
mapfile -t calls <"$call_log"
|
||||
}
|
||||
|
||||
# A responsive shell locks immediately, even when the clamshell sync stalls.
|
||||
setup_scenario responsive
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
printf 'ok\n'
|
||||
elif [[ $* == "lock status" ]]; then
|
||||
printf '{"secure":true}\n'
|
||||
fi
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell 2
|
||||
|
||||
run_sleep_lock 4000
|
||||
|
||||
(( exit_status == 0 )) ||
|
||||
fail "sleep lock succeeds once the session reports secure" "exit: $exit_status"
|
||||
pass "sleep lock succeeds once the session reports secure"
|
||||
|
||||
[[ ${calls[0]} == "shell lock lock" ]] ||
|
||||
fail "sleep lock requests the session lock first" "first call: ${calls[0]}"
|
||||
pass "sleep lock requests the session lock first"
|
||||
|
||||
[[ ${calls[1]} == "clamshell" && ${calls[2]} == "shell lock status" ]] ||
|
||||
fail "sleep lock checks security after clamshell reconciliation"
|
||||
pass "sleep lock checks security after clamshell reconciliation"
|
||||
|
||||
(( elapsed_us < 1500000 )) ||
|
||||
fail "sleep lock bounds a stalled clamshell sync" "elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock bounds a stalled clamshell sync"
|
||||
|
||||
# A shell that never secures the session must give up inside the budget rather
|
||||
# than hold logind's delay inhibitor open.
|
||||
setup_scenario never_secure
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
printf 'ok\n'
|
||||
elif [[ $* == "lock status" ]]; then
|
||||
printf '{"secure":false}\n'
|
||||
fi
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell
|
||||
|
||||
run_sleep_lock 1500
|
||||
|
||||
(( exit_status != 0 )) ||
|
||||
fail "sleep lock reports failure when the session never secures"
|
||||
pass "sleep lock reports failure when the session never secures"
|
||||
|
||||
# The contract is the budget plus at most one poll interval, since the pause
|
||||
# between polls is not itself clipped. Derived budgets hold back a full second
|
||||
# for logind, so that overshoot is always well inside the reserve.
|
||||
(( elapsed_us <= 1600000 )) ||
|
||||
fail "sleep lock gives up within its budget" "elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock gives up within its budget"
|
||||
|
||||
polls=0
|
||||
for call in "${calls[@]}"; do
|
||||
[[ $call == "shell lock status" ]] && (( ++polls ))
|
||||
done
|
||||
(( polls > 1 )) ||
|
||||
fail "sleep lock keeps polling until the deadline" "polls: $polls"
|
||||
pass "sleep lock keeps polling until the deadline"
|
||||
|
||||
# A lock request that times out may never have landed, so the wait retries it
|
||||
# instead of suspending an unlocked session over one slow IPC call.
|
||||
setup_scenario retry_lock
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
if [[ -f $STATE_DIR/requested ]]; then
|
||||
touch "$STATE_DIR/locked"
|
||||
printf 'ok\n'
|
||||
exit 0
|
||||
fi
|
||||
touch "$STATE_DIR/requested"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $* == "lock status" ]]; then
|
||||
if [[ -f $STATE_DIR/locked ]]; then
|
||||
printf '{"secure":true}\n'
|
||||
else
|
||||
printf '{"secure":false}\n'
|
||||
fi
|
||||
fi
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell
|
||||
|
||||
run_sleep_lock 4000
|
||||
|
||||
(( exit_status == 0 )) ||
|
||||
fail "sleep lock retries a failed lock request" "exit: $exit_status"
|
||||
pass "sleep lock retries a failed lock request"
|
||||
|
||||
requests=0
|
||||
for call in "${calls[@]}"; do
|
||||
[[ $call == "shell lock lock" ]] && (( ++requests ))
|
||||
done
|
||||
(( requests == 2 )) ||
|
||||
fail "sleep lock stops requesting once the lock lands" "requests: $requests"
|
||||
pass "sleep lock stops requesting once the lock lands"
|
||||
|
||||
# A request can land even when its IPC response times out. Pending status proves
|
||||
# that Quickshell is already securing the session, so do not spend the remaining
|
||||
# inhibitor budget sending the same request again.
|
||||
setup_scenario pending_lock
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $* == "lock status" ]]; then
|
||||
if [[ -f $STATE_DIR/pending_seen ]]; then
|
||||
printf '{"secure":true}\n'
|
||||
else
|
||||
touch "$STATE_DIR/pending_seen"
|
||||
printf '{"secure":false,"requested":true,"pending":true,"sessionLocked":false}\n'
|
||||
fi
|
||||
fi
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell
|
||||
|
||||
run_sleep_lock 4000
|
||||
|
||||
(( exit_status == 0 )) ||
|
||||
fail "sleep lock succeeds after observing a pending lock" "exit: $exit_status"
|
||||
pass "sleep lock succeeds after observing a pending lock"
|
||||
|
||||
requests=0
|
||||
for call in "${calls[@]}"; do
|
||||
[[ $call == "shell lock lock" ]] && (( ++requests ))
|
||||
done
|
||||
(( requests == 1 )) ||
|
||||
fail "sleep lock does not retry an observed pending lock" "requests: $requests"
|
||||
pass "sleep lock does not retry an observed pending lock"
|
||||
|
||||
# The shell reports a refusal on stdout with a zero exit, so a lock it can never
|
||||
# perform has to end the wait instead of burning the rest of the window on it.
|
||||
setup_scenario missing_pam
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
printf 'missing-pam\n'
|
||||
fi
|
||||
exit 0
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell
|
||||
|
||||
run_sleep_lock 4000
|
||||
|
||||
(( exit_status != 0 )) ||
|
||||
fail "sleep lock fails fast when the shell cannot lock at all"
|
||||
(( elapsed_us < 500000 )) ||
|
||||
fail "sleep lock fails fast when the shell cannot lock at all" "elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock fails fast when the shell cannot lock at all"
|
||||
|
||||
[[ ${calls[*]} != *"lock status"* ]] ||
|
||||
fail "sleep lock stops polling a shell that refused to lock" "calls: ${calls[*]}"
|
||||
pass "sleep lock stops polling a shell that refused to lock"
|
||||
|
||||
# logind suspends regardless of this exit status, so an unlocked suspend is
|
||||
# otherwise invisible. The warning is the only trace the user ever sees, and the
|
||||
# journal line is what makes it diagnosable after the fact.
|
||||
grep -qF "did not lock before suspend" "$notify_log" ||
|
||||
fail "sleep lock warns that the session was left unlocked" \
|
||||
"notifications: $(< "$notify_log")"
|
||||
pass "sleep lock warns that the session was left unlocked"
|
||||
|
||||
grep -qF "suspending without a secure lock" "$journal_log" ||
|
||||
fail "sleep lock records the unlocked suspend in the journal" \
|
||||
"journal: $(< "$journal_log")"
|
||||
pass "sleep lock records the unlocked suspend in the journal"
|
||||
|
||||
# A never-securing shell is the scenario that runs out the whole budget, so it
|
||||
# is also the one that shows which budget was derived.
|
||||
never_secures() {
|
||||
cat >"$mock_bin/omarchy-shell" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf 'shell %s\n' "$*" >>"$CALL_LOG"
|
||||
if [[ $* == "lock lock" ]]; then
|
||||
printf 'ok\n'
|
||||
elif [[ $* == "lock status" ]]; then
|
||||
printf '{"secure":false,"requested":true,"pending":true,"sessionLocked":false}\n'
|
||||
fi
|
||||
SH
|
||||
chmod +x "$mock_bin/omarchy-shell"
|
||||
mock_clamshell
|
||||
}
|
||||
|
||||
# The drop-in only counts once logind has reloaded it, and a machine can carry
|
||||
# its own override, so the budget follows whatever logind actually enforces.
|
||||
setup_scenario derived_short_window
|
||||
mock_logind_window 2000000
|
||||
never_secures
|
||||
|
||||
run_sleep_lock
|
||||
|
||||
(( elapsed_us <= 1300000 )) ||
|
||||
fail "sleep lock derives its budget from logind's window" "elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock derives its budget from logind's window"
|
||||
|
||||
# Without a readable window there is no way to know what logind will tolerate,
|
||||
# so fall back to the budget that was safe before the drop-in existed.
|
||||
setup_scenario unreadable_window
|
||||
cat >"$mock_bin/busctl" <<'SH'
|
||||
#!/bin/bash
|
||||
exit 1
|
||||
SH
|
||||
chmod +x "$mock_bin/busctl"
|
||||
never_secures
|
||||
|
||||
run_sleep_lock
|
||||
|
||||
(( elapsed_us > 1300000 && elapsed_us <= 4300000 )) ||
|
||||
fail "sleep lock falls back to a conservative budget" "elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock falls back to a conservative budget when logind cannot be read"
|
||||
|
||||
# A hand-raised window must not strand a closed laptop awake in a bag. This
|
||||
# scenario runs for the whole capped budget by design.
|
||||
setup_scenario capped_window
|
||||
mock_logind_window 600000000
|
||||
never_secures
|
||||
|
||||
run_sleep_lock
|
||||
|
||||
(( exit_status != 0 )) ||
|
||||
fail "sleep lock caps the budget a huge logind window would allow"
|
||||
(( elapsed_us <= 12500000 )) ||
|
||||
fail "sleep lock caps the budget a huge logind window would allow" \
|
||||
"elapsed: ${elapsed_us}us"
|
||||
pass "sleep lock caps the budget a huge logind window would allow"
|
||||
|
||||
# The cap is only reachable because the shipped drop-in widens logind's window
|
||||
# past it. Ship one without the other and the cap is dead weight.
|
||||
inhibit_delay=$(sed -n 's/^InhibitDelayMaxSec=//p' "$ROOT/etc/systemd/logind.conf.d/20-inhibit-delay.conf")
|
||||
budget_cap_ms=$(sed -n 's/^budget_cap_ms=//p' "$sleep_lock")
|
||||
|
||||
[[ -n $inhibit_delay && -n $budget_cap_ms ]] ||
|
||||
fail "sleep lock cap and logind window are both declared" \
|
||||
"window: ${inhibit_delay:-unset} cap: ${budget_cap_ms:-unset}"
|
||||
(( budget_cap_ms < inhibit_delay * 1000 )) ||
|
||||
fail "sleep lock cap leaves logind room to act" \
|
||||
"cap: ${budget_cap_ms}ms window: ${inhibit_delay}s"
|
||||
pass "sleep lock cap stays inside the shipped logind inhibitor window"
|
||||
@@ -28,12 +28,19 @@ grep -F 'ExecStart=/usr/bin/omarchy-system-sleep-monitor' "$upgrade_to_quattro"
|
||||
grep -F 'reset-failed omarchy-sleep-lock.service' "$upgrade_to_quattro" >/dev/null
|
||||
pass "Omarchy 4 upgrade repairs the legacy sleep lock unit path"
|
||||
|
||||
notify_path="$ROOT/default/systemd/user/omarchy-update-user-notify.path"
|
||||
! grep -q 'PathExistsGlob' "$notify_path"
|
||||
grep -Fx 'PathModified=/usr/share/omarchy/migrations' "$notify_path" >/dev/null
|
||||
pass "migration watcher is edge-triggered so applied migrations on disk cannot re-trigger it"
|
||||
[[ -e $ROOT/default/systemd/user/omarchy-update-user-notify.path ]] &&
|
||||
fail "the retired migration watcher is back; pacman writing the migration directory during omarchy update would notify about migrations that update is already applying"
|
||||
grep -rlE '^(Path[A-Za-z]+|DirectoryNotEmpty)=.*/usr/share/omarchy/migrations' "$ROOT/default/systemd/user" >/dev/null 2>&1 &&
|
||||
fail "a user unit watches the migration directory again; the notifier must stay login-only"
|
||||
pass "no unit watches the migration directory, so package updates cannot trigger the notifier"
|
||||
|
||||
notify_service="$ROOT/default/systemd/user/omarchy-update-user-notify.service"
|
||||
! grep -q 'StartLimit' "$notify_service"
|
||||
notify_service="$ROOT/default/systemd/user/omarchy-migrate-notify.service"
|
||||
grep -Fx 'ExecStart=/usr/bin/omarchy-migrate-notify' "$notify_service" >/dev/null
|
||||
grep -Fx 'WantedBy=graphical-session.target' "$notify_service" >/dev/null
|
||||
pass "migration notifier keeps its start-rate limit and still runs once per login"
|
||||
pass "migration notifier only checks once per login"
|
||||
|
||||
grep -F 'omarchy-migrate-notify.service' "$first_run_units" >/dev/null ||
|
||||
fail "first-run does not enable the login migration notifier"
|
||||
grep -F 'omarchy-update-user-notify' "$first_run_units" >/dev/null &&
|
||||
fail "first-run still enables the retired notifier units"
|
||||
pass "first-run enables the login-only migration notifier"
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
require_command jq
|
||||
|
||||
run_node_test <<'JS'
|
||||
const tmux = requireFromRoot('shell/plugins/services/tmux/TmuxModel.js')
|
||||
|
||||
assertEqual(tmux.waitingFromOutput('{"count":2,"tooltip":"claude (Work:2)"}').count, 2, 'tmux model reads the waiting count')
|
||||
assertEqual(tmux.waitingFromOutput('{"count":2,"tooltip":"claude (Work:2)"}').tooltip, 'claude (Work:2)', 'tmux model reads the tooltip')
|
||||
assertEqual(tmux.waitingFromOutput('shell noise\n{"count":1,"tooltip":"editor (Work:1)"}').count, 1, 'tmux model ignores output before the json line')
|
||||
assertEqual(tmux.waitingFromOutput('').count, 0, 'tmux model treats empty output as nothing waiting')
|
||||
assertEqual(tmux.waitingFromOutput('not json').count, 0, 'tmux model treats unparseable output as nothing waiting')
|
||||
assertEqual(tmux.waitingFromOutput('not json').tooltip, '', 'tmux model blanks the tooltip on unparseable output')
|
||||
assertEqual(tmux.waitingFromOutput('{"count":-3}').count, 0, 'tmux model clamps a negative count')
|
||||
assertEqual(tmux.waitingFromOutput('{"tooltip":"editor (Work:1)"}').count, 0, 'tmux model defaults a missing count')
|
||||
JS
|
||||
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
cat >"$test_dir/tmux" <<'STUB'
|
||||
#!/bin/bash
|
||||
|
||||
if [[ $1 == "list-windows" ]]; then
|
||||
cat "$TMUX_STUB_WINDOWS"
|
||||
elif [[ $1 == "list-clients" ]]; then
|
||||
cat "$TMUX_STUB_CLIENTS"
|
||||
elif [[ $1 == "set-option" ]]; then
|
||||
printf '%s\n' "$*" >>"$TMUX_STUB_CALLS"
|
||||
fi
|
||||
STUB
|
||||
chmod +x "$test_dir/tmux"
|
||||
|
||||
cat >"$test_dir/omarchy-shell" <<'STUB'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$*" >>"$TMUX_STUB_CALLS"
|
||||
STUB
|
||||
chmod +x "$test_dir/omarchy-shell"
|
||||
|
||||
export TMUX_STUB_WINDOWS="$test_dir/windows"
|
||||
export TMUX_STUB_CLIENTS="$test_dir/clients"
|
||||
export TMUX_STUB_CALLS="$test_dir/calls"
|
||||
export PATH="$test_dir:$PATH"
|
||||
|
||||
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||
@1:000:100::Work:1:editor
|
||||
@2:100:110::Work:2:claude
|
||||
@3:001:120::Side|Gig:1:server: still going
|
||||
WINDOWS
|
||||
: >"$TMUX_STUB_CLIENTS"
|
||||
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
expected='{"count":2,"tooltip":"claude (Work:2), server: still going (Side|Gig:1)"}'
|
||||
[[ $output == "$expected" ]] || fail "tmux alert reports alerted windows" "expected: $expected"$'\n'"actual: $output"
|
||||
pass "tmux alert reports alerted windows"
|
||||
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show)
|
||||
[[ $output == "claude (Work:2), server: still going (Side|Gig:1)" ]] || fail "tmux alert describes alerted windows" "$output"
|
||||
pass "tmux alert describes alerted windows"
|
||||
|
||||
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||
@1:000:200:100:Work:1:editor
|
||||
WINDOWS
|
||||
echo 'attached,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
|
||||
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
expected='{"count":1,"tooltip":"editor (Work:1)"}'
|
||||
[[ $output == "$expected" ]] || fail "tmux alert reports output in an unfocused active window" "expected: $expected"$'\n'"actual: $output"
|
||||
pass "tmux alert reports output in an unfocused active window"
|
||||
|
||||
echo 'attached,focused,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert ignores output in a focused active window" "$output"
|
||||
pass "tmux alert ignores output in a focused active window"
|
||||
|
||||
cat >"$TMUX_STUB_CLIENTS" <<'CLIENTS'
|
||||
attached,UTF-8:@1
|
||||
attached,focused,UTF-8:@1
|
||||
CLIENTS
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert ignores a window visible in another focused client" "$output"
|
||||
pass "tmux alert ignores a window visible in another focused client"
|
||||
|
||||
echo 'attached,UTF-8:@1' >"$TMUX_STUB_CLIENTS"
|
||||
cat >"$TMUX_STUB_WINDOWS" <<'WINDOWS'
|
||||
@1:000:200:200:Work:1:editor
|
||||
WINDOWS
|
||||
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert reports no alerted windows" "$output"
|
||||
pass "tmux alert reports no alerted windows"
|
||||
|
||||
[[ -z $("$ROOT/bin/omarchy-tmux-alert" show) ]] || fail "tmux alert stays quiet without alerts"
|
||||
pass "tmux alert stays quiet without alerts"
|
||||
|
||||
: >"$TMUX_STUB_WINDOWS"
|
||||
: >"$TMUX_STUB_CLIENTS"
|
||||
output=$("$ROOT/bin/omarchy-tmux-alert" show --json)
|
||||
[[ $output == '{"count":0,"tooltip":""}' ]] || fail "tmux alert handles a missing tmux server" "$output"
|
||||
pass "tmux alert handles a missing tmux server"
|
||||
|
||||
: >"$TMUX_STUB_CALLS"
|
||||
"$ROOT/bin/omarchy-tmux-alert" track @3 345
|
||||
expected=$'set-option -wq -t @3 @omarchy_unfocused_activity 345\n-q omarchy.indicators refresh'
|
||||
output=$(cat "$TMUX_STUB_CALLS")
|
||||
[[ $output == "$expected" ]] || fail "tmux alert records the activity watermark and refreshes indicators" "expected: $expected"$'\n'"actual: $output"
|
||||
pass "tmux alert records the activity watermark and refreshes indicators"
|
||||
|
||||
: >"$TMUX_STUB_CALLS"
|
||||
"$ROOT/bin/omarchy-tmux-alert" track invalid nope
|
||||
[[ ! -s $TMUX_STUB_CALLS ]] || fail "tmux alert ignores invalid tracking arguments" "$(cat "$TMUX_STUB_CALLS")"
|
||||
pass "tmux alert ignores invalid tracking arguments"
|
||||
@@ -0,0 +1,54 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
source "$(dirname "$0")/base-test.sh"
|
||||
|
||||
migration="$ROOT/migrations/1784955584.sh"
|
||||
test_dir=$(mktemp -d)
|
||||
trap 'rm -rf "$test_dir"' EXIT
|
||||
|
||||
mkdir -p "$test_dir/home/.config/tmux" "$test_dir/bin"
|
||||
|
||||
cat >"$test_dir/home/.config/tmux/tmux.conf" <<'EOF'
|
||||
# Alerts
|
||||
set-hook -g alert-bell 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||
set-hook -g after-select-window 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||
set-hook -g client-session-changed 'run-shell -b "omarchy-shell -q omarchy.indicators refresh"'
|
||||
set-hook -g client-focus-out[42] 'display-message "custom focus hook"'
|
||||
EOF
|
||||
|
||||
cat >"$test_dir/bin/omarchy-restart-tmux" <<'EOF'
|
||||
#!/bin/bash
|
||||
|
||||
echo restart >>"$TMUX_MIGRATION_RESTART_LOG"
|
||||
EOF
|
||||
chmod +x "$test_dir/bin/omarchy-restart-tmux"
|
||||
|
||||
export TMUX_MIGRATION_RESTART_LOG="$test_dir/restarts"
|
||||
tmux_config="$test_dir/home/.config/tmux/tmux.conf"
|
||||
|
||||
HOME="$test_dir/home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
|
||||
|
||||
grep -Fq 'after-select-window '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
|
||||
fail "tmux activity migration tracks newly selected windows"
|
||||
grep -Fq 'client-session-changed '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
|
||||
fail "tmux activity migration tracks session changes"
|
||||
grep -Fq 'client-focus-out[100] '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
|
||||
fail "tmux activity migration tracks terminal focus loss"
|
||||
grep -Fq 'client-focus-in[100] '"'"'run-shell -b "omarchy-tmux-alert track #{window_id} #{window_activity}"'"'" "$tmux_config" ||
|
||||
fail "tmux activity migration tracks terminal focus gain"
|
||||
grep -Fq 'client-focus-out[42] '"'"'display-message "custom focus hook"'"'" "$tmux_config" ||
|
||||
fail "tmux activity migration preserves custom indexed focus hooks"
|
||||
restart_count=$(wc -l <"$TMUX_MIGRATION_RESTART_LOG")
|
||||
((restart_count == 1)) || fail "tmux activity migration reloads tmux once"
|
||||
pass "tmux activity migration installs focus-aware hooks"
|
||||
|
||||
before=$(sha256sum "$tmux_config")
|
||||
HOME="$test_dir/home" PATH="$test_dir/bin:$PATH" bash -euo pipefail "$migration" >/dev/null
|
||||
after=$(sha256sum "$tmux_config")
|
||||
|
||||
[[ $before == "$after" ]] || fail "tmux activity migration is idempotent"
|
||||
restart_count=$(wc -l <"$TMUX_MIGRATION_RESTART_LOG")
|
||||
((restart_count == 1)) || fail "idempotent tmux activity migration does not reload tmux"
|
||||
pass "tmux activity migration is idempotent"
|
||||
@@ -8,6 +8,7 @@ test_tmp=$(mktemp -d)
|
||||
trap 'rm -rf "$test_tmp"' EXIT
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
git_log="$test_tmp/git.log"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/checkupdates" <<'SH'
|
||||
@@ -52,8 +53,48 @@ exit 0
|
||||
SH
|
||||
chmod +x "$stub_bin/pacman"
|
||||
|
||||
cat >"$stub_bin/git" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$*" >>"$TEST_GIT_LOG"
|
||||
|
||||
[[ $1 == "-C" ]] || exit 1
|
||||
shift 2
|
||||
|
||||
case "$1" in
|
||||
fetch)
|
||||
[[ ${TEST_GIT_FETCH:-ok} == "ok" ]]
|
||||
;;
|
||||
rev-parse)
|
||||
case "$2" in
|
||||
--is-inside-work-tree)
|
||||
[[ ${TEST_GIT_CHECKOUT:-yes} == "yes" ]] || exit 1
|
||||
echo true
|
||||
;;
|
||||
--abbrev-ref)
|
||||
[[ ${TEST_GIT_UPSTREAM:-origin/quattro} != "none" ]] || exit 1
|
||||
echo "${TEST_GIT_UPSTREAM:-origin/quattro}"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
rev-list)
|
||||
echo "${TEST_GIT_BEHIND:-0}"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$stub_bin/git"
|
||||
|
||||
run_checker() {
|
||||
PATH="$stub_bin:$PATH" "$ROOT/bin/omarchy-update-available"
|
||||
OMARCHY_PATH="${TEST_OMARCHY_PATH:-/usr/share/omarchy}" \
|
||||
TEST_GIT_LOG="$git_log" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-update-available"
|
||||
}
|
||||
|
||||
capture_checker() {
|
||||
@@ -124,3 +165,49 @@ fi
|
||||
[[ $status -eq 1 ]] || fail "update checker exits non-zero when no updates are available"
|
||||
grep -q '^Omarchy is up to date$' "$stdout" || fail "update checker prints up-to-date message"
|
||||
pass "update checker reports up-to-date Omarchy packages"
|
||||
|
||||
: >"$git_log"
|
||||
if capture_checker "$stdout" "$stderr" \
|
||||
TEST_CHECKUPDATES=none \
|
||||
TEST_INSTALLED_PACKAGE=none \
|
||||
TEST_OMARCHY_PATH="$test_tmp/checkout" \
|
||||
TEST_GIT_BEHIND=2; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
[[ $status -eq 0 ]] || fail "update checker exits successfully when dev commits are available"
|
||||
grep -Fx 'omarchy-dev-checkout 2 new commits on origin/quattro' "$stdout" >/dev/null ||
|
||||
fail "update checker reports available dev commits" "$(cat "$stdout")"
|
||||
grep -Fx -- "-C $test_tmp/checkout fetch --quiet" "$git_log" >/dev/null ||
|
||||
fail "update checker fetches the dev checkout upstream" "$(cat "$git_log")"
|
||||
pass "update checker detects new commits in the dev checkout"
|
||||
|
||||
if capture_checker "$stdout" "$stderr" \
|
||||
TEST_CHECKUPDATES=none \
|
||||
TEST_INSTALLED_PACKAGE=none \
|
||||
TEST_OMARCHY_PATH="$test_tmp/checkout" \
|
||||
TEST_GIT_BEHIND=0; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
[[ $status -eq 1 ]] || fail "update checker exits non-zero when the dev checkout is current"
|
||||
grep -q '^Omarchy is up to date$' "$stdout" || fail "update checker reports a current dev checkout"
|
||||
pass "update checker ignores a current dev checkout"
|
||||
|
||||
if capture_checker "$stdout" "$stderr" \
|
||||
TEST_CHECKUPDATES=none \
|
||||
TEST_INSTALLED_PACKAGE=none \
|
||||
TEST_OMARCHY_PATH="$test_tmp/checkout" \
|
||||
TEST_GIT_BEHIND=1 \
|
||||
TEST_GIT_FETCH=fail; then
|
||||
status=0
|
||||
else
|
||||
status=$?
|
||||
fi
|
||||
[[ $status -eq 0 ]] || fail "update checker uses cached upstream state when fetch fails"
|
||||
grep -Fx 'omarchy-dev-checkout 1 new commit on origin/quattro' "$stdout" >/dev/null ||
|
||||
fail "update checker reports cached dev commits after a fetch failure" "$(cat "$stdout")"
|
||||
[[ ! -s $stderr ]] || fail "update checker keeps dev fetch failures quiet" "$(cat "$stderr")"
|
||||
pass "update checker uses cached dev state when fetching is unavailable"
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
#!/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
|
||||
|
||||
stub_bin="$test_tmp/bin"
|
||||
git_log="$test_tmp/git.log"
|
||||
checkout="$test_tmp/checkout"
|
||||
mkdir -p "$stub_bin" "$checkout"
|
||||
|
||||
cat >"$stub_bin/git" <<'SH'
|
||||
#!/bin/bash
|
||||
|
||||
printf '%s\n' "$*" >>"$TEST_GIT_LOG"
|
||||
|
||||
[[ $1 == "-C" ]] || exit 1
|
||||
shift 2
|
||||
|
||||
case "$1" in
|
||||
rev-parse)
|
||||
case "$2" in
|
||||
--is-inside-work-tree)
|
||||
[[ ${TEST_GIT_CHECKOUT:-yes} == "yes" ]] || exit 1
|
||||
echo true
|
||||
;;
|
||||
--abbrev-ref)
|
||||
[[ ${TEST_GIT_UPSTREAM:-origin/quattro} != "none" ]] || exit 1
|
||||
echo "${TEST_GIT_UPSTREAM:-origin/quattro}"
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
;;
|
||||
pull)
|
||||
[[ $2 == "--ff-only" ]]
|
||||
;;
|
||||
*)
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
SH
|
||||
chmod +x "$stub_bin/git"
|
||||
|
||||
run_dev_update() {
|
||||
OMARCHY_PATH="$1" \
|
||||
TEST_GIT_LOG="$git_log" \
|
||||
PATH="$stub_bin:$PATH" \
|
||||
"$ROOT/bin/omarchy-update-dev"
|
||||
}
|
||||
|
||||
: >"$git_log"
|
||||
run_dev_update /usr/share/omarchy
|
||||
[[ ! -s $git_log ]] || fail "package-backed updates do not invoke git" "$(cat "$git_log")"
|
||||
pass "package-backed updates skip the dev checkout step"
|
||||
|
||||
: >"$git_log"
|
||||
run_dev_update "$checkout"
|
||||
grep -Fx -- "-C $checkout pull --ff-only" "$git_log" >/dev/null ||
|
||||
fail "dev checkout update pulls its upstream with fast-forward only" "$(cat "$git_log")"
|
||||
pass "dev checkout update pulls its configured upstream"
|
||||
|
||||
: >"$git_log"
|
||||
TEST_GIT_UPSTREAM=none run_dev_update "$checkout"
|
||||
if grep -q ' pull ' "$git_log"; then
|
||||
fail "dev checkout without an upstream is not pulled" "$(cat "$git_log")"
|
||||
fi
|
||||
pass "dev checkout without an upstream is skipped safely"
|
||||
|
||||
: >"$git_log"
|
||||
if TEST_GIT_CHECKOUT=no run_dev_update "$checkout" >"$test_tmp/invalid.out" 2>"$test_tmp/invalid.err"; then
|
||||
fail "invalid dev checkout fails the update"
|
||||
fi
|
||||
grep -F "OMARCHY_PATH is not a git checkout: $checkout" "$test_tmp/invalid.err" >/dev/null ||
|
||||
fail "invalid dev checkout reports the configured path" "$(cat "$test_tmp/invalid.err")"
|
||||
pass "invalid dev checkout fails with a useful error"
|
||||
|
||||
grep -qE '^ *omarchy-update-dev$' "$ROOT/bin/omarchy-update" ||
|
||||
fail "top-level update includes the dev checkout step"
|
||||
pass "top-level update includes the dev checkout step"
|
||||
@@ -34,6 +34,7 @@ SH
|
||||
for command in \
|
||||
omarchy-toggle-idle \
|
||||
systemd-inhibit \
|
||||
omarchy-update-dev \
|
||||
omarchy-update-keyring \
|
||||
omarchy-update-system-pkgs \
|
||||
omarchy-migrate \
|
||||
@@ -99,3 +100,36 @@ wait "$perform_pid"
|
||||
grep -q "already running" "$test_tmp/perform-second.out" || fail "second omarchy-update-perform reports held update lock"
|
||||
[[ ! -f $test_tmp/perform-second-started ]] || fail "second omarchy-update-perform did not snapshot while lock was held"
|
||||
pass "omarchy-update-perform compatibility wrapper respects update lock"
|
||||
|
||||
# Update-owned Stay Awake state must be cleared before the restart helper can
|
||||
# reboot the machine, rather than relying on an EXIT trap during shutdown.
|
||||
write_stub omarchy-snapshot 'exit 0'
|
||||
write_stub omarchy-update-keyring 'exit 0'
|
||||
write_stub omarchy-toggle-idle '
|
||||
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
|
||||
case "$1" in
|
||||
stay-awake)
|
||||
mkdir -p "$(dirname "$state_file")"
|
||||
touch "$state_file"
|
||||
;;
|
||||
allow-idle)
|
||||
rm -f "$state_file"
|
||||
;;
|
||||
esac'
|
||||
write_stub omarchy-update-restart '
|
||||
state_file="$HOME/.local/state/omarchy/indicators/stay-awake"
|
||||
if [[ ${EXPECT_STAY_AWAKE:-0} == "1" ]]; then
|
||||
[[ -f $state_file ]]
|
||||
else
|
||||
[[ ! -f $state_file ]]
|
||||
fi'
|
||||
|
||||
rm -f "$test_home/.local/state/omarchy/indicators/stay-awake"
|
||||
OMARCHY_UPDATE_LOGGED=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y
|
||||
[[ ! -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update clears its Stay Awake state before restart handling"
|
||||
|
||||
mkdir -p "$test_home/.local/state/omarchy/indicators"
|
||||
touch "$test_home/.local/state/omarchy/indicators/stay-awake"
|
||||
OMARCHY_UPDATE_LOGGED=1 EXPECT_STAY_AWAKE=1 run_with_lock_env "$ROOT/bin/omarchy-update" -y
|
||||
[[ -f $test_home/.local/state/omarchy/indicators/stay-awake ]] || fail "update preserves pre-existing Stay Awake state"
|
||||
pass "omarchy-update restores only its own Stay Awake state before restart handling"
|
||||
|
||||
@@ -32,10 +32,13 @@ cat >"$test_bin/systemd-run" <<'EOF'
|
||||
#!/bin/bash
|
||||
echo "systemd-run:$*" >>"$TEST_LOG"
|
||||
while (($# > 0)); do
|
||||
[[ $1 == "bash" ]] && exec "$@"
|
||||
shift
|
||||
case $1 in
|
||||
-p) shift 2 ;;
|
||||
-*) shift ;;
|
||||
*) break ;;
|
||||
esac
|
||||
done
|
||||
exit 1
|
||||
exec "$@"
|
||||
EOF
|
||||
chmod +x "$test_bin/systemd-run"
|
||||
|
||||
@@ -48,15 +51,18 @@ run_invitation_hook
|
||||
|
||||
[[ -f $test_home/.local/state/omarchy/done/voxtype-install-invitation ]] || fail "Voxtype invitation records completion"
|
||||
[[ -f $hook_path ]] || fail "Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 1 ]] || fail "Voxtype invitation uses a durable user service"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "Voxtype invitation uses durable user services"
|
||||
grep -q -- '--user --collect --quiet --service-type=exec --unit=omarchy-voxtype-install-invitation' "$log_file" || fail "Voxtype invitation configures its user service"
|
||||
# KillMode=process keeps the launcher's setsid child alive once the short-lived
|
||||
# main process exits, otherwise the install terminal never appears.
|
||||
grep -q -- '--user --collect --quiet -p KillMode=process --unit=omarchy-voxtype-install ' "$log_file" || fail "Voxtype invitation outlives its launcher unit"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "Voxtype invitation sends one notification"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "Voxtype invitation handles the notification action"
|
||||
|
||||
HOME="$test_home" PATH="$test_bin:$ROOT/bin:$PATH" TEST_LOG="$log_file" bash "$hook_path"
|
||||
|
||||
[[ -f $hook_path ]] || fail "completed Voxtype invitation keeps its hook installed"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation does not schedule again"
|
||||
[[ $(grep -c '^systemd-run:' "$log_file") -eq 2 ]] || fail "completed Voxtype invitation does not schedule again"
|
||||
[[ $(grep -c '^notification$' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation hook does not notify again"
|
||||
[[ $(grep -c '^launch$' "$log_file") -eq 1 ]] || fail "completed Voxtype invitation hook does not launch again"
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
migration=$(grep -rl 'Move zram tuning to a vendor drop-in' "$ROOT/migrations" | head -n 1 || true)
|
||||
[[ -n $migration ]] || fail "zram drop-in migration exists"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
# The migration shells out to pacman (ownership check) and sudo (removal).
|
||||
# Stub both so the test never touches the real system, and let each case pick
|
||||
# what `pacman -Qo` reports through PACMAN_OWNS.
|
||||
stub_bin="$TMPDIR/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/pacman" <<'STUB'
|
||||
#!/bin/bash
|
||||
[[ ${PACMAN_OWNS:-0} == 1 ]]
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin/pacman" "$stub_bin/sudo"
|
||||
|
||||
# The migration removes the /etc copy only once the drop-in that replaces it is
|
||||
# installed. Point that at a fixture so the result does not depend on whether
|
||||
# the machine running the tests happens to carry the real one.
|
||||
dropin="$TMPDIR/90-omarchy.conf"
|
||||
: >"$dropin"
|
||||
|
||||
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
|
||||
# whole chain on a non-zero exit, so match that invocation exactly.
|
||||
run_migration() {
|
||||
local conf="$1"
|
||||
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
|
||||
bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean for $(basename "$conf")"
|
||||
}
|
||||
|
||||
# archinstall's own output: a [zram0] section with nothing but the algorithm.
|
||||
conf="$TMPDIR/archinstall.conf"
|
||||
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
||||
run_migration "$conf"
|
||||
[[ -f $conf ]] && fail "migration removes archinstall's generated config"
|
||||
pass "migration removes archinstall's generated config"
|
||||
|
||||
# Same shape, different algorithm, plus comments and blank lines.
|
||||
conf="$TMPDIR/commented.conf"
|
||||
printf '# written by archinstall\n\n[zram0]\ncompression-algorithm = lz4\n\n' >"$conf"
|
||||
run_migration "$conf"
|
||||
[[ -f $conf ]] && fail "migration ignores comments and a non-zstd algorithm"
|
||||
pass "migration ignores comments and a non-zstd algorithm"
|
||||
|
||||
# A config that sets nothing decides nothing, and must not take the migration
|
||||
# chain down with it.
|
||||
conf="$TMPDIR/comments-only.conf"
|
||||
printf '# nothing to see here\n\n' >"$conf"
|
||||
run_migration "$conf"
|
||||
[[ -f $conf ]] && fail "migration removes a config that sets nothing"
|
||||
pass "migration removes a config that sets nothing"
|
||||
|
||||
conf="$TMPDIR/empty.conf"
|
||||
: >"$conf"
|
||||
run_migration "$conf"
|
||||
[[ -f $conf ]] && fail "migration removes an empty config"
|
||||
pass "migration removes an empty config"
|
||||
|
||||
# A local override must survive.
|
||||
conf="$TMPDIR/local.conf"
|
||||
printf '[zram0]\ncompression-algorithm = zstd\nzram-size = ram / 4\n' >"$conf"
|
||||
run_migration "$conf"
|
||||
[[ -f $conf ]] || fail "migration keeps a locally edited config"
|
||||
pass "migration keeps a locally edited config"
|
||||
|
||||
# Package-owned copies go away with their package; the migration must not touch
|
||||
# them.
|
||||
conf="$TMPDIR/owned.conf"
|
||||
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
||||
PATH="$stub_bin:$PATH" PACMAN_OWNS=1 OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$dropin" \
|
||||
bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean for a package-owned config"
|
||||
[[ -f $conf ]] || fail "migration keeps a package-owned config"
|
||||
pass "migration keeps a package-owned config"
|
||||
|
||||
# Nothing to do, and running twice must stay clean.
|
||||
conf="$TMPDIR/absent.conf"
|
||||
run_migration "$conf"
|
||||
run_migration "$conf"
|
||||
pass "migration no-ops when the config is already gone"
|
||||
|
||||
# Without the drop-in installed, the /etc copy is the only thing configuring
|
||||
# zram at all. Removing it would leave the machine with no zram device, so the
|
||||
# migration has to leave it alone and stay clean doing it.
|
||||
conf="$TMPDIR/no-dropin.conf"
|
||||
printf '[zram0]\ncompression-algorithm = zstd\n' >"$conf"
|
||||
PATH="$stub_bin:$PATH" OMARCHY_ZRAM_CONF="$conf" OMARCHY_ZRAM_DROPIN="$TMPDIR/absent-dropin.conf" \
|
||||
bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean when the drop-in is missing"
|
||||
[[ -f $conf ]] || fail "migration keeps the config when the drop-in is missing"
|
||||
pass "migration keeps the config until the drop-in is installed"
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/bin/bash
|
||||
|
||||
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
|
||||
|
||||
require_command /usr/lib/systemd/system-generators/zram-generator
|
||||
|
||||
migration=$(grep -rl 'Resize zram to match the shipped config' "$ROOT/migrations" | head -n 1 || true)
|
||||
[[ -n $migration ]] || fail "zram resize migration exists"
|
||||
|
||||
TMPDIR=$(mktemp -d)
|
||||
trap 'rm -rf "$TMPDIR"' EXIT
|
||||
|
||||
# The migration shells out to sudo, systemctl and omarchy-state. Stub all three
|
||||
# so the test never touches the real system, and record what each run did.
|
||||
stub_bin="$TMPDIR/bin"
|
||||
mkdir -p "$stub_bin"
|
||||
|
||||
cat >"$stub_bin/sudo" <<'STUB'
|
||||
#!/bin/bash
|
||||
exec "$@"
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/systemctl" <<'STUB'
|
||||
#!/bin/bash
|
||||
echo "systemctl $*" >>"$ACTIONS"
|
||||
[[ ${SYSTEMCTL_FAIL:-0} == 1 ]] && exit 1
|
||||
exit 0
|
||||
STUB
|
||||
|
||||
cat >"$stub_bin/omarchy-state" <<'STUB'
|
||||
#!/bin/bash
|
||||
echo "omarchy-state $*" >>"$ACTIONS"
|
||||
STUB
|
||||
|
||||
chmod +x "$stub_bin/sudo" "$stub_bin/systemctl" "$stub_bin/omarchy-state"
|
||||
|
||||
# ZRAM_GENERATOR_ROOT redirects both the config search and /proc/meminfo, so the
|
||||
# migration's own generator call resolves against this fixture instead of the
|
||||
# host. 16G of RAM against the shipped config is a 8192MB device.
|
||||
gen_root="$TMPDIR/genroot"
|
||||
mkdir -p "$gen_root/usr/lib/systemd/zram-generator.conf.d" "$gen_root/proc"
|
||||
cp "$ROOT/default/systemd/zram-generator.conf.d/90-omarchy.conf" \
|
||||
"$gen_root/usr/lib/systemd/zram-generator.conf.d/"
|
||||
printf 'MemTotal: %d kB\n' $((16 * 1024 * 1024)) >"$gen_root/proc/meminfo"
|
||||
|
||||
desired_bytes=$((8192 * 1024 * 1024))
|
||||
|
||||
# omarchy-migrate runs each migration with `bash -euo pipefail` and stops the
|
||||
# whole chain on a non-zero exit, so match that invocation exactly.
|
||||
run_migration() {
|
||||
local disksize="$1" used="$2" fail_reload="${3:-0}"
|
||||
|
||||
printf '%s' "$disksize" >"$TMPDIR/disksize"
|
||||
printf 'Filename\tType\tSize\tUsed\tPriority\n' >"$TMPDIR/swaps"
|
||||
[[ -n $used ]] &&
|
||||
printf '/dev/zram0 partition 8388604 %s 100\n' "$used" >>"$TMPDIR/swaps"
|
||||
|
||||
: >"$TMPDIR/actions"
|
||||
|
||||
PATH="$stub_bin:$PATH" \
|
||||
ACTIONS="$TMPDIR/actions" \
|
||||
SYSTEMCTL_FAIL="$fail_reload" \
|
||||
ZRAM_GENERATOR_ROOT="$gen_root" \
|
||||
OMARCHY_ZRAM_DISKSIZE="$TMPDIR/disksize" \
|
||||
OMARCHY_SWAPS="$TMPDIR/swaps" \
|
||||
bash -euo pipefail "$migration" >/dev/null ||
|
||||
fail "migration exits clean (disksize=$disksize used=$used reload_fail=$fail_reload)"
|
||||
}
|
||||
|
||||
did() { grep -qF "$1" "$TMPDIR/actions"; }
|
||||
|
||||
# Already the size the config asks for: the device is correct however it got
|
||||
# there, so the migration must not restart it or ask for a reboot.
|
||||
run_migration "$desired_bytes" 4193612
|
||||
[[ -s $TMPDIR/actions ]] && fail "correctly sized device is left alone" \
|
||||
"expected no privileged calls, got: $(cat "$TMPDIR/actions")"
|
||||
pass "correctly sized device is left alone"
|
||||
pass "correctly sized device asks for no reboot"
|
||||
|
||||
# Right size but still in use, and the reload never even happens.
|
||||
run_migration "$desired_bytes" 0
|
||||
did "systemctl restart" && fail "correctly sized empty device is left alone"
|
||||
pass "correctly sized empty device is left alone"
|
||||
|
||||
# Wrong size and empty: safe to resize now.
|
||||
run_migration $((4096 * 1024 * 1024)) 0
|
||||
did "systemctl restart dev-zram0.swap" || fail "empty device is restarted"
|
||||
did "omarchy-state set reboot-required" && fail "empty device asks for no reboot"
|
||||
pass "empty device is restarted"
|
||||
pass "empty device asks for no reboot"
|
||||
|
||||
# Wrong size with pages stored: resizing would fault them all back in, so defer.
|
||||
run_migration $((4096 * 1024 * 1024)) 4193612
|
||||
did "systemctl restart" && fail "device in use is not restarted"
|
||||
did "omarchy-state set reboot-required" || fail "device in use asks for a reboot"
|
||||
pass "device in use is not restarted"
|
||||
pass "device in use asks for a reboot"
|
||||
|
||||
# No zram device at all reads as empty, and the restart brings it up.
|
||||
run_migration "" ""
|
||||
did "systemctl restart dev-zram0.swap" || fail "absent device is created"
|
||||
pass "absent device is created"
|
||||
|
||||
# A failed daemon-reload must fall back to asking for a reboot.
|
||||
run_migration $((4096 * 1024 * 1024)) 0 1
|
||||
did "systemctl restart" && fail "failed reload does not restart"
|
||||
did "omarchy-state set reboot-required" || fail "failed reload asks for a reboot"
|
||||
pass "failed reload does not restart"
|
||||
pass "failed reload asks for a reboot"
|
||||
Reference in New Issue
Block a user