Merge quattro into USB authorization branch

This commit is contained in:
acrogenesis committed 2026-09-21 08:23:51 -06:00
commit 27bbbb2fe9
121 files changed
+2208 -1118

No files matched your search

+15 -1
View File
@@ -24,6 +24,20 @@ verify_core_packages() {
pass "all Omarchy core packages are installed (${#missing[@]} missing)"
}
verify_kernel_headers() {
local kernel=linux-omarchy
local release
release=$(uname -r)
omarchy-pkg-present linux-t2 && kernel=linux-t2
[[ $(cat "/usr/lib/modules/$release/pkgbase") == "$kernel" ]] ||
fail "the installed system boots the supported kernel" "$release is not $kernel"
omarchy-pkg-present "$kernel-headers" || fail "kernel headers are installed" "$kernel-headers is missing"
[[ $(cat "/usr/lib/modules/$release/build/include/config/kernel.release") == "$release" ]] ||
fail "headers match the running kernel" "$release has missing or mismatched headers"
pass "the running $kernel kernel has matching headers ($release)"
}
verify_defaults() {
[[ $(omarchy-default-browser) == "chromium" ]] || fail "Chromium is the default browser"
pass "Chromium is the default browser"
@@ -112,7 +126,7 @@ verify_user_setup() {
pass "Omarchy user state and shell configuration exist"
}
for check in verify_core_packages verify_defaults verify_services verify_runtime_tools verify_user_setup; do
for check in verify_core_packages verify_kernel_headers verify_defaults verify_services verify_runtime_tools verify_user_setup; do
if ! ("$check"); then
status=1
fi
+17 -2
View File
@@ -21,16 +21,31 @@ fi
# Keep going after a failing file. A test file exits at its first failed
# assertion, so aborting the run there too would hide every file after it --
# one packaging failure was masking 114 of 134 files.
output=$(mktemp)
trap 'rm -f "$output"' EXIT
failed=()
skipped=()
for test in "${tests[@]}"; do
printf '==> %s\n' "${test#$ROOT/}"
bash "$test" || failed+=("${test#$ROOT/}")
bash "$test" | tee "$output" || failed+=("${test#$ROOT/}")
if grep -q '^ok - .* # SKIP$' "$output"; then
skipped+=("${test#$ROOT/}")
fi
done
if (( ${#skipped[@]} > 0 )); then
printf '\nSkipped checks in %d of %d test files:\n' "${#skipped[@]}" "${#tests[@]}"
printf ' %s\n' "${skipped[@]}"
fi
if (( ${#failed[@]} > 0 )); then
printf '\n%d of %d test files failed:\n' "${#failed[@]}" "${#tests[@]}" >&2
printf ' %s\n' "${failed[@]}" >&2
exit 1
fi
printf '\nAll %d test files passed.\n' "${#tests[@]}"
if (( ${#skipped[@]} == 0 )); then
printf '\nAll %d test files passed.\n' "${#tests[@]}"
else
printf '\n%d test files completed without failures; %d had skipped checks.\n' "${#tests[@]}" "${#skipped[@]}"
fi
+1 -1
View File
@@ -43,7 +43,7 @@ if (( EUID != 0 )); then
fi
if (( ! root_runtime_available )); then
pass "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
skip "no unprivileged user namespace; skipping the root lock-helper lookup matrix"
exit 0
fi
+1 -1
View File
@@ -7,7 +7,7 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_compositor "bar icon geometry test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping bar icon geometry test"
skip "quickshell not installed; skipping bar icon geometry test"
exit 0
fi
+1 -1
View File
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "bar widget contract test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping bar widget contract test"
skip "quickshell not installed; skipping bar widget contract test"
exit 0
fi
+5 -1
View File
@@ -14,6 +14,10 @@ pass() {
printf 'ok - %s\n' "$1"
}
skip() {
printf 'ok - %s # SKIP\n' "$1"
}
fail() {
local description="$1"
local detail="${2:-}"
@@ -72,7 +76,7 @@ require_compositor() {
return 0
fi
pass "no Wayland compositor; skipping $description"
skip "no Wayland compositor; skipping $description"
exit 0
}
@@ -123,7 +123,7 @@ if [[ -n $real_hiddev ]]; then
fail "wrapper did not trust a valid cached hiddev node: $real_hiddev"
pass "wrapper trusts a cached hiddev character device without re-detecting"
else
pass "no /dev/hiddev* character device present; skipping the valid-cache case"
skip "no /dev/hiddev* character device present; skipping the valid-cache case"
fi
# --- With no XDG_RUNTIME_DIR, the predictable /tmp cache is not consulted ------
@@ -148,5 +148,5 @@ if mkfifo "$tmp_cache" 2>/dev/null; then
"it blocked reading the FIFO decoy at $tmp_cache"
pass "wrapper ignores the /tmp cache path when XDG_RUNTIME_DIR is unset"
else
pass "$tmp_cache already present or not safely creatable; skipping the /tmp-fallback case"
skip "$tmp_cache already present or not safely creatable; skipping the /tmp-fallback case"
fi
+1 -1
View File
@@ -46,7 +46,7 @@ mode=$(stat -c '%a' "$write_dir/color.json")
pass "theme colour writes a 0644 color.json"
if (( EUID == 0 )); then
pass "running as root; skipping the mktemp-failure check"
skip "running as root; skipping the mktemp-failure check"
else
chmod u+w "$write_dir"
export TMPDIR=$test_tmp/missing-tmp
+1 -1
View File
@@ -82,7 +82,7 @@ SH
chmod +x "$stub_bin/sudo"
if ((EUID == 0)); then
pass "running as root; skipping the elevation checks, which would rewrite this machine's browser policy"
skip "running as root; skipping the elevation checks, which would rewrite this machine's browser policy"
else
elevation_for() {
: >"$test_tmp/elevation"
+1 -1
View File
@@ -20,7 +20,7 @@ JS
require_compositor "Button hover geometry runtime test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping Button hover geometry runtime test"
skip "quickshell not installed; skipping Button hover geometry runtime test"
exit 0
fi
+50
View File
@@ -0,0 +1,50 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Exercise the privileged installer without writing to the host's /usr/share.
if ! command -v bwrap >/dev/null || ! bwrap --ro-bind / / --unshare-user --uid 0 --gid 0 true 2>/dev/null; then
skip "user namespaces unavailable; skipping isolated Claude extension installation"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/share" "$test_tmp/bin"
installer="$ROOT/bin/omarchy-install-chromium-claude"
extension_id=fcoeoabgfenejglbffodgkkbkcdhcgfn
sandbox=(bwrap --ro-bind / / --bind "$test_tmp" "$test_tmp" --dev /dev --proc /proc --unshare-user)
"${sandbox[@]}" --uid 0 --gid 0 --bind "$test_tmp/share" /usr/share bash "$installer"
for browser in chromium google-chrome microsoft-edge; do
file="$test_tmp/share/$browser/extensions/$extension_id.json"
jq -e '.external_update_url == "https://clients2.google.com/service/update2/crx"' "$file" >/dev/null ||
fail "$browser registers the official Claude Web Store extension"
[[ $(stat -c '%a' "$file") == "644" ]] || fail "$browser extension registration is readable"
done
pass "Claude extension installer registers all supported browser families"
cat >"$test_tmp/bin/pkexec" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$AUTH_LOG"
exit 42
SH
chmod +x "$test_tmp/bin/pkexec"
export AUTH_LOG="$test_tmp/auth-log"
export PATH="$test_tmp/bin:$PATH"
# A read-only /usr/share and a failing auth stub prove that a repeated run
# neither rewrites the files nor requests authentication.
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null
[[ ! -e $AUTH_LOG ]] || fail "configured Claude extensions need no authentication"
pass "configured Claude extensions need neither writes nor authentication"
rm "$test_tmp/share/google-chrome/extensions/$extension_id.json"
status=0
"${sandbox[@]}" --uid 1000 --gid 1000 --ro-bind "$test_tmp/share" /usr/share bash "$installer" </dev/null || status=$?
[[ $status == 42 ]] || fail "Claude extension installer propagates authentication failure"
[[ $(cat "$AUTH_LOG") == "/usr/bin/omarchy-install-chromium-claude" ]] ||
fail "menu installation elevates only the packaged installer"
pass "missing registration uses pkexec and propagates authentication failure"
+6 -6
View File
@@ -277,12 +277,12 @@ printf '%s\n' "$1" >"$EDITOR_PATH_OUT"
cat "$1" >"$EDITOR_TEXT_OUT"
SH
cat >"$TMPDIR/bin/tensaku-edit" <<'SH'
cat >"$TMPDIR/bin/omasnap" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >"$TENSAKU_OUT"
printf '%s\n' "$*" >"$OMASNAP_OUT"
SH
chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wl-paste" "$TMPDIR/bin/wtype" "$TMPDIR/bin/omarchy-launch-browser" "$TMPDIR/bin/omarchy-launch-editor" "$TMPDIR/bin/tensaku-edit"
chmod +x "$TMPDIR/bin/wl-copy" "$TMPDIR/bin/wl-paste" "$TMPDIR/bin/wtype" "$TMPDIR/bin/omarchy-launch-browser" "$TMPDIR/bin/omarchy-launch-editor" "$TMPDIR/bin/omasnap"
capture_output=$(XDG_RUNTIME_DIR="$TMPDIR" XDG_STATE_HOME="$TMPDIR/state" PATH="$TMPDIR/bin:$PATH" "$ROOT/shell/plugins/clipboard/capture.sh")
[[ $capture_output == '{"type":"text","text":"terminal copy"}' ]] || fail "clipboard capture records normal text events"
@@ -513,8 +513,8 @@ pass "clipboard open helper opens text entries in editor"
[[ $(<"$TMPDIR/editor-path") == "$TMPDIR"/state/omarchy/clipboard-open/clipboard.*.txt ]] || fail "clipboard open helper writes text entries to a temporary file"
pass "clipboard open helper writes text entries to a temporary file"
TENSAKU_OUT="$TMPDIR/tensaku" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \
OMASNAP_OUT="$TMPDIR/omasnap" HOME="$TMPDIR/home" PATH="$TMPDIR/bin:$PATH" \
"$ROOT/bin/omarchy-clipboard-open" --history-index 2
[[ $(<"$TMPDIR/tensaku") == "$TMPDIR/image.png" ]] || fail "clipboard open helper opens image entries in Tensaku"
pass "clipboard open helper opens image entries in Tensaku"
[[ $(<"$TMPDIR/omasnap") == "$TMPDIR/image.png" ]] || fail "clipboard open helper opens image entries in Omasnap"
pass "clipboard open helper opens image entries in Omasnap"
+2 -2
View File
@@ -62,7 +62,7 @@ run_guard() {
' bash "$SHELL_TEST_DIR" 2>&1 || printf 'guard exited %s\n' "$?"
}
skipped="ok - no Wayland compositor; skipping sample runtime test"
skipped="ok - no Wayland compositor; skipping sample runtime test # SKIP"
output=$(run_guard -u WAYLAND_DISPLAY XDG_RUNTIME_DIR="$runtime_dir")
[[ $output == "$skipped" ]] || fail "guard skips without a display" "$output"
@@ -76,7 +76,7 @@ pass "guard skips when the socket is unreachable"
# Everything below needs a socket to stand in for a live or abandoned compositor.
if (( ! socket_bound )); then
pass "cannot bind a Unix socket here; skipping the cases that need one"
skip "cannot bind a Unix socket here; skipping the cases that need one"
exit 0
fi
+9
View File
@@ -33,6 +33,15 @@ jq -e '
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default center anchor exists in center layout"
jq -e '
def ids: map(.id // .);
(.bar.layout.center | ids) as $ids |
($ids | index("omacom.elsewhen")) as $elsewhen |
($ids | index("omarchy.clock")) as $clock |
$elsewhen != null and $clock == $elsewhen + 1
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default center layout puts elsewhen immediately before the clock"
jq -e '
any(.bar.layout.center[]; (.id // .) == "omarchy.clock" and (.formatAlt // "") == "d MMMM \u0027W\u0027ww yyyy")
' "$ROOT/config/omarchy/shell.json" >/dev/null
@@ -1,238 +0,0 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command jq
require_command python3
migration="$ROOT/migrations/1786643346.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
home="$test_dir/home"
profile_root="$home/.config/chromium"
preferences="$profile_root/Default/Preferences"
mkdir -p "$(dirname "$preferences")"
# Any id Chromium once derived from the extension's keyless load path; the
# repair keys off the registered command name, not the id.
ghost_id="ikkebdkaanlebnifjnbeiaklodhbjcci"
pinned_id="bgpiichlckmfanooecilcjemknkcpngb"
write_stale_preferences() {
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {($ghost): {commands: {"copy-url": {suggested_key: "Alt+Shift+L", was_assigned: true}}}, ($pinned): {commands: {"copy-url": {suggested_key: "Alt+Shift+L"}}}}}}' >"$preferences"
}
stub_bin="$test_dir/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/python3" <<'STUB'
#!/bin/bash
exit 127
STUB
chmod +x "$stub_bin/python3"
# Test stubs must delegate to the system interpreter, not a user shim that can
# route python3 back through the stubs and recurse.
REAL_PYTHON=$(PATH="$stub_bin:$PATH" command -p -v python3)
[[ $REAL_PYTHON != "$stub_bin/python3" ]] || fail "real Python resolution bypasses user shims"
export REAL_PYTHON
rm -f "$stub_bin/python3"
run_migration() {
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1
}
# A running Chromium-family browser marks its profile root with a SingletonLock
# symlink to <hostname>-<pid>, a target that never exists on disk. That lock —
# not the mere presence of a browser process — is what the migration waits on.
open_browser() {
mkdir -p "$profile_root"
ln -sfn "test-host-1234" "$profile_root/SingletonLock"
}
close_browser() {
rm -f "$profile_root/SingletonLock"
}
# The affected profile being open prompts for the windows to be closed;
# declining (or having no terminal to ask in) defers the repair so a
# rewrite-on-exit cannot revert it.
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
chmod +x "$stub_bin/gum"
write_stale_preferences
open_browser
before_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
run_migration && fail "migration defers while the affected profile is open"
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$before_hash" ]] ||
fail "migration leaves preferences alone while the affected profile is open"
pass "migration defers the repair while the affected profile is open"
# gum paints its prompt on stderr, so that stream has to stay attached:
# suppressing it leaves gum reading keys behind an unpainted screen, which
# reads as a hung update.
cat >"$stub_bin/gum" <<'STUB'
#!/bin/bash
echo "gum-prompt-painted" >&2
exit 1
STUB
prompt_stderr="$test_dir/prompt-stderr"
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>"$prompt_stderr" &&
fail "migration defers when the browser prompt is declined"
grep -q "gum-prompt-painted" "$prompt_stderr" || fail "migration keeps the browser prompt visible"
pass "migration keeps the browser prompt visible"
# A browser holding a different profile root cannot revert this repair, so it
# must not hold the update: the repair goes through without ever reaching the
# prompt, which the still-declining gum stub would otherwise fail.
close_browser
mkdir -p "$home/.config/google-chrome"
ln -sfn "test-host-1234" "$home/.config/google-chrome/SingletonLock"
write_stale_preferences
run_migration || fail "migration repairs while a different profile root is open"
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
fail "migration repairs the shortcut while a different profile root is open"
pass "migration ignores a browser on a different profile root"
rm -f "$home/.config/google-chrome/SingletonLock" "$preferences.omarchy-copy-url-repair.bak"
# Closing the affected profile and confirming the prompt lets the repair
# proceed.
write_stale_preferences
open_browser
cat >"$stub_bin/gum" <<'STUB'
#!/bin/bash
"$CLOSE_BROWSER"
touch "${GUM_CALLED:?}"
exit 0
STUB
cat >"$stub_bin/close-browser" <<'STUB'
#!/bin/bash
rm -f "$HOME/.config/chromium/SingletonLock"
STUB
chmod +x "$stub_bin/gum" "$stub_bin/close-browser"
GUM_CALLED="$test_dir/gum-called" CLOSE_BROWSER="$stub_bin/close-browser" \
HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1 ||
fail "migration proceeds once the profile is closed and the prompt confirmed"
[[ -e $test_dir/gum-called ]] || fail "migration asks before repairing under a running browser"
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
fail "migration repairs after the browser prompt is confirmed"
pass "migration asks to close the browser and repairs on confirmation"
rm -f "$preferences.omarchy-copy-url-repair.bak"
# With the affected profile closed the ghost registration moves to the pinned id.
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/gum"
close_browser
write_stale_preferences
run_migration || fail "migration repairs the shortcut when no browser is running"
jq -e --arg ghost "$ghost_id" --arg pinned "$pinned_id" '
.extensions.commands["linux:Alt+Shift+L"].extension == $pinned and
(.extensions.settings | has($ghost) | not) and
.extensions.settings[$pinned].commands["copy-url"].was_assigned == true
' "$preferences" >/dev/null || fail "migration rebinds the Copy URL shortcut to the pinned extension id"
[[ -f $preferences.omarchy-copy-url-repair.bak ]] ||
fail "migration backs up preferences before the repair"
pass "migration rebinds the Copy URL shortcut to the pinned extension id"
# A repaired profile has no ghost registration left, so nothing is pending —
# even while that same profile is open.
rm "$preferences.omarchy-copy-url-repair.bak"
repaired_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
open_browser
run_migration || fail "migration reruns cleanly after the repair"
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$repaired_hash" && ! -e $preferences.omarchy-copy-url-repair.bak ]] ||
fail "migration is idempotent after the repair"
pass "migration is idempotent after the repair"
close_browser
# A remapped shortcut keeps the user's chosen key while moving to the pinned id.
jq -n --arg ghost "$ghost_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
run_migration || fail "migration repairs remapped shortcuts"
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned' "$preferences" >/dev/null ||
fail "migration keeps the remapped key while rebinding to the pinned id"
pass "migration keeps remapped shortcut keys"
# When the pinned extension already holds a copy-url binding (the user fixed
# it by hand), the ghost is dropped rather than doubled into a second binding.
jq -n --arg ghost "$ghost_id" --arg pinned "$pinned_id" '{extensions: {commands: {"linux:Ctrl+Alt+P": {command_name: "copy-url", extension: $pinned, global: false}, "linux:Alt+Shift+L": {command_name: "copy-url", extension: $ghost, global: false}}, settings: {}}}' >"$preferences"
run_migration || fail "migration cleans ghosts alongside a manual repair"
jq -e --arg pinned "$pinned_id" '
(.extensions.commands | has("linux:Alt+Shift+L") | not) and
.extensions.commands["linux:Ctrl+Alt+P"].extension == $pinned
' "$preferences" >/dev/null || fail "migration drops the ghost instead of double-binding the pinned extension"
pass "migration never double-binds the pinned extension"
# A browser starting mid-repair may write stale Preferences back on exit, so
# the migration must stay pending for a later browser-free run to verify. A
# stub hands the repair call through and opens the profile right after it.
write_stale_preferences
close_browser
rm -f "$preferences.omarchy-copy-url-repair.bak"
cat >"$stub_bin/python3" <<'STUB'
#!/bin/bash
# Called as `python3 -c <script> <preferences> <pinned_id> <check|repair>`, and
# the check calls report a surviving ghost through their exit status.
"${REAL_PYTHON}" "$@"
status=$?
[[ ${5:-} == "repair" ]] && ln -sfn "test-host-1234" "$HOME/.config/chromium/SingletonLock"
exit $status
STUB
chmod +x "$stub_bin/python3"
if HOME="$home" PATH="$stub_bin:$PATH" bash -euo pipefail "$migration" >/dev/null 2>&1; then
fail "migration stays pending when a browser starts mid-repair"
fi
jq -e --arg pinned "$pinned_id" '.extensions.commands["linux:Alt+Shift+L"].extension == $pinned' "$preferences" >/dev/null ||
fail "migration still repairs preferences before deferring on a late browser"
pass "migration stays pending when a browser starts mid-repair"
rm -f "$stub_bin/python3" "$preferences.omarchy-copy-url-repair.bak"
close_browser
# A browser that started and exited mid-repair restores stale Preferences
# before the final profile check; the post-repair file verification catches it.
write_stale_preferences
cp "$preferences" "$test_dir/stale-preferences"
cat >"$stub_bin/python3" <<'STUB'
#!/bin/bash
"${REAL_PYTHON}" "$@"
status=$?
[[ ${5:-} == "repair" ]] && cp "${STALE_PREFERENCES:?}" "${REPAIRED_PREFERENCES:?}"
exit $status
STUB
chmod +x "$stub_bin/python3"
if HOME="$home" PATH="$stub_bin:$PATH" STALE_PREFERENCES="$test_dir/stale-preferences" \
REPAIRED_PREFERENCES="$preferences" bash -euo pipefail "$migration" >/dev/null 2>&1; then
fail "migration stays pending when a briefly-lived browser undoes the repair"
fi
pass "migration stays pending when a briefly-lived browser undoes the repair"
rm -f "$stub_bin/python3"
close_browser
write_stale_preferences
run_migration || fail "migration recovers after a reverted repair"
rm -f "$preferences.omarchy-copy-url-repair.bak"
# A repair attempted while the affected profile was open leaves its backup
# behind. A rerun that sees a clean disk while that profile still runs must
# stay pending — the browser can restore the ghost on exit — and only a
# browser-free rerun verifies the repair and completes.
write_stale_preferences
run_migration || fail "repair run before the verification scenario"
[[ -f $preferences.omarchy-copy-url-repair.bak ]] || fail "verification scenario has a repair backup"
open_browser
run_migration && fail "migration must not complete an unverified repair while a browser runs"
pass "migration keeps an unverified repair pending while a browser runs"
close_browser
run_migration || fail "migration completes once the repair is verified with browsers closed"
pass "migration verifies an attempted repair on a browser-free rerun"
rm -f "$preferences.omarchy-copy-url-repair.bak"
# An installed third-party extension with a command that happens to be named
# copy-url keeps its own registration.
jq -n '{extensions: {commands: {"linux:Alt+Shift+L": {command_name: "copy-url", extension: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", global: false}}, settings: {aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa: {path: "/home/user/.config/some-extension", commands: {}}}}}' >"$preferences"
untouched_hash=$(sha256sum "$preferences" | cut -d' ' -f1)
run_migration || fail "migration leaves installed third-party extensions alone"
[[ $(sha256sum "$preferences" | cut -d' ' -f1) == "$untouched_hash" ]] ||
fail "migration does not steal a third-party copy-url command registration"
pass "migration leaves installed third-party extensions alone"
+24
View File
@@ -22,6 +22,15 @@ menu_log="$test_tmp/menu"
muse_login_log="$test_tmp/muse-login"
mkdir -p "$mock_bin" "$test_home"
cat >"$mock_bin/omarchy-install-chromium-claude" <<'SH'
#!/bin/bash
echo claude-extension >>"$OMARCHY_TEST_STUB_LOG"
if [[ ${OMARCHY_TEST_EXTENSION_FAIL:-false} == "true" ]]; then
echo "Extension installation failed" >&2
exit 1
fi
SH
cat >"$mock_bin/omarchy-notification-send" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_NOTIFICATION_HISTORY"
@@ -406,9 +415,16 @@ declare -A expected_packages=(
for selection in "${!expected_agents[@]}"; do
expected=${expected_agents[$selection]}
: >"$agent_open_log"
: >"$stub_log"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent "$selection"
[[ $(omarchy-default-agent) == $expected ]] || fail "default agent canonicalizes $selection"
if [[ $expected == "claude" ]]; then
grep -qx claude-extension "$stub_log" || fail "Claude selection installs the browser extension"
else
[[ ! -s $stub_log ]] || fail "other agents do not install the Claude extension"
fi
mapfile -d '' -t mise_args <"$mise_log"
[[ ${mise_args[0]} == "use" && ${mise_args[1]} == "-g" ]] ||
fail "default agent installs $selection globally through mise"
@@ -426,6 +442,14 @@ pass "default agent selects and opens every supported provider and alias"
fail "default agent stores its selection in Omarchy user config"
pass "default agent stores its selection in Omarchy user config"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$agent_open_log"
OMARCHY_TEST_AGENT_INSTALLED=true OMARCHY_TEST_EXTENSION_FAIL=true omarchy-default-agent claude >"$test_tmp/extension-failure" 2>&1
[[ $(omarchy-default-agent) == "claude" ]] || fail "extension installation failure still selects Claude"
mapfile -d '' -t agent_open_args <"$agent_open_log"
[[ ${agent_open_args[*]} == "omarchy-agent" ]] || fail "extension installation failure still launches Claude"
[[ ! -s $test_tmp/extension-failure ]] || fail "extension installation failure is silent"
pass "extension installation failure silently continues selecting and launching Claude"
OMARCHY_TEST_AGENT_INSTALLED=true omarchy-default-agent pi
: >"$notification_history"
: >"$agent_open_log"
+2 -2
View File
@@ -81,13 +81,13 @@ SH
rm -rf "$poison_dir"
pass "root omarchy-dns resolves system helpers from a trusted PATH, not the invocation PATH"
else
pass "no unprivileged user namespace; skipping the root trusted-PATH probe"
skip "no unprivileged user namespace; skipping the root trusted-PATH probe"
fi
# require_root returns immediately for root, so the stubs below would not stand
# between the script and the host's real NetworkManager and resolved config.
if (( EUID == 0 )); then
pass "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
skip "running as root; skipping the elevation checks, which would rewrite this machine's DNS"
exit 0
fi
@@ -0,0 +1,89 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command jq
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/bin" "$test_dir/home"
export CALL_LOG="$test_dir/calls"
cat >"$test_dir/bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf 'package %s\n' "$*" >>"$CALL_LOG"
exit "${PACKAGE_STATUS:-0}"
SH
cat >"$test_dir/bin/omarchy-shell" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$CALL_LOG"
[[ $2 != "rescanPlugins" ]] || exit "${SCAN_STATUS:-0}"
printf '%s\n' "${TEST_PUT_RESULT:-ok}"
SH
cat >"$test_dir/bin/omarchy-restart-shell" <<'SH'
#!/bin/bash
echo 'migration must leave the restart to omarchy update' >&2
exit 1
SH
chmod +x "$test_dir/bin/"*
mkdir -p "$test_dir/packaged/shell/plugins/omacom.elsewhen"
sed "s|/usr/share/omarchy|$test_dir/packaged|g" "$ROOT/migrations/1789581661.sh" >"$test_dir/migration.sh"
plugin="$test_dir/home/.config/omarchy/plugins/omacom.elsewhen"
run_migration() {
: >"$CALL_LOG"
HOME="$test_dir/home" OMARCHY_PATH="${1:-$test_dir/packaged}" PATH="$test_dir/bin:$ROOT/bin:$PATH" \
bash -euo pipefail "$test_dir/migration.sh" >"$test_dir/output" 2>&1
}
if PACKAGE_STATUS=1 run_migration; then
fail "package failure stops the migration"
fi
[[ ! -e $plugin && ! -L $plugin && $(cat "$CALL_LOG") == "package elsewhen" ]] ||
fail "package failure leaves the plugin and shell untouched"
pass "package failure stops before changing the shell"
run_migration
[[ ! -e $plugin && ! -L $plugin ]] || fail "migration does not create a user plugin link"
[[ ! -e $ROOT/config/omarchy/plugins/omacom.elsewhen && ! -L $ROOT/config/omarchy/plugins/omacom.elsewhen ]] || fail "fresh installs do not ship a user plugin link"
pass "migration and fresh installs rely on the packaged plugin directory"
expected=$'package elsewhen\nshell rescanPlugins\nshell putBarWidget omacom.elsewhen {"before":"omarchy.clock"}'
[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "install, scan and placement run in order" "$(cat "$CALL_LOG")"
pass "real bar helper enables and places before the clock without restarting during reload"
run_migration
[[ $(cat "$CALL_LOG") == "$expected" ]] || fail "migration can be rerun"
[[ ! -e $plugin && ! -L $plugin ]] || fail "rerunning the migration does not create a user plugin link"
pass "migration can be rerun without a user plugin link"
run_migration "$ROOT"
[[ $(readlink "$plugin") == "$test_dir/packaged/shell/plugins/omacom.elsewhen" ]] || fail "dev checkout links the packaged plugin"
pass "dev checkout discovers Elsewhen through a user plugin link"
run_migration "$ROOT"
[[ $(readlink "$plugin") == "$test_dir/packaged/shell/plugins/omacom.elsewhen" ]] || fail "dev link survives a rerun"
pass "dev link is idempotent"
rm "$plugin"
mkdir -p "$plugin"
printf 'local work\n' >"$plugin/notes"
run_migration "$ROOT"
[[ ! -L $plugin && $(cat "$plugin/notes") == "local work" ]] || fail "existing checkout is preserved"
pass "existing checkout and local files are preserved"
rm "$plugin/notes"
rmdir "$plugin"
ln -s "$test_dir/custom-plugin" "$plugin"
run_migration "$ROOT"
[[ $(readlink "$plugin") == "$test_dir/custom-plugin" ]] || fail "existing symlink is preserved"
pass "existing symlink is preserved, including a missing target"
for failure in 'SCAN_STATUS=1' 'TEST_PUT_RESULT=unknown'; do
if env "$failure" HOME="$test_dir/home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" \
bash -euo pipefail "$test_dir/migration.sh" >"$test_dir/output" 2>&1; then
fail "$failure must leave the migration pending"
fi
pass "$failure leaves the migration pending"
done
+148
View File
@@ -0,0 +1,148 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if (( EUID != 0 )); then
if unshare --user --map-root-user true 2>/dev/null; then
exec unshare --user --map-root-user bash "$0"
fi
skip "no unprivileged user namespace; skipping factory account cleanup"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
# Load the production functions without self-elevation or the reset entrypoint.
awk '
/^[a-z_]+\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-system-factory-reset" >"$test_tmp/functions"
cat >"$test_tmp/reset" <<'SH'
#!/bin/bash
set -euo pipefail
source "$1/functions"
TOP_MNT="$2"
NEXT_NAME=@omarchy-reset-next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE="$TOP_MNT/reset.log"
log() { printf '%s\n' "$1" >>"$LOG_FILE"; }
fail() { log "$1"; exit 1; }
# Account tools are real. Only snapshots, boot rebuilding, and system services
# are replaced: all writes stay inside this test's disposable directory.
btrfs() {
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then
mkdir -p "$4"
cp -a "$3/." "$4/"
elif [[ $1 == "property" ]]; then
printf '%s\n' "$6" >"$4/read-only"
else
return 1
fi
}
systemd-id128() { printf '%032d\n' 1; }
install_provisioning_units() { :; }
encrypted_install() { return 1; }
rebuild_next_boot() { touch "$TOP_MNT/rebuilt"; }
sync() { :; }
userdel() {
[[ ${FAIL_COMMAND:-} == "userdel" && $2 == "$FAIL_ROOT" ]] && return 42
command userdel "$@"
}
usermod() {
[[ ${FAIL_COMMAND:-} == "usermod" && $2 == "$FAIL_ROOT" ]] && return 42
command usermod "$@"
}
rm() {
[[ ${FAIL_COMMAND:-} == "rm" && $* == *"$FAIL_ROOT/etc/shadow-"* ]] && return 42
command rm "$@"
}
stage_full_reset
SH
make_fixture() {
local top="$1" root_hash="${2:-original-root-hash}"
local factory="$top/@factory"
mkdir -p "$top/@" "$factory/etc" "$factory/home/seller" \
"$factory/usr/bin" "$factory/usr/share/omarchy/install/provisioning" \
"$factory/var/lib/omarchy/provisioning/packages"
touch "$top/@/old-system" "$factory/home/seller/private-file" \
"$factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service" \
"$factory/var/lib/omarchy/provisioning/packages/node-v0.tar.gz"
printf '#!/bin/bash\n' >"$factory/usr/bin/omarchy-provision-owner"
chmod +x "$factory/usr/bin/omarchy-provision-owner"
printf 'true\n' >"$factory/read-only"
cat >"$factory/etc/passwd" <<'EOF'
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/:/usr/bin/nologin
seller:x:1000:1000:Seller:/home/seller:/bin/bash
EOF
printf 'root:%s:20000:0:99999:7:::\ndaemon:*:20000:0:99999:7:::\nseller:original-user-hash:20000:0:99999:7:::\n' \
"$root_hash" >"$factory/etc/shadow"
printf 'root:x:0:\ndaemon:x:1:\nseller:x:1000:\nwheel:x:998:seller\n' >"$factory/etc/group"
printf 'root:!::\ndaemon:!::\nseller:!::\nwheel:!::seller\n' >"$factory/etc/gshadow"
printf 'USERGROUPS_ENAB yes\n' >"$factory/etc/login.defs"
printf 'seller:100000:65536\n' >"$factory/etc/subuid"
printf 'seller:100000:65536\n' >"$factory/etc/subgid"
chmod 600 "$factory/etc/"{shadow,gshadow}
for file in passwd shadow group gshadow subuid subgid; do
cp "$factory/etc/$file" "$factory/etc/$file-"
done
}
assert_scrubbed() {
local root="$1" file
[[ $(awk -F: '$1 == "root" { print $2 }' "$root/etc/shadow") == "!" ]] ||
fail "reset erases the root hash while keeping the account locked"
! grep -q 'original-.*-hash\|seller' "$root/etc/"{passwd,shadow,group,gshadow} ||
fail "reset removes seller account credentials and group membership"
[[ ! -e $root/home/seller ]] || fail "reset removes the seller's baseline home"
grep -q '^daemon:\*:' "$root/etc/shadow" || fail "reset preserves service accounts"
[[ $(stat -c '%a' "$root/etc/shadow") == "600" ]] || fail "shadow stays private"
for file in passwd shadow group gshadow subuid subgid; do
[[ ! -e $root/etc/$file- ]] || fail "reset removes the $file backup"
done
}
for scenario in normal locked; do
top="$test_tmp/$scenario"
if [[ $scenario == "locked" ]]; then
make_fixture "$top" '!'
else
make_fixture "$top"
fi
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset stages successfully"
assert_scrubbed "$top/@factory"
assert_scrubbed "$top/@"
[[ $(cat "$top/@factory/read-only") == "true" ]] || fail "baseline returns to read-only"
[[ -f $top/@/var/lib/omarchy/provisioning/pending && -f $top/rebuilt ]] ||
fail "reset reaches provisioning after cleanup"
bash "$test_tmp/reset" "$test_tmp" "$top" || fail "$scenario reset can be repeated"
assert_scrubbed "$top/@factory"
assert_scrubbed "$top/@"
pass "$scenario reset scrubs both roots, preserves service accounts, and can be repeated"
done
for target in @omarchy-reset-next @factory; do
for command in userdel usermod rm; do
top="$test_tmp/fail-$target-$command"
make_fixture "$top"
if FAIL_COMMAND="$command" FAIL_ROOT="$top/$target" bash "$test_tmp/reset" "$test_tmp" "$top"; then
fail "reset accepted failed $command in $target"
fi
[[ -f $top/@/old-system && ! -e $top/rebuilt ]] ||
fail "failed cleanup must not activate or rebuild the reset system"
[[ $(cat "$top/@factory/read-only") == "true" ]] ||
fail "failed cleanup must leave the baseline read-only"
pass "failed $command in $target aborts reset before activation"
done
done
+1 -1
View File
@@ -4,7 +4,7 @@ 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"
skip "fingerprint invitation test skipped: host already has fingerprint auth configured"
exit 0
fi
+69
View File
@@ -0,0 +1,69 @@
import QtQuick
import Quickshell
ShellRoot {
id: root
property var view
property var wallpaper
property var feed
property int step: 0
Item { id: host; width: 1000; height: 700 }
function findItem(item, name) {
if (item.objectName === name) return item
for (var i = 0; i < item.children.length; i++) {
var found = findItem(item.children[i], name)
if (found) return found
}
return null
}
function check(condition, message) {
if (!condition) throw new Error(message)
}
Timer {
interval: 150
repeat: true
running: true
onTriggered: {
try {
if (root.step === 0) {
var component = Qt.createComponent("file://" + Quickshell.env("OMARCHY_PATH") + "/shell/plugins/lock/LockView.qml")
check(component.status === Component.Ready, component.errorString())
root.view = component.createObject(host, {width: 1000, height: 700, backgroundPath: "/still.png", loadBackground: false})
check(root.view !== null, component.errorString())
root.wallpaper = findItem(root.view, "lockWallpaper")
root.feed = findItem(root.view, "lockFeedLoader")
check(root.wallpaper.path === "", "hidden still must not decode")
root.view.backgroundPath = "/video.mp4"
root.view.videoPosterPath = "/poster.jpg"
} else if (root.step === 1) {
check(!root.feed.active && root.feed.item === null, "hidden video must not load a feed client")
root.view.loadBackground = true
root.view.powerSaverActive = true
} else if (root.step === 2) {
check(root.wallpaper.path === "/poster.jpg", "power saver keeps the video poster")
check(!root.feed.active, "power saver must not connect to the feed")
root.view.powerSaverActive = false
} else if (root.step === 3) {
check(root.feed.active, "visible video enables the isolated feed loader")
check(root.wallpaper.path === "/poster.jpg", "poster stays beneath the feed before its first frame")
root.view.displaysBlank = true
} else if (root.step === 4) {
check(!root.feed.active && root.feed.item === null, "blanking releases the feed client")
root.view.loadBackground = false
} else {
check(root.wallpaper.path === "", "unlock releases the poster image")
console.log("OWE_LOCK_TEST_PASS")
Qt.quit()
}
root.step++
} catch (error) {
console.error("OWE_LOCK_TEST_FAIL: " + error)
Qt.quit()
}
}
}
}
+158
View File
@@ -0,0 +1,158 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
work_dir=$(mktemp -d)
trap 'rm -rf "$work_dir"' EXIT
fake_home="$work_dir/home"
mkdir -p "$fake_home/.config/omarchy/hooks" "$fake_home/.local/state/omarchy"
# --- omarchy-hook --------------------------------------------------------------
# A hook name is a label, not a path. One carrying a slash, or one that is a
# bare `.` or `..`, would run a script from outside the hooks directory.
cat >"$fake_home/.config/omarchy/hooks/test-hook" <<'SH'
touch "$HOME/hook-ran"
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook" test-hook
[[ -f $fake_home/hook-ran ]] ||
fail "omarchy hook runs a named hook from the hooks directory"
pass "omarchy hook runs a named hook from the hooks directory"
# Dots inside a name are not a path. a..b stays inside the hooks directory.
cat >"$fake_home/.config/omarchy/hooks/a..b" <<'SH'
touch "$HOME/dotted-hook-ran"
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook" a..b
[[ -f $fake_home/dotted-hook-ran ]] ||
fail "omarchy hook accepts a hook name with dots in the middle"
pass "omarchy hook accepts a hook name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "$name" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name of $name" "exit: $status"
pass "omarchy hook refuses a hook name of $name"
done
# This file sits where a name of ../../evil would resolve: hooks/../.. is
# ~/.config.
cat >"$fake_home/.config/evil" <<'SH'
touch "$HOME/escape-ran"
SH
chmod +x "$fake_home/.config/evil"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "../../evil" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/escape-ran ]] ||
fail "omarchy hook runs nothing when it refuses the name"
pass "omarchy hook refuses a hook name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook" "sub/dir" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook refuses a hook name with a slash" "exit: $status"
pass "omarchy hook refuses a hook name with a slash"
# --- omarchy-hook-install ------------------------------------------------------
# The installer joins the type into ~/.config/omarchy/hooks/<type>.d before
# mkdir/cp. The runner already refuses a slashed type; install must too, or a
# name the runner will not run still lands on disk.
source_hook="$work_dir/source-hook"
cat >"$source_hook" <<'SH'
#!/bin/bash
true
SH
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" post-update "$source_hook" >/dev/null
[[ -f $fake_home/.config/omarchy/hooks/post-update.d/source-hook ]] ||
fail "omarchy hook install still installs a named hook"
pass "omarchy hook install still installs a named hook"
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" a..b "$source_hook" >/dev/null
[[ -f $fake_home/.config/omarchy/hooks/a..b.d/source-hook ]] ||
fail "omarchy hook install accepts a hook name with dots in the middle"
pass "omarchy hook install accepts a hook name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "$name" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name of $name" "exit: $status"
[[ ! -e $fake_home/.config/omarchy/hooks/${name}.d ]] ||
fail "omarchy hook install creates no directory for a hook name of $name"
pass "omarchy hook install refuses a hook name of $name"
done
# hooks/../../evil.d is ~/.config/evil.d. The guard must fire before mkdir.
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "../../evil" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/.config/evil.d ]] ||
fail "omarchy hook install creates nothing outside the hooks directory"
pass "omarchy hook install refuses a hook name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-hook-install" "sub/dir" "$source_hook" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy hook install refuses a hook name with a slash" "exit: $status"
[[ ! -e $fake_home/.config/omarchy/hooks/sub ]] ||
fail "omarchy hook install creates no nested directory from a slashed name"
pass "omarchy hook install refuses a hook name with a slash"
# --- omarchy-state -------------------------------------------------------------
state_dir="$fake_home/.local/state/omarchy"
HOME="$fake_home" "$ROOT/bin/omarchy-state" set reboot-required
[[ -f $state_dir/reboot-required ]] ||
fail "omarchy state set still creates a plain state file"
pass "omarchy state set still creates a plain state file"
HOME="$fake_home" "$ROOT/bin/omarchy-state" set v1..2
[[ -f $state_dir/v1..2 ]] ||
fail "omarchy state set accepts a state name with dots in the middle"
pass "omarchy state set accepts a state name with dots in the middle"
for name in . ..; do
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "$name" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name of $name" "exit: $status"
pass "omarchy state set refuses a state name of $name"
done
# state/../.. is ~/.local. The guard must fire before touch gets there.
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "../../escape" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name with a dot-dot" "exit: $status"
[[ ! -e $fake_home/.local/escape ]] ||
fail "omarchy state set creates nothing outside the state directory"
pass "omarchy state set refuses a state name with a dot-dot"
status=0
HOME="$fake_home" "$ROOT/bin/omarchy-state" set "sub/dir" >/dev/null 2>&1 || status=$?
(( status == 2 )) ||
fail "omarchy state set refuses a state name with a slash" "exit: $status"
pass "omarchy state set refuses a state name with a slash"
# clear takes patterns by design ("state-name-or-pattern") and matches
# basenames through find -name, so it can never walk out of the directory.
touch "$state_dir/restart-a-required" "$state_dir/restart-b-required" "$state_dir/keep-me"
HOME="$fake_home" "$ROOT/bin/omarchy-state" clear "restart-*-required"
[[ ! -e $state_dir/restart-a-required && ! -e $state_dir/restart-b-required && -f $state_dir/keep-me ]] ||
fail "omarchy state clear still clears matching patterns only"
pass "omarchy state clear still clears matching patterns only"
+1 -1
View File
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "QML contract test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping QML contract test"
skip "quickshell not installed; skipping QML contract test"
exit 0
fi
+58
View File
@@ -0,0 +1,58 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin"
export INSTALLED_PACKAGES="$tmp_dir/installed" CALL_LOG="$tmp_dir/calls"
export PATH="$tmp_dir/bin:$ROOT/bin:$PATH"
# Keep the real package helpers, but contain every pacman transaction here.
cat > "$tmp_dir/bin/pacman" <<'SH'
#!/bin/bash
case "$1" in
-Q) grep -Fxq -- "$2" "$INSTALLED_PACKAGES" ;;
-S)
[[ ${FAIL_INSTALL:-0} == 0 ]] || exit 1
shift 3 # -S --noconfirm --needed
printf '%s\n' "$@" >> "$INSTALLED_PACKAGES"
printf '%s\n' "$@" >> "$CALL_LOG"
;;
*) exit 1 ;;
esac
SH
cat > "$tmp_dir/bin/sudo" <<'SH'
#!/bin/bash
[[ $1 == "pacman" ]] || exit 1
"$@"
SH
chmod +x "$tmp_dir/bin/"*
migration="$ROOT/migrations/1789444024.sh"
for kernels in linux-omarchy linux-t2 'linux-omarchy linux-t2'; do
read -ra installed <<< "$kernels"
printf '%s\n' linux linux-headers "${installed[@]}" > "$INSTALLED_PACKAGES"
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
for kernel in "${installed[@]}"; do
grep -Fxq "$kernel-headers" "$INSTALLED_PACKAGES" || fail "$kernel gets its headers"
done
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
[[ ! -s $CALL_LOG ]] || fail "header repair is idempotent"
pass "missing headers are repaired once for $kernels"
done
printf '%s\n' linux linux-aarch64 > "$INSTALLED_PACKAGES"
: > "$CALL_LOG"
bash -euo pipefail "$migration" >/dev/null
[[ ! -s $CALL_LOG ]] || fail "header repair skips unrelated kernels"
pass "header repair skips unrelated kernels"
echo linux-omarchy > "$INSTALLED_PACKAGES"
if FAIL_INSTALL=1 bash -euo pipefail "$migration" >/dev/null; then
fail "a failed header installation must leave the migration pending"
fi
pass "header installation failure is propagated"
+1 -1
View File
@@ -150,5 +150,5 @@ pass "font controls create missing Kitty overrides without restoring the theme i
if "$ROOT/bin/omarchy-cmd-present" kitty; then
kitty +runpy "$(cat "$ROOT/test/shell.d/fixtures/kitty/check-config.py")"
else
pass "Kitty not installed; skipping native config parser checks"
skip "Kitty not installed; skipping native config parser checks"
fi
+3
View File
@@ -4,6 +4,9 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Start with color enabled; the no-color scenarios opt in below.
unset NO_COLOR
tmp_dir="$(mktemp -d)"
trap 'rm -rf "$tmp_dir"' EXIT
-9
View File
@@ -32,15 +32,6 @@ check(command == ["/usr/bin/updatedb", *options],
"locate service runs updatedb directly with fixed Btrfs options")
check("ConditionACPower=true" in (root / "etc/systemd/system/plocate-updatedb.service.d/ac-only.conf").read_text(),
"scheduled locate indexing keeps its AC-power condition")
check(not (root / "install/config/locate.sh").exists() and not (root / "migrations/1784809451.sh").exists(),
"the retired locate configuration helper and migration are absent")
for directory in ("bin", "install", "migrations"):
for path in (root / directory).rglob("*"):
if path.is_file():
content = path.read_text()
if "OMARCHY_UPDATEDB_CONF_PATH" in content or "config/locate.sh" in content:
raise SystemExit("not ok - retired locate configuration path remains in " + str(path))
check(True, "runtime and installation no longer reference the configuration rewrite")
with tempfile.TemporaryDirectory(prefix="omarchy-locate-") as scratch:
scratch = Path(scratch)
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "lock fingerprint indicator test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping lock fingerprint indicator test"
skip "quickshell not installed; skipping lock fingerprint indicator test"
exit 0
fi
+1 -1
View File
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "lock password overflow test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping lock password overflow test"
skip "quickshell not installed; skipping lock password overflow test"
exit 0
fi
+1 -1
View File
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "manifest entrypoint load test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping manifest entrypoint load test"
skip "quickshell not installed; skipping manifest entrypoint load test"
exit 0
fi
+270
View File
@@ -0,0 +1,270 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command mise
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
migration="$ROOT/migrations/1789095456.sh"
run_migration() {
local test_home="$1"
env -i \
HOME="$test_home" \
XDG_CACHE_HOME="$test_home/.cache" \
XDG_CONFIG_HOME="$test_home/.config" \
XDG_DATA_HOME="$test_home/.local/share" \
XDG_STATE_HOME="$test_home/.local/state" \
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
PATH=/usr/bin \
bash -euo pipefail "$migration"
}
run_mise() {
local test_home="$1"
shift
env -i \
HOME="$test_home" \
XDG_CACHE_HOME="$test_home/.cache" \
XDG_CONFIG_HOME="$test_home/.config" \
XDG_DATA_HOME="$test_home/.local/share" \
XDG_STATE_HOME="$test_home/.local/state" \
MISE_PARANOID="${OMARCHY_TEST_MISE_PARANOID:-false}" \
PATH=/usr/bin \
mise "$@"
}
mise_environment() {
local test_home="$1"
local project="$2"
(
cd "$project"
run_mise "$test_home" env -s bash
)
}
mise_path_active() {
local test_home="$1"
local project="$2"
local output
if ! output=$(mise_environment "$test_home" "$project" 2>/dev/null); then
return 1
fi
grep -F "$project/bin" <<<"$output" >/dev/null
}
assert_unsafe_variant_removed() {
local variant="$1"
local assignment="$2"
local variant_home="$test_dir/$variant-home"
local variant_config="$variant_home/Work/.mise.toml"
local variant_project="$variant_home/Work/tries/untrusted-repository"
mkdir -p "$variant_project/bin"
printf '[env]\n%s\n' "$assignment" >"$variant_config"
run_mise "$variant_home" trust "$variant_config" >/dev/null
mise_path_active "$variant_home" "$variant_project" || fail "$variant legacy config prepends the repository bin directory"
run_migration "$variant_home" >/dev/null
if mise_path_active "$variant_home" "$variant_project"; then
fail "$variant repository bin directory remains in PATH after migration"
fi
}
install_home="$test_dir/install-home"
install_log="$test_dir/install-mise.log"
mkdir -p "$install_home" "$test_dir/bin"
cat >"$test_dir/bin/mise" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$MISE_TEST_LOG"
SH
chmod +x "$test_dir/bin/mise"
env \
HOME="$install_home" \
MISE_TEST_LOG="$install_log" \
OMARCHY_SETUP_CONTEXT=runtime \
PATH="$test_dir/bin:/usr/bin" \
bash -euo pipefail -c 'source "$1"' bash "$ROOT/install/user/mise-work.sh"
[[ -d $install_home/Work/tries ]] || fail "installer creates the work and tries directories"
[[ ! -e $install_home/Work/.mise.toml ]] || fail "installer does not create a trusted Work Mise config"
[[ $(<"$install_log") == "use -g node@latest" ]] || fail "installer only invokes Mise for the global Node setup"
pass "new installs do not add project bin directories to PATH"
stock_home="$test_dir/stock-home"
stock_config="$stock_home/Work/.mise.toml"
stock_project="$stock_home/Work/tries/untrusted-repository"
mkdir -p "$stock_project/bin"
cat >"$stock_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
TOML
run_mise "$stock_home" trust "$stock_config" >/dev/null
mise_path_active "$stock_home" "$stock_project" || fail "legacy config prepends the repository bin directory"
run_migration "$stock_home" >/dev/null
[[ ! -e $stock_config ]] || fail "migration removes the stock Work Mise config"
cat >"$stock_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
TOML
if mise_path_active "$stock_home" "$stock_project"; then
fail "recreated Work config remains trusted after migration"
fi
run_migration "$stock_home" >/dev/null
[[ ! -e $stock_config ]] || fail "stock migration is idempotent"
pass "migration removes the repository bin directory and revokes the Work trust root"
assert_unsafe_variant_removed inline-comment '_.path = "{{ cwd }}/bin" # Omarchy default'
assert_unsafe_variant_removed single-quoted "_.path = '{{ cwd }}/bin'"
pass "migration removes annotated and single-quoted project bin paths"
custom_home="$test_dir/custom-home"
custom_config="$custom_home/Work/.mise.toml"
mkdir -p "$(dirname "$custom_config")"
cat >"$custom_config" <<'TOML'
[env]
KEEP = "yes"
_.path = "{{ cwd }}/bin"
# _.path = "{{ cwd }}/bin"
[tools]
ruby = "latest"
[other]
_.path = "{{ cwd }}/bin"
TOML
cp "$custom_config" "$test_dir/custom-original"
cat >"$test_dir/custom-expected" <<'TOML'
[env]
KEEP = "yes"
# _.path = "{{ cwd }}/bin"
[tools]
ruby = "latest"
[other]
_.path = "{{ cwd }}/bin"
TOML
chmod 600 "$custom_config"
run_mise "$custom_home" trust "$custom_config" >/dev/null
custom_output=$(run_migration "$custom_home")
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "migration preserves unrelated custom Mise settings"
[[ $(stat -c %a "$custom_config") == "600" ]] || fail "migration preserves custom config permissions"
grep -F "mise trust $custom_config" <<<"$custom_output" >/dev/null || fail "migration explains how to review and re-trust a custom config"
custom_backups=("$custom_config".bak.*)
[[ -f ${custom_backups[0]} ]] || fail "migration backs up a customized Mise config"
(( ${#custom_backups[@]} == 1 )) || fail "migration creates one custom config backup"
cmp -s "$test_dir/custom-original" "${custom_backups[0]}" || fail "custom config backup preserves the original"
run_migration "$custom_home" >/dev/null
custom_backups=("$custom_config".bak.*)
(( ${#custom_backups[@]} == 1 )) || fail "custom migration does not create another backup on rerun"
cmp -s "$test_dir/custom-expected" "$custom_config" || fail "custom migration is idempotent"
pass "custom Mise settings, permissions, and original backup survive the repair"
unrelated_home="$test_dir/unrelated-home"
unrelated_config="$unrelated_home/Work/.mise.toml"
unrelated_project="$unrelated_home/Work/tries/untrusted-repository"
mkdir -p "$unrelated_project/bin"
printf '[env]\nKEEP = "yes"\n' >"$unrelated_config"
cp "$unrelated_config" "$test_dir/unrelated-original"
run_mise "$unrelated_home" trust "$unrelated_config" >/dev/null
run_migration "$unrelated_home" >/dev/null
cmp -s "$test_dir/unrelated-original" "$unrelated_config" || fail "unrelated Mise config remains unchanged"
unrelated_backups=("$unrelated_config".bak.*)
[[ ! -e ${unrelated_backups[0]} ]] || fail "unchanged Mise config is not backed up"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$unrelated_config"
if mise_path_active "$unrelated_home" "$unrelated_project"; then
fail "safe Work config retains its old trust grant"
fi
absent_home="$test_dir/absent-home"
absent_config="$absent_home/Work/.mise.toml"
absent_project="$absent_home/Work/tries/untrusted-repository"
mkdir -p "$(dirname "$absent_config")"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
run_mise "$absent_home" trust "$absent_config" >/dev/null
rm "$absent_config"
rmdir "$absent_home/Work"
run_migration "$absent_home" >/dev/null
[[ ! -e $absent_home/Work ]] || fail "migration does not retain a temporary Work directory"
mkdir -p "$absent_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$absent_config"
if mise_path_active "$absent_home" "$absent_project"; then
fail "deleted Work directory retains its stale trust grant"
fi
pass "migration leaves unrelated configs alone and revokes dangling Work trust"
paranoid_home="$test_dir/paranoid-home"
paranoid_work="$paranoid_home/Work"
paranoid_config="$paranoid_work/.mise.toml"
paranoid_project="$paranoid_work/tries/untrusted-repository"
mkdir -p "$paranoid_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
OMARCHY_TEST_MISE_PARANOID=true run_mise "$paranoid_home" trust "$paranoid_config" >/dev/null
OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project" || fail "paranoid legacy config prepends the repository bin directory"
rm -r "$paranoid_work"
OMARCHY_TEST_MISE_PARANOID=true run_migration "$paranoid_home" >/dev/null
[[ ! -e $paranoid_work ]] || fail "paranoid migration removes its temporary Work directory"
mkdir -p "$paranoid_project/bin"
printf '[env]\n_.path = "{{ cwd }}/bin"\n' >"$paranoid_config"
if OMARCHY_TEST_MISE_PARANOID=true mise_path_active "$paranoid_home" "$paranoid_project"; then
fail "paranoid migration retains content-bound trust for the deleted legacy config"
fi
pass "migration revokes stale content-bound trust in Mise paranoid mode"
ignored_home="$test_dir/ignored-home"
ignored_config="$ignored_home/Work/.mise.toml"
ignored_project="$ignored_home/Work/tries/untrusted-repository"
mkdir -p "$ignored_project/bin"
cat >"$ignored_config" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
KEEP = "yes"
TOML
run_mise "$ignored_home" trust "$ignored_config" >/dev/null
run_mise "$ignored_home" trust --ignore "$ignored_config" >/dev/null
ignored_output=$(run_migration "$ignored_home")
grep -F '{{ cwd }}/bin' "$ignored_config" >/dev/null && fail "ignored config retains the unsafe path"
grep -Fx 'KEEP = "yes"' "$ignored_config" >/dev/null || fail "ignored config keeps unrelated settings"
ignored_entries=("$ignored_home/.local/state/mise/ignored-configs/"*)
[[ -L ${ignored_entries[0]} ]] || fail "migration preserves the explicit Mise ignore marker"
(( ${#ignored_entries[@]} == 1 )) || fail "migration preserves exactly one Mise ignore marker"
ignored_target=$(readlink "${ignored_entries[0]}")
[[ $ignored_target == $ignored_home/Work || $ignored_target == $ignored_config ]] || fail "preserved Mise ignore marker still targets the Work config"
grep -F "remains ignored by Mise" <<<"$ignored_output" >/dev/null || fail "migration reports that the custom config remains ignored"
if mise_path_active "$ignored_home" "$ignored_project"; then
fail "ignored config becomes active after migration"
fi
pass "migration preserves an explicit decision to ignore the Work config"
symlink_home="$test_dir/symlink-home"
symlink_config="$symlink_home/Work/.mise.toml"
symlink_target="$test_dir/dotfiles-mise.toml"
mkdir -p "$(dirname "$symlink_config")"
cat >"$symlink_target" <<'TOML'
[env]
_.path = "{{ cwd }}/bin"
KEEP = "yes"
TOML
ln -s "$symlink_target" "$symlink_config"
run_migration "$symlink_home" >/dev/null
[[ -L $symlink_config ]] || fail "migration preserves a dotfile symlink"
grep -F '{{ cwd }}/bin' "$symlink_target" >/dev/null && fail "symlink target retains the unsafe path"
grep -Fx 'KEEP = "yes"' "$symlink_target" >/dev/null || fail "symlink target keeps unrelated settings"
pass "custom dotfile symlinks survive the repair"
+18 -5
View File
@@ -7,6 +7,14 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin"
export QR_ROUTE_EXIT=0
# Loopback is never wireless, so exercise the connected-Wi-Fi fallback.
cat >"$tmp/bin/ip" <<'EOF'
#!/bin/bash
[[ $QR_ROUTE_EXIT == "0" ]] || exit "$QR_ROUTE_EXIT"
printf '1.1.1.1 dev lo\n'
EOF
cat >"$tmp/bin/nmcli" <<'EOF'
#!/bin/bash
@@ -28,13 +36,13 @@ payload=$(</dev/stdin)
printf '%s' "$payload" >"$QR_PAYLOAD_FILE"
printf '## \n ## \n ##\n'
EOF
chmod +x "$tmp/bin/nmcli" "$tmp/bin/qrencode"
chmod +x "$tmp/bin/ip" "$tmp/bin/nmcli" "$tmp/bin/qrencode"
run_success_case() {
local description=$1 fields=$2 expected_payload=$3
shift 3
local output meta matrix payload arg with_meta=false
local expected_matrix expected_security expected_ssid expected_iface="*"
local expected_matrix expected_security expected_ssid expected_iface="wlan0"
for arg in "$@"; do
[[ $arg == "--meta" ]] && with_meta=true || expected_iface=$arg
@@ -49,9 +57,8 @@ run_success_case() {
meta=$(head -n1 <<<"$output")
matrix=$(tail -n +2 <<<"$output")
# The meta line leads with the shared interface, security, and SSID. With
# no interface argument the helper detects one from the live host, so that
# field is only pinned when the case pinned it.
# The metadata reports the requested interface, or the connected Wi-Fi
# interface supplied by the nmcli stub when no interface was requested.
expected_security=${expected_payload#WIFI:T:}
expected_security=${expected_security%%;*}
expected_ssid=$(head -n1 <<<"$fields")
@@ -88,6 +95,12 @@ run_success_case \
'WIFI:T:WPA;S:Cafe Detected;P:secret;;' \
--meta
QR_ROUTE_EXIT=2 run_success_case \
"network QR helper detects Wi-Fi when route lookup fails" \
$'Cafe No Route\nwpa-psk\nsecret\nno\n' \
'WIFI:T:WPA;S:Cafe No Route;P:secret;;' \
--meta
run_success_case \
"network QR helper supports open networks" \
$'Cafe Open\nnone\n\nno\n' \
@@ -140,7 +140,6 @@ export OMARCHY_PTL_REBUILD_MARKER="$scratch/state/1789095456"
export OMARCHY_MIGRATION_STATE="$scratch/user-markers"
touch "$OMARCHY_PTL_REBUILD_MARKER" "$OMARCHY_MIGRATION_STATE/1789095456.sh"
cp "$migration" "$scratch/omarchy/migrations/"
[[ ! -e $ROOT/migrations/1789095456.sh ]] || fail "the superseded migration must not install the PTL variant first"
pending=$(OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" --pending)
[[ $pending == "1789325478.sh" ]] || fail "the renamed migration is pending after completing the old migration"
OMARCHY_PATH="$scratch/omarchy" "$ROOT/bin/omarchy-migrate" > "$scratch/output" 2>&1
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
packages="$ROOT/install/omarchy-base.packages"
migration="$ROOT/migrations/1788129995.sh"
grep -qxF omasnap "$packages" || fail "fresh installs include Omasnap"
! grep -qxF tensaku "$packages" || fail "fresh installs no longer include Tensaku"
! grep -qxF satty "$packages" || fail "fresh installs no longer include Satty"
pass "fresh installs use Omasnap as the screenshot tool"
grep -Fq 'namespace = "^omasnap$"' "$ROOT/default/hypr/apps/screenshot-selection.lua" ||
fail "Omasnap has a layer rule"
grep -Fq 'no_screen_share = true' "$ROOT/default/hypr/apps/screenshot-selection.lua" ||
fail "the Omasnap overlay is excluded from screen sharing"
! grep -Fq 'dev.tensaku.Tensaku' "$ROOT/default/hypr/apps/system.lua" ||
fail "the removed Tensaku window rules are gone"
pass "Hyprland applies Omasnap's overlay policy without stale Tensaku rules"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/omasnap" <<'SH'
#!/bin/bash
printf '%s\t%s\n' "${OMASNAP_SCREENSHOT_DIR:-}" "$*" >>"$OMASNAP_TEST_LOG"
SH
chmod +x "$stub_bin/omasnap"
capture_log="$test_tmp/capture.log"
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-capture-screenshot"
[[ $(<"$capture_log") == $'\t' ]] || fail "the default screenshot opens Omasnap without extra arguments"
: >"$capture_log"
OMASNAP_TEST_LOG="$capture_log" OMARCHY_SCREENSHOT_DIR="$test_tmp/legacy-output" PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-capture-screenshot" windows copy
[[ $(<"$capture_log") == "$test_tmp/legacy-output"$'\twindows --copy' ]] ||
fail "the screenshot command maps the legacy directory and copy argument to Omasnap"
: >"$capture_log"
OMASNAP_TEST_LOG="$capture_log" OMARCHY_SCREENSHOT_DIR="$test_tmp/legacy-output" OMASNAP_SCREENSHOT_DIR="$test_tmp/native-output" PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-capture-screenshot" fullscreen save
[[ $(<"$capture_log") == "$test_tmp/native-output"$'\tfullscreen --save' ]] ||
fail "the native Omasnap directory wins while legacy save syntax still works"
: >"$capture_log"
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-capture-screenshot" region slurp
[[ $(<"$capture_log") == $'\tregion' ]] || fail "the former default slurp argument remains a harmless compatibility no-op"
: >"$capture_log"
OMASNAP_TEST_LOG="$capture_log" PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-capture-screenshot" scroll --save
[[ $(<"$capture_log") == $'\tscroll --save' ]] || fail "native Omasnap modes and flags pass through unchanged"
pass "the Omarchy screenshot route delegates compatible arguments to Omasnap"
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf 'add\t%s\n' "$*" >>"$OMASNAP_MIGRATION_LOG"
exit "${OMASNAP_PACKAGE_STATUS:-0}"
SH
cat >"$stub_bin/omarchy-pkg-drop" <<'SH'
#!/bin/bash
printf 'drop\t%s\n' "$*" >>"$OMASNAP_MIGRATION_LOG"
SH
chmod +x "$stub_bin/omarchy-pkg-add" "$stub_bin/omarchy-pkg-drop"
migration_home="$test_tmp/home"
mkdir -p "$migration_home/.config/imv" "$migration_home/dotfiles"
cat >"$migration_home/dotfiles/imv.config" <<'EOF'
[binds]
# Edit the current image in Tensaku and quit the viewer
<Ctrl+e> = exec tensaku-edit "$imv_current_file" & ; quit
<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit
EOF
ln -s "$migration_home/dotfiles/imv.config" "$migration_home/.config/imv/config"
migration_log="$test_tmp/migration.log"
run_migration() {
: >"$migration_log"
OMASNAP_MIGRATION_LOG="$migration_log" HOME="$migration_home" PATH="$stub_bin:$PATH" \
bash -euo pipefail "$migration" >/dev/null
}
run_migration
[[ $(sed -n '1p' "$migration_log") == $'add\tomasnap' ]] || fail "the migration installs Omasnap first"
[[ $(sed -n '2p' "$migration_log") == $'drop\tsatty tensaku' ]] || fail "the migration removes Satty and Tensaku after Omasnap is ready"
grep -Fq '# Edit the current image in Omasnap and quit the viewer' "$migration_home/.config/imv/config" ||
fail "the migration updates the stock imv editor comment"
grep -Fq '<Ctrl+e> = exec omasnap "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
fail "the migration sends the stock imv edit binding to Omasnap"
grep -Fq '<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
fail "the migration preserves custom imv bindings"
[[ -L $migration_home/.config/imv/config ]] || fail "the migration preserves a dotfile-managed imv symlink"
[[ $(stat -c '%a' "$migration") == 644 ]] || fail "the Omasnap migration has mode 0644"
pass "the migration swaps packages and safely updates only the stock imv binding"
cat >"$migration_home/dotfiles/imv.config" <<'EOF'
[binds]
# Edit the current image in Satty and quit the viewer
<Ctrl+e> = exec satty --filename "$imv_current_file" & ; quit
<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit
EOF
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-before"
legacy_desktop="$migration_home/.local/share/applications/omasnap.desktop"
mkdir -p "$(dirname "$legacy_desktop")"
cat >"$legacy_desktop" <<'EOF'
[Desktop Entry]
Type=Application
Name=Omasnap
Exec=omasnap
NoDisplay=true
EOF
if OMASNAP_PACKAGE_STATUS=1 run_migration; then
fail "Omasnap install failure stops the migration"
fi
[[ $(<"$migration_log") == $'add\tomasnap' ]] || fail "install failure leaves old screenshot packages installed"
cmp -s "$test_tmp/imv-before" "$migration_home/dotfiles/imv.config" || fail "install failure leaves the imv binding untouched"
[[ -f $legacy_desktop ]] || fail "install failure preserves the existing launcher entry"
pass "Omasnap install failure preserves the previous screenshot setup"
run_migration
[[ ! -e $legacy_desktop && ! -L $legacy_desktop ]] || fail "the migration removes the old user-local Omasnap desktop entry"
pass "the migration removes the desktop entry that hides the packaged launcher"
grep -Fq '# Edit the current image in Omasnap and quit the viewer' "$migration_home/.config/imv/config" ||
fail "the migration updates the stock Satty comment"
grep -Fq '<Ctrl+e> = exec omasnap "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
fail "the migration replaces the stock Satty binding before removing Satty"
grep -Fq '<Ctrl+x> = exec custom-editor "$imv_current_file" & ; quit' "$migration_home/.config/imv/config" ||
fail "the Satty migration preserves custom bindings"
[[ -L $migration_home/.config/imv/config ]] || fail "the Satty migration preserves the imv symlink"
pass "the migration upgrades the stock Satty binding to Omasnap"
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-migrated"
run_migration
cmp -s "$test_tmp/imv-migrated" "$migration_home/dotfiles/imv.config" || fail "rerunning the migration preserves the updated imv config"
pass "the migration can be rerun"
printf '<Ctrl+e> = exec custom-editor "$imv_current_file" & ; quit\n' >"$migration_home/dotfiles/imv.config"
cp "$migration_home/dotfiles/imv.config" "$test_tmp/imv-custom"
run_migration
cmp -s "$test_tmp/imv-custom" "$migration_home/dotfiles/imv.config" || fail "the migration preserves a custom edit shortcut"
pass "custom imv edit shortcuts remain unchanged"
+90
View File
@@ -0,0 +1,90 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin" "$work/home"
export TEST_CALLS="$work/calls"
for command in omarchy-pkg-add omarchy-hook-install systemctl; do
cat >"$work/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$TEST_CALLS"
case "$*" in
'--user enable owed.service') [[ ${TEST_TTY:-0} == 0 ]] ;;
'--user is-active --quiet graphical-session.target') [[ ${TEST_TTY:-0} == 0 ]] ;;
'--user start owed.service') [[ ${TEST_START_FAIL:-0} == 0 ]] ;;
*) exit 0 ;;
esac
SH
chmod +x "$work/bin/$command"
done
migration="$ROOT/migrations/1789764927.sh"
run_migration() {
HOME="$work/home" PATH="$work/bin:$PATH" bash -euo pipefail "$migration"
}
run_migration
run_migration
grep -Fx 'omarchy-pkg-add owe owe-lockfeed' "$TEST_CALLS" >/dev/null
grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null
grep -Fx 'systemctl --user start owed.service' "$TEST_CALLS" >/dev/null
pass "migration installs OWE, its theme refresh hook, and starts the graphical session service"
: >"$TEST_CALLS"
TEST_TTY=1 run_migration
TEST_TTY=1 run_migration
[[ $(readlink "$work/home/.config/systemd/user/graphical-session.target.wants/owed.service") == /usr/lib/systemd/user/owed.service ]]
if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then
fail "TTY migration must not start the renderer"
fi
pass "TTY migrations enable the next login without starting a renderer"
: >"$TEST_CALLS"
OMARCHY_UPGRADE_TO_QUATTRO_LIVE=1 run_migration
if grep -F 'systemctl --user start' "$TEST_CALLS" >/dev/null; then
fail "Quattro upgrade must defer OWE until the new graphical session"
fi
pass "Quattro upgrade enables OWE without starting it in the old shell"
if TEST_START_FAIL=1 run_migration; then
fail "a failed live service start leaves the migration pending"
fi
pass "a failed live service start fails the migration"
: >"$TEST_CALLS"
HOME="$work/home" PATH="$work/bin:$PATH" bash "$ROOT/install/user/first-run/enable-user-units.sh"
grep -E '^systemctl --user enable --now .*owed.service' "$TEST_CALLS" >/dev/null
grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null
pass "fresh installs enable OWE and install the theme refresh hook"
cat >"$work/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
printf 'thumbnail\n' >>"$TEST_CALLS"
while (( $# )); do
case "$1" in
-o) output=$2; shift 2 ;;
*) shift ;;
esac
done
[[ ${TEST_POSTER_FAIL:-0} == 0 ]] || exit 1
printf 'poster\n' >"$output"
SH
chmod +x "$work/bin/ffmpegthumbnailer"
source_path="$work/video with 'quotes'.mp4"
printf 'source\n' >"$source_path"
poster() {
PATH="$work/bin:$PATH" XDG_CACHE_HOME="$work/cache" bash "$ROOT/shell/plugins/lock/poster.sh" "$source_path"
}
: >"$TEST_CALLS"
first=$(poster)
[[ -s $first && $(poster) == "$first" ]]
[[ $(wc -l <"$TEST_CALLS") == 1 ]]
printf 'changed source\n' >>"$source_path"
second=$(poster)
[[ -s $second && $second != "$first" && ! -e $first ]]
pass "lock posters are cached, refreshed after source changes, and old frames are evicted"
printf 'broken source\n' >>"$source_path"
if TEST_POSTER_FAIL=1 poster; then fail "failed poster generation must fail"; fi
[[ -z $(find "$work/cache" -name '.poster-*.jpg' -print -quit) ]]
pass "failed poster generation never publishes a partial image"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
if ! command -v quickshell >/dev/null 2>&1; then
skip "quickshell unavailable; skipping lock feed QML lifecycle"
exit 0
fi
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/config" "$work/runtime" "$work/home"
chmod 700 "$work/runtime"
cp "$SHELL_TEST_DIR/fixtures/owe-lock/shell.qml" "$work/config/shell.qml"
ln -s "$ROOT/shell/Ui" "$work/config/Ui"
ln -s "$ROOT/shell/Commons" "$work/config/Commons"
# Offscreen rendering exercises the real QML bindings without taking a lock
# or connecting to the user's compositor.
HOME="$work/home" XDG_RUNTIME_DIR="$work/runtime" \
OMARCHY_PATH="$ROOT" QT_QPA_PLATFORM=offscreen QT_QPA_PLATFORMTHEME= \
QT_STYLE_OVERRIDE= QT_QUICK_BACKEND=software \
timeout 15 quickshell -p "$work/config" --no-color >"$work/log" 2>&1 || {
cat "$work/log" >&2
fail "lock feed QML lifecycle runs"
}
if ! grep -q 'OWE_LOCK_TEST_PASS' "$work/log" || grep -q 'OWE_LOCK_TEST_FAIL' "$work/log"; then
cat "$work/log" >&2
fail "lock feed respects visibility and keeps its poster fallback"
fi
pass "lock feed respects visibility, power saver and blanking; hidden views release images"
+1 -1
View File
@@ -158,7 +158,7 @@ if script -qec true /dev/null >/dev/null 2>&1; then
fail "plugin add reached git clone for an option-shaped gum-prompt URL"
pass "plugin add guard rejects an option-shaped URL from the interactive prompt"
else
pass "script -qec unavailable; skipping the interactive gum-prompt guard case"
skip "script -qec unavailable; skipping the interactive gum-prompt guard case"
fi
# Legitimate URL forms pass the guard and reach git clone (stubbed, no network).
+1 -1
View File
@@ -156,7 +156,7 @@ pass "service facades resolve enabled clones without widening replacement-bar ac
require_compositor "plugin authentication boundary runtime test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping plugin authentication boundary runtime test"
skip "quickshell not installed; skipping plugin authentication boundary runtime test"
exit 0
fi
@@ -21,7 +21,7 @@ trap cleanup EXIT
require_compositor "plugin registry contract test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping plugin registry contract test"
skip "quickshell not installed; skipping plugin registry contract test"
exit 0
fi
+1 -1
View File
@@ -54,7 +54,7 @@ JS
require_compositor "pointer movement gate runtime test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping pointer movement gate runtime test"
skip "quickshell not installed; skipping pointer movement gate runtime test"
exit 0
fi
+1 -1
View File
@@ -20,7 +20,7 @@ trap cleanup EXIT
require_compositor "shell runtime smoke test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping shell runtime smoke test"
skip "quickshell not installed; skipping shell runtime smoke test"
exit 0
fi
+111
View File
@@ -299,3 +299,114 @@ grep -F 'move = { "(monitor_w-monitor_h*2/9-40)", "(monitor_h-monitor_h/4-40)" }
grep -F 'move = { "(monitor_w-monitor_h*3/10-40)", "(monitor_h-monitor_h*27/80-40)" }' "$webcam_rules" >/dev/null || \
fail "large webcam starts at its final corner position"
pass "webcam size rules place the initial window in its final corner"
# The stop path reads the recording state file back and uses its contents as a
# path -- ffmpeg writes beside it, `mv` replaces it, `rm -f` deletes its
# preview -- so it has to live in the per-user runtime directory rather than
# under a name in world-writable /tmp that another account can create first.
recording_dir="$tmp_dir/recordings"
mkdir -p "$recording_dir"
cat >"$stub_bin/pgrep" <<'SH'
#!/bin/bash
exit 1
SH
cat >"$stub_bin/omarchy-hyprland-monitor-focused" <<'SH'
#!/bin/bash
printf 'DP-1\n'
SH
cat >"$stub_bin/gpu-screen-recorder" <<'SH'
#!/bin/bash
for i in "$@"; do
[[ -n ${take_next:-} ]] && { : >"$i"; break; }
[[ $i == "-o" ]] && take_next=1
done
sleep 5
SH
cat >"$stub_bin/omarchy-shell" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$stub_bin"/pgrep "$stub_bin"/omarchy-hyprland-monitor-focused \
"$stub_bin"/gpu-screen-recorder "$stub_bin"/omarchy-shell
# Compare that name across the run rather than demanding it be absent: the
# whole point of the finding is that anyone can own it already, and a leftover
# from a pre-fix recording would red-light the fixed script.
tmp_state="/tmp/omarchy-screenrecord-filename"
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
fail "screen recording keeps no state under a fixed /tmp name"
pass "screen recording keeps no state under a fixed /tmp name"
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-filename ]] ||
fail "the recording state file lives in the per-user runtime directory" \
"$(ls -a "$XDG_RUNTIME_DIR")"
pass "the recording state file lives in the per-user runtime directory"
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename") == "$recording_dir"/* ]] ||
fail "the recording state file names the recording that was started" \
"$(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-filename")"
pass "the recording state file names the recording that was started"
# The :-/tmp fallback would reopen the hole this PR closes. A recording
# started without a session runtime dir has to land under the state directory.
state_home="$tmp_dir/state-home"
mkdir -p "$state_home/omarchy" "$tmp_dir/home-fallback"
chmod 755 "$state_home/omarchy"
tmp_state_before=$(stat -c '%y %s' "$tmp_state" 2>/dev/null || true)
env -u XDG_RUNTIME_DIR \
HOME="$tmp_dir/home-fallback" \
XDG_STATE_HOME="$state_home" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/gpu-screen-recorder" 2>/dev/null || true
[[ $(stat -c '%y %s' "$tmp_state" 2>/dev/null) == "$tmp_state_before" ]] ||
fail "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
pass "without a runtime dir, screen recording still keeps no state under a fixed /tmp name"
fallback_file="$state_home/omarchy/omarchy-screenrecord-filename"
[[ -s $fallback_file ]] ||
fail "without a runtime dir the recording state file lives in the state directory" \
"$(ls -la "$state_home/omarchy" 2>/dev/null || true)"
pass "without a runtime dir the recording state file lives in the state directory"
[[ $(<"$fallback_file") == "$recording_dir"/* ]] ||
fail "the fallback state file names the recording that was started" \
"$(<"$fallback_file")"
pass "the fallback state file names the recording that was started"
# The overlay resizer reads the region file the recorder writes, so the two
# have to resolve the same fallback as well as the same runtime dir.
: >"$OMARCHY_TEST_HYPRCTL_ARGS"
echo "800x600+100+100" >"$state_home/omarchy/omarchy-screenrecord-region"
env -u XDG_RUNTIME_DIR \
HOME="$tmp_dir/home-fallback" \
XDG_STATE_HOME="$state_home" \
"$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 "without a runtime dir the webcam anchors to the recorded region" "$(diff -u "$expected_hyprctl_args" "$OMARCHY_TEST_HYPRCTL_ARGS")"
fi
pass "without a runtime dir the webcam anchors to the recorded region"
mode=$(stat -c '%a' "$state_home/omarchy" 2>/dev/null || stat -f '%Lp' "$state_home/omarchy")
[[ $mode == "700" ]] || fail "fallback directory is private even when it already existed" "mode: $mode"
pass "fallback directory is private even when it already existed"
+11 -11
View File
@@ -21,17 +21,17 @@ trap cleanup EXIT
require_compositor "screenshot sanity test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping screenshot sanity test"
skip "quickshell not installed; skipping screenshot sanity test"
exit 0
fi
if pgrep -x slurp >/dev/null 2>&1; then
pass "slurp is already running; skipping screenshot sanity test"
if pgrep -x omasnap >/dev/null 2>&1; then
skip "omasnap is already running; skipping screenshot sanity test"
exit 0
fi
require_command hyprctl
require_command grim
require_command omasnap
require_command jq
require_command python3
@@ -118,13 +118,13 @@ jq -e '
fail_with_log "screenshot test shell rendered visible bar widgets"
}
screenshot=$(
OMARCHY_PATH="$test_root" \
OMARCHY_SCREENSHOT_DIR="$screenshot_dir" \
HOME="$test_home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy" capture screenshot fullscreen save 2>"$screenshot_err" | tail -n 1
)
OMARCHY_PATH="$test_root" \
OMASNAP_SCREENSHOT_DIR="$screenshot_dir" \
HOME="$test_home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy" capture screenshot fullscreen save >/dev/null 2>"$screenshot_err"
screenshot=$(find "$screenshot_dir" -maxdepth 1 -type f -name '*.png' -print -quit)
[[ -n $screenshot && -f $screenshot ]] || fail_with_log "fullscreen screenshot was captured"
+84
View File
@@ -0,0 +1,84 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
mkdir -p "$test_dir/test/shell.d"
cp "$ROOT/test/"{shell,all} "$test_dir/test/"
cp "$SHELL_TEST_DIR/base-test.sh" "$test_dir/test/shell.d/"
# Run the real runners in a tiny fixture tree, without recursing into this test.
run_suite() {
status=0
output=$(bash "$test_dir/test/${1:-shell}" 2>&1) || status=$?
}
run_suite
(( status == 1 )) && [[ $output == *"No shell tests found"* ]] ||
fail "an empty suite fails instead of counting the helper as a test" "$output"
pass "an empty suite fails instead of counting the helper as a test"
cat >"$test_dir/test/shell.d/a-pass-test.sh" <<'SH'
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
captured=$(skip "captured fixture output is not a skipped check")
pass "the command skips an optional action"
SH
run_suite
(( status == 0 )) && [[ $output == *"All 1 test files passed."* ]] ||
fail "passing assertions and captured skip output do not mark a file skipped" "$output"
pass "passing assertions and captured skip output do not mark a file skipped"
cat >"$test_dir/test/shell.d/b-partial-test.sh" <<'SH'
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
pass "static check"
skip "first unavailable runtime check"
skip "second unavailable runtime check"
pass "check after skips"
SH
cat >"$test_dir/test/shell.d/c-skipped-test.sh" <<'SH'
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
unset WAYLAND_DISPLAY
require_compositor "runtime fixture"
fail "unreachable after compositor skip"
SH
cat >"$test_dir/test/shell.d/z-last-test.sh" <<'SH'
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
pass "last file ran"
SH
run_suite
(( status == 0 )) && [[ $output == *"4 test files completed without failures; 2 had skipped checks."* ]] ||
fail "whole and partial skips are counted once per file and remain successful" "$output"
[[ $output == *"ok - check after skips"* && $output == *"ok - last file ran"* ]] ||
fail "skip returns normally and later files still run" "$output"
[[ $output == *$'Skipped checks in 2 of 4 test files:\n test/shell.d/b-partial-test.sh\n test/shell.d/c-skipped-test.sh\n'* ]] ||
fail "the summary identifies only files with skipped checks" "$output"
pass "whole and partial skips are visible without failing or stopping the suite"
cat >"$test_dir/test/shell.d/d-failed-test.sh" <<'SH'
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
skip "unavailable check before failure"
echo "failure detail" >&2
false
pass "unreachable after failure"
SH
run_suite
(( status == 1 )) && [[ $output == *$'1 of 5 test files failed:\n test/shell.d/d-failed-test.sh'* ]] ||
fail "a failure after a skip still fails the suite through the output pipe" "$output"
[[ $output == *"Skipped checks in 3 of 5 test files:"* && $output == *"failure detail"* && $output == *"ok - last file ran"* && $output != *"ok - unreachable after failure"* ]] ||
fail "failures preserve skip reporting, diagnostics, errexit, and later tests" "$output"
pass "failures remain fatal while skips and later test results stay visible"
printf '#!/bin/bash\necho "CLI fixture passed"\n' >"$test_dir/test/cli"
chmod +x "$test_dir/test/cli"
run_suite all
(( status == 1 )) && [[ $output == *"CLI fixture passed"* && $output == *"Skipped checks in 3 of 5 test files:"* && $output == *$'1 of 2 suites failed:\n test/shell'* ]] ||
fail "the aggregate runner preserves shell failures and skip reporting" "$output"
pass "the aggregate runner preserves shell failures and skip reporting"
+1 -1
View File
@@ -173,7 +173,7 @@ if locale -a 2>/dev/null | grep -qix 'en_US.utf-\?8'; then
pass "the accepted set does not move with the desktop's locale"
else
pass "no en_US.UTF-8 locale; skipping the locale-pinning check"
skip "no en_US.UTF-8 locale; skipping the locale-pinning check"
fi
# basename reads a leading dash as an option once the scp-style prefix is gone.
+2 -2
View File
@@ -25,7 +25,7 @@ trap cleanup EXIT
require_compositor "tray menu activation test"
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping tray menu activation test"
skip "quickshell not installed; skipping tray menu activation test"
exit 0
fi
@@ -36,7 +36,7 @@ python - <<'PY' || {
import dbus
import gi
PY
pass "python DBus bindings unavailable; skipping tray menu activation test"
skip "python DBus bindings unavailable; skipping tray menu activation test"
exit 0
}
+5 -2
View File
@@ -31,10 +31,13 @@ allowed = {
# them so the hook does not exist where it does not apply.
"/usr/lib/systemd/system-sleep",
# Written through a variable, so the scan below cannot see them at the point
# they are written. Both drop configuration into another project's tree rather
# than Omarchy's, which is why neither is a candidate for omarchy-settings.
# they are written. These drop configuration into another project's tree
# rather than Omarchy's and are not candidates for omarchy-settings.
"/usr/share/chromium/extensions",
"/usr/lib/firefox/distribution",
# Claude's extension is registered only when the user selects Claude.
"/usr/share/google-chrome/extensions",
"/usr/share/microsoft-edge/extensions",
# Static content that belongs in omarchy-settings. It cannot move there in the
# same release that first ships omarchy-update-system-pkgs-when-conflicted: the
# upgrade carrying the handler is the one that would hit the conflict, and the
+137
View File
@@ -0,0 +1,137 @@
#!/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/keyring.log"
mkdir -p "$stub_bin"
# Behavior is driven by env vars so each case can pick its failure point:
# KEYRING_TEST_PKG_MISSING exit status of omarchy-pkg-missing (default 1: installed)
# KEYRING_TEST_LIST_FAIL_ON which --list-keys call fails, counted per run (default: none)
# KEYRING_TEST_RECV_STATUS exit status of --recv-keys (default 0)
# KEYRING_TEST_REINSTALL_STATUS exit status of the archlinux-keyring reinstall (default 0)
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo' >>"$KEYRING_TEST_LOG"
for arg in "$@"; do
printf '\t%s' "$arg" >>"$KEYRING_TEST_LOG"
done
printf '\n' >>"$KEYRING_TEST_LOG"
if [[ $1 == "pacman-key" && $2 == "--list-keys" ]]; then
calls_file="$KEYRING_TEST_DIR/list-calls"
calls=$(( $(cat "$calls_file" 2>/dev/null || echo 0) + 1 ))
echo "$calls" >"$calls_file"
if [[ ${KEYRING_TEST_LIST_FAIL_ON:-} == "$calls" ]]; then
exit 1
fi
exit 0
fi
if [[ $1 == "pacman-key" && $2 == "--recv-keys" ]]; then
exit "${KEYRING_TEST_RECV_STATUS:-0}"
fi
if [[ $1 == "pacman-key" && $2 == "--lsign-key" ]]; then
exit 0
fi
if [[ $1 == "pacman" && $* == *archlinux-keyring* ]]; then
exit "${KEYRING_TEST_REINSTALL_STATUS:-0}"
fi
exit 0
SH
chmod +x "$stub_bin/sudo"
cat >"$stub_bin/omarchy-pkg-missing" <<'SH'
#!/bin/bash
exit "${KEYRING_TEST_PKG_MISSING:-1}"
SH
chmod +x "$stub_bin/omarchy-pkg-missing"
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
printf 'pkg-add\t%s\n' "$1" >>"$KEYRING_TEST_LOG"
exit 0
SH
chmod +x "$stub_bin/omarchy-pkg-add"
run_keyring() {
KEYRING_TEST_LOG="$log_file" \
KEYRING_TEST_DIR="$test_tmp" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-update-keyring" "$@"
}
# Everything healthy: the key and package are present, the reinstall works.
: >"$log_file"
rm -f "$test_tmp/list-calls"
run_keyring >"$test_tmp/ok.out"
grep -F "Keys are correct" "$test_tmp/ok.out" >/dev/null ||
fail "update-keyring reports success when the keyring is healthy" "$(cat "$test_tmp/ok.out")"
pass "update-keyring reports success when the keyring is healthy"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "update-keyring still reinstalls archlinux-keyring" "$(cat "$log_file")"
pass "update-keyring still reinstalls archlinux-keyring"
# Key and package missing: the full populate path runs and verifies at the end.
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_PKG_MISSING=0 run_keyring >"$test_tmp/populate.out"
grep -F "Keys are correct" "$test_tmp/populate.out" >/dev/null ||
fail "update-keyring populates a missing keyring and reports success" "$(cat "$test_tmp/populate.out")"
for expected in 'recv-keys' 'lsign-key' $'pkg-add\tomarchy-keyring'; do
grep -Eq "$expected" "$log_file" ||
fail "update-keyring populates a missing keyring and reports success" "$(cat "$log_file")"
done
pass "update-keyring populates a missing keyring and reports success"
# recv-keys failing must stop the script, not end in "Keys are correct".
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_PKG_MISSING=0 KEYRING_TEST_RECV_STATUS=1 run_keyring >"$test_tmp/recv.out" 2>&1; then
fail "update-keyring fails when recv-keys fails"
fi
if grep -F "Keys are correct" "$test_tmp/recv.out" >/dev/null; then
fail "update-keyring fails when recv-keys fails" "$(cat "$test_tmp/recv.out")"
fi
if grep -q 'lsign-key' "$log_file"; then
fail "update-keyring stops at the failed recv instead of signing anyway" "$(cat "$log_file")"
fi
pass "update-keyring fails when recv-keys fails"
# A failed archlinux-keyring reinstall must not end in success either.
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_REINSTALL_STATUS=1 run_keyring >"$test_tmp/reinstall.out" 2>&1; then
fail "update-keyring fails when the archlinux-keyring reinstall fails"
fi
if grep -F "Keys are correct" "$test_tmp/reinstall.out" >/dev/null; then
fail "update-keyring fails when the archlinux-keyring reinstall fails" "$(cat "$test_tmp/reinstall.out")"
fi
pass "update-keyring fails when the archlinux-keyring reinstall fails"
# The closing check is what backs the success line: the first --list-keys
# passes (key present, populate skipped), the verifying one fails.
: >"$log_file"
rm -f "$test_tmp/list-calls"
if KEYRING_TEST_LIST_FAIL_ON=2 run_keyring >"$test_tmp/verify.out" 2>&1; then
fail "update-keyring fails when the final key check fails"
fi
if grep -F "Keys are correct" "$test_tmp/verify.out" >/dev/null; then
fail "update-keyring fails when the final key check fails" "$(cat "$test_tmp/verify.out")"
fi
pass "update-keyring fails when the final key check fails"
+1 -1
View File
@@ -200,7 +200,7 @@ run_with_lock_env "$ROOT/bin/omarchy-update-stay-awake" stop
pass "stale update ownership preserves a newer Stay Awake choice"
# A stale PID is safe even if it has been reused by another process.
sleep 30 &
sleep 30 >/dev/null &
unrelated_pid=$!
unrelated_start_time=$(awk '{ print $22 }' "/proc/$unrelated_pid/stat")
mkdir -p "$stay_awake_helper_state"
+2
View File
@@ -28,6 +28,8 @@ else
expected_scope=""
fi
# The baseline has no locale override; forwarding is tested below.
unset LC_ALL
run_helper -Syu --noconfirm
[[ $(cat "$test_tmp/call") == "env OMARCHY_UPDATE_PACMAN=1 ${expected_scope}pacman -Syu --noconfirm" ]] ||
fail "helper composes the guarded pacman invocation" "$(cat "$test_tmp/call")"
+6 -2
View File
@@ -17,7 +17,7 @@ import tempfile
repo = Path(sys.argv[1])
source = (repo / 'bin/omarchy-system-factory-reset').read_text()
functions = []
for name in ['install_provisioning_units', 'sanitize_factory_baseline', 'stage_full_reset']:
for name in ['install_provisioning_units', 'scrub_factory_accounts', 'sanitize_factory_baseline', 'stage_full_reset']:
match = re.search(r'^' + name + r'\(\) \{\n.*?^\}', source, re.M | re.S)
assert match, name
functions.append(match.group())
@@ -66,7 +66,11 @@ btrfs() {
return 90
fi
}
passwd() { :; }
# Account cleanup has its own real-tool suite; keep this fixture focused on USB.
usermod() {
[[ $1 == --root && ( $2 == "$TOP_MNT/$NEXT_NAME" || $2 == "$TOP_MNT/@factory" ) &&
$3 == --password && $4 == '!' && $5 == root ]]
}
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
+49 -76
View File
@@ -9,15 +9,13 @@ const fs = require('fs')
const utilQml = fs.readFileSync(path.join(root, 'shell/Commons/Util.qml'), 'utf8')
const mediaQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundMedia.qml'), 'utf8')
const videoQml = fs.readFileSync(path.join(root, 'shell/Ui/BackgroundVideo.qml'), 'utf8')
const backgroundQml = fs.readFileSync(path.join(root, 'shell/plugins/background/Background.qml'), 'utf8')
const lockQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const lockFeedQml = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockFeedSurface.qml'), 'utf8')
const themeSwitcher = fs.readFileSync(path.join(root, 'bin/omarchy-theme-switcher'), 'utf8')
const quattroUpgrade = fs.readFileSync(path.join(root, 'bin/omarchy-upgrade-to-quattro'), 'utf8')
const multimediaMigration = fs.readFileSync(path.join(root, 'migrations/1786609204.sh'), 'utf8')
const barTextColor = fs.readFileSync(path.join(root, 'bin/omarchy-bar-text-color'), 'utf8')
const menuImages = fs.readFileSync(path.join(root, 'bin/omarchy-menu-images'), 'utf8')
const lockView = fs.readFileSync(path.join(root, 'shell/plugins/lock/LockView.qml'), 'utf8')
const lockService = fs.readFileSync(path.join(root, 'shell/plugins/lock/Service.qml'), 'utf8')
const batteryService = fs.readFileSync(path.join(root, 'shell/plugins/services/battery/Service.qml'), 'utf8')
const themeSet = fs.readFileSync(path.join(root, 'bin/omarchy-theme-set'), 'utf8')
@@ -29,54 +27,43 @@ assert(
'shared media helper identifies video paths without truncating valid local names'
)
assert(
videoQml.includes('loops: MediaPlayer.Infinite') &&
videoQml.includes('autoPlay: root.playbackEnabled') &&
videoQml.includes('fillMode: VideoOutput.PreserveAspectCrop') &&
/imageUrl: path && !Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
/videoUrl: path && Util\.isVideoPath\(path\) \? Util\.fileUrl\(path\) : ""/.test(mediaQml),
'background media plays aspect-cropped videos on a loop, and hands each loader only its own kind of file'
!fs.existsSync(path.join(root, 'shell/Ui/BackgroundVideo.qml')),
'the shell keeps no video player of its own'
)
assert(
videoQml.includes('MediaPlayer.LoadedMedia') &&
videoQml.includes('primePauseTimer') &&
videoQml.includes('mediaGeneration') &&
videoQml.includes('videoSink') &&
videoQml.includes('onVideoFrameChanged') &&
videoQml.includes('MediaPlayer.BufferedMedia') &&
videoQml.includes('mediaStatus !== MediaPlayer.BufferedMedia') &&
videoQml.includes('interval: 1000') &&
videoQml.includes('interval: 50') &&
videoQml.includes('frameReceived') &&
videoQml.includes('output.clearOutput()') &&
!videoQml.includes('KeepLastFrame') &&
/onPlaybackEnabledChanged:[\s\S]*?if \(playbackEnabled\) player\.play\(\)[\s\S]*?else player\.pause\(\)/.test(videoQml) &&
videoQml.includes('primingGeneration') &&
videoQml.includes('player.play()') &&
videoQml.includes('player.pause()'),
'paused video sources are primed to display their first frame'
/imageUrl: path && !video \? Util\.fileUrl\(path\) \+ \(version \? "\?v=" \+ version : ""\) : ""/.test(mediaQml) &&
/active: root\.path !== "" && !root\.video/.test(mediaQml) &&
!mediaQml.includes('videoUrl') &&
!mediaQml.includes('videoLoader') &&
!mediaQml.includes('videoEnabled'),
'background media is image-only: it loads stills and leaves videos to OWE'
)
assert(
!/^\s*import QtMultimedia/m.test(mediaQml) &&
mediaQml.includes('source: "BackgroundVideo.qml"'),
'the still-image path never imports QtMultimedia, so image-only sessions do not map it'
!/^\s*import QtMultimedia/m.test(backgroundQml) &&
!/^\s*import QtMultimedia/m.test(lockQml) &&
!backgroundQml.includes('MediaPlayer') &&
!lockQml.includes('MediaPlayer'),
'no shell surface builds a Qt Multimedia pipeline'
)
assert(
!/^\s*Video\s*\{/m.test(videoQml) &&
/property bool audioEnabled: false/.test(videoQml) &&
/property bool audioEnabled: false/.test(mediaQml) &&
/active: root\.audioEnabled && player\.hasAudio/.test(videoQml) &&
/audioOutput: audioLoader\.item/.test(videoQml) &&
/muted: root\.priming \|\| !root\.playbackEnabled/.test(videoQml) &&
/property: "audioEnabled"\s*\n\s*value: root\.audioEnabled/.test(mediaQml) &&
/firstScreen: Quickshell\.screens\.length > 0\s*\n\s*&& String\(Quickshell\.screens\[0\]\.name/.test(backgroundQml) &&
backgroundQml.includes('audioEnabled: panel.firstScreen') &&
!lockQml.includes('audioEnabled'),
'a sound track plays from the first monitor only, a silent file builds no audio output, and the lock stays quiet'
/^import Owe\.LockFeed$/m.test(lockFeedQml) &&
lockFeedQml.includes('LockFeed {') &&
lockQml.includes('source: "LockFeedSurface.qml"') &&
lockQml.includes('active: root.feedActive') &&
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml),
'the lock screen shows video through the OWE lock feed, loaded so a missing module costs only the video'
)
assert(
/property: "mediaSource"[\s\S]*?when: videoLoader\.item !== null && Util\.isVideoPath\(root\.path\)\s*\n\s*restoreMode: Binding\.RestoreNone/.test(mediaQml) &&
!videoQml.includes('Component.onDestruction'),
'a player on its way out keeps its source, so nothing is left loading for its destructor to cancel'
!/^import Owe\./m.test(lockQml),
'the lock view itself carries no foreign import, so the lock still loads without the feed module'
)
assert(
lockQml.includes('path: root.loadBackground ? (root.video ? root.videoPosterPath : root.backgroundPath) : ""') &&
lockQml.indexOf('id: feedLoader') > lockQml.indexOf('MultiEffect {') &&
lockQml.includes('visible: root.video') &&
!lockQml.includes('wallpaper.video'),
'the lock screen keeps its image effect for stills and shows the feed for videos'
)
assert(
!mediaQml.includes('mipmap'),
@@ -86,33 +73,21 @@ assert(
/instant \|\| !displayedBackground \|\| isVideo\(path\) \|\| isVideo\(displayedBackground\)[\s\S]*displayedBackground = finalPath/.test(backgroundQml),
'video switches bypass the image-only reveal stack and use the durable background path'
)
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share video-capable media rendering')
assert(backgroundQml.includes('BackgroundMedia {') && lockQml.includes('BackgroundMedia {'), 'desktop and lock screen share still rendering')
assert(
lockQml.includes('source: wallpaper.video ? null : wallpaper') &&
lockQml.includes('visible: !wallpaper.video') &&
lockQml.includes('visible: wallpaper.video'),
'lock screen bypasses its image effect for video output'
!backgroundQml.includes('videoEnabled') &&
!backgroundQml.includes('displayedReloads') &&
!mediaQml.includes('displayedReloads'),
'the desktop hands video backgrounds to OWE with no player switch to carry'
)
assert(
/sessionObscured:\s*lockActive \|\| screensaverActive/.test(backgroundQml) &&
backgroundQml.includes('playbackEnabled: !root.sessionObscured && !root.powerSaverActive && !panel.fullscreenHere') &&
backgroundQml.includes('omarchy.lock') &&
backgroundQml.includes('omarchy.idle') &&
backgroundQml.includes('omarchy.battery'),
'desktop playback stops while covered or on battery power-saver'
)
assert(
backgroundQml.includes('Hyprland.monitorFor(modelData)') &&
/fullscreenHere: visibleWorkspace \? visibleWorkspace\.hasFullscreen : false/.test(backgroundQml) &&
!backgroundQml.includes('ToplevelManager.activeToplevel'),
'a fullscreen window pauses only the output it covers, wherever focus is'
)
assert(
/if \(displayedBackground === finalPath\) displayedReloads \+= 1/.test(backgroundQml) &&
backgroundQml.includes('reloads: root.displayedReloads') &&
/active: root\.path !== "" && root\.video && !root\.reloading/.test(mediaQml) &&
/onReloadsChanged: \{[\s\S]*?reloading = true/.test(mediaQml),
'a theme switch that keeps the video path still reopens the replaced file'
!backgroundQml.includes('playbackEnabled') &&
!backgroundQml.includes('sessionObscured') &&
!backgroundQml.includes('fullscreenHere') &&
!backgroundQml.includes('Hyprland.monitorFor') &&
!backgroundQml.includes('omarchy.lock') &&
!backgroundQml.includes('omarchy.battery'),
'the desktop no longer carries the shell video pause policy'
)
assert(
barTextColor.includes('magick "$background_path[0]"'),
@@ -163,13 +138,12 @@ assert(
'a locked video wallpaper follows what each panel actually did, not only what the lock asked for'
)
assert(
lockView.includes('playbackEnabled: root.loadBackground && !root.displaysBlank') &&
lockView.includes('&& !root.powerSaverActive') &&
/feedActive: root\.video && root\.loadBackground && !root\.displaysBlank && !root\.powerSaverActive/.test(lockQml) &&
/displaysBlank: root\.screenBlank\(/.test(lockService) &&
/powerSaverActive: root\.powerSaverActive/.test(lockService) &&
/function runBlank\(\) \{\s*\n\s*root\.displaysBlank = true/.test(lockService) &&
/function runWake\(\) \{\s*\n\s*root\.displaysBlank = false/.test(lockService),
'the lock screen stops playback once displays go dark or power-saver is active'
'the lock feed stops once displays go dark or power-saver is active'
)
assert(
batteryService.includes('property string activePowerProfile') &&
@@ -186,10 +160,6 @@ assert(
'theme switcher previews video-only themes, named preview files included'
)
assert(quattroUpgrade.includes("-iname '*.mp4'"), 'Quattro upgrade can seed a video-only theme background')
assert(
multimediaMigration.includes('omarchy-pkg-add qt6-multimedia qt6-multimedia-ffmpeg'),
'existing Quattro installations receive video playback dependencies'
)
JS
test_tmp=$(mktemp -d)
@@ -303,8 +273,11 @@ timeout_rows=$(PATH="$test_tmp/bin:$PATH" XDG_CACHE_HOME="$timeout_cache" \
timeout_marker=$(find "$timeout_cache/omarchy/image-selector" -maxdepth 1 -type f -name '*.failed' -print -quit)
[[ -z $timeout_marker ]] || fail "a timed out video is left to retry rather than remembered as failed"
grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia runtime is a base package"
grep -qx 'qt6-multimedia-ffmpeg' "$ROOT/install/omarchy-base.packages" || fail "Qt Multimedia FFmpeg backend is a base package"
grep -qx 'owe' "$ROOT/install/omarchy-base.packages" || fail "OWE is a base package"
grep -qx 'owe-lockfeed' "$ROOT/install/omarchy-base.packages" || fail "the OWE lock feed module is a base package"
if grep -qx 'qt6-multimedia' "$ROOT/install/omarchy-base.packages"; then
fail "Qt Multimedia is no longer needed by the shell"
fi
pass "menu image generator creates thumbnails consumed by the picker"
pass "direct picker generates and reuses still thumbnails"
@@ -312,7 +285,7 @@ pass "direct picker omits videos whose thumbnails cannot be generated"
pass "a rejected video is remembered so it costs nothing on the next open"
pass "a timed out video is left to retry"
pass "a locked video wallpaper follows the panels' real DPMS state"
pass "Qt Multimedia playback dependencies are declared"
pass "OWE and its lock feed module are declared"
source <(awk '
/^(is_video_path|snapshot_background_path|background_transition_uses_snapshots|choose_theme_background|choose_staged_theme_background|set_theme_background)\(\) \{/ { copying=1 }
+2 -2
View File
@@ -11,7 +11,7 @@ if [[ ${OMARCHY_WINDOWS_TEST_NAMESPACE:-0} != 1 ]]; then
exec env OMARCHY_WINDOWS_TEST_NAMESPACE=1 \
unshare --user --map-current-user --keep-caps --mount --propagation private bash "$0"
fi
pass "unprivileged mount namespaces unavailable; skipping Windows VM mount runtime tests"
skip "unprivileged mount namespaces unavailable; skipping Windows VM mount runtime tests"
exit 0
fi
@@ -241,7 +241,7 @@ race_swaps="$TMPDIR/concurrent-race-swaps"
sleep 0.005
fi
done
) &
) >/dev/null &
racer_pid=$!
concurrent_dc_calls=0
dc() {
@@ -8,7 +8,7 @@ if ((EUID != 0)); then
if unshare --user --map-auto --map-root-user --mount true 2>/dev/null; then
exec unshare --user --map-auto --map-root-user --mount --propagation private bash "$0"
fi
pass "automatic subordinate-id namespace unavailable; skipping root Windows VM boundary probe"
skip "automatic subordinate-id namespace unavailable; skipping root Windows VM boundary probe"
exit 0
fi