Merge quattro into update security foundation
This commit is contained in:
commit
56ca654dc8
87 files changed
+923
-1071
No files matched your search
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ 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
|
||||
pass "user namespaces unavailable; skipping isolated Claude extension installation"
|
||||
skip "user namespaces unavailable; skipping isolated Claude extension installation"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -8,7 +8,7 @@ if (( EUID != 0 )); then
|
||||
if unshare --user --map-root-user true 2>/dev/null; then
|
||||
exec unshare --user --map-root-user bash "$0"
|
||||
fi
|
||||
pass "no unprivileged user namespace; skipping factory account cleanup"
|
||||
skip "no unprivileged user namespace; skipping factory account cleanup"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
|
||||
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -212,7 +212,7 @@ run_with_lock_env "$SUDO_TEST_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"
|
||||
|
||||
@@ -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")"
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
Reference in new issue
Block a user