Merge current Quattro while preserving command checks

This commit is contained in:
Afonso Oliveira committed 2026-09-07 11:50:18 +01:00
commit aa422fcd4e
63 files changed
+4644 -353

No files matched your search

+8
View File
@@ -55,6 +55,14 @@ const entries = [
}
]
// Keep the packaged launcher when upstream rebuilds register their own entry.
const configuredHides = new Set(fs.readFileSync(path.join(root, 'default/omarchy/launcher.hides'), 'utf8').trim().split(/\n/))
const hermesEntries = [{ name: 'Hermes', id: 'hermes' }, { name: 'Hermes', id: 'hermes-desktop' }]
for (const query of ['', 'hermes']) {
const visible = search.sortedEntries(hermesEntries, query, entry => configuredHides.has(entry.id))
assertDeepEqual(visible.map(row => row.entry.id), ['hermes-desktop'], 'only the packaged Hermes launcher is visible')
}
const contactMatches = search.sortedEntries(entries, 'contact').map(row => search.entryName(row.entry))
assertDeepEqual(contactMatches, ['Google Contacts'], 'contact search only returns direct contact matches')
+1
View File
@@ -151,6 +151,7 @@ package_defaults = [
("default/systemd/user/omarchy-fcitx5.service", "/usr/lib/systemd/user/omarchy-fcitx5.service", "systemd/user/omarchy-fcitx5.service"),
("default/systemd/user/omarchy-crash-watch.service", "/usr/lib/systemd/user/omarchy-crash-watch.service", "systemd/user/omarchy-crash-watch.service"),
("default/systemd/zram-generator.conf.d/90-omarchy.conf", "/usr/lib/systemd/zram-generator.conf.d/90-omarchy.conf", "systemd/zram-generator.conf.d/90-omarchy.conf"),
("default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "/usr/lib/systemd/system/plocate-updatedb.service.d/10-omarchy.conf", "systemd/system/plocate-updatedb.service.d/10-omarchy.conf"),
("default/fonts/omarchy/omarchy.ttf", "/usr/share/fonts/omarchy/omarchy.ttf", "omarchy.ttf"),
("default/snapper/root", "/etc/snapper/config-templates/omarchy", "snapper/root"),
]
@@ -0,0 +1,58 @@
import os
from pathlib import Path
from tempfile import TemporaryDirectory
from kitty.config import load_config
from kitty.options.utils import parse_map
root = Path(os.environ['ROOT'])
system = root / 'etc/xdg/kitty/kitty.conf'
template = root / 'config/kitty/kitty.conf'
legacy = root / 'test/shell.d/fixtures/kitty/legacy.conf'
active_lines = [line for line in template.read_text().splitlines() if line and not line.startswith('#')]
assert active_lines == ['include ~/.local/state/omarchy/current/theme/kitty.conf']
with TemporaryDirectory() as tmp:
user = Path(tmp) / 'kitty.conf'
# Use a local theme to avoid depending on the developer's generated state.
theme = Path(tmp) / 'theme.conf'
theme.write_text('background #123456\n')
themed = f'include {theme}\n'
user.write_text(themed)
errors = []
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert not errors, errors
assert opts.allow_remote_control == 'socket-only'
assert opts.listen_on == 'unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}'
old = Path(tmp) / 'legacy.conf'
old.write_text(legacy.read_text().replace(active_lines[0], themed.strip()))
before = load_config(str(old), accumulate_bad_lines=errors)
# Moving defaults must preserve appearance and behavior except remote control.
for key in ('font_family', 'bold_italic_font', 'font_size', 'window_padding_width',
'hide_window_decorations', 'confirm_os_window_close', 'cursor_shape',
'cursor_blink_interval', 'shell_integration', 'enable_audio_bell',
'tab_bar_edge', 'tab_bar_style', 'tab_powerline_style',
'tab_title_template', 'background'):
assert getattr(opts, key) == getattr(before, key), key
def binding(options, shortcut):
trigger = next(parse_map(shortcut)).trigger
return [entry.definition for entry in options.keyboard_modes[''].keymap.get(trigger, [])]
for shortcut in ('ctrl+insert', 'shift+insert', 'shift+enter', 'alt+shift+enter'):
assert binding(opts, shortcut) == binding(before, shortcut), shortcut
user.write_text(themed + 'font_size 15\nmap ctrl+insert\nmap shift+insert copy_to_clipboard\n')
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert opts.font_size == 15
assert not any(binding(opts, 'ctrl+insert'))
assert binding(opts, 'shift+insert')[-1] == 'copy_to_clipboard'
user.write_text(themed + 'clear_all_shortcuts yes\nmap f1 new_window\n')
opts = load_config(str(system), str(user), accumulate_bad_lines=errors)
assert len(opts.keyboard_modes[''].keymap) == 1
assert binding(opts, 'f1') == ['new_window']
assert not errors, errors
print('ok - Kitty loads defaults and theme, preserves appearance, and supports user overrides and unmapping')
+35
View File
@@ -0,0 +1,35 @@
include ~/.local/state/omarchy/current/theme/kitty.conf
# Font
font_family JetBrainsMono Nerd Font
bold_italic_font auto
font_size 9.0
# Window
window_padding_width 14
hide_window_decorations yes
confirm_os_window_close 0
# Keybindings
map ctrl+insert copy_to_clipboard
map shift+insert paste_from_clipboard
# Send Shift+Enter as CSI-u so TUIs can distinguish it from Enter.
map shift+enter send_text all \e[13;2u
# Kitty legacy encoding sends Alt+Shift+Enter the same as Alt+Enter; send CSI-u so tmux can match M-S-Enter.
map alt+shift+enter send_text all \e[13;4u
# Allow remote access
allow_remote_control yes
listen_on unix:${XDG_RUNTIME_DIR}/omarchy-kitty-{kitty_pid}
# Aesthetics
cursor_shape block
cursor_blink_interval 0
shell_integration no-cursor
enable_audio_bell no
# Minimal Tab bar styling
tab_bar_edge bottom
tab_bar_style powerline
tab_powerline_style slanted
tab_title_template {title}{' :{}:'.format(num_windows) if num_windows > 1 else ''}
@@ -0,0 +1,20 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function retain(id, service) {
AuthServiceStore.put(id, service)
}
function has(id) {
return AuthServiceStore.has(id)
}
function isTrusted(id) {
return AuthServiceStore.isTrusted(id)
}
function updateManifest(id, manifest) {
AuthServiceStore.updateManifest(id, manifest)
}
}
@@ -0,0 +1,8 @@
import QtQuick
import "services/AuthServiceStore.js" as AuthServiceStore
QtObject {
function has(id) {
return AuthServiceStore.has(id)
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
import Quickshell.Io
import "services"
ShellRoot {
id: root
property var calls: []
property QtObject ownService: QtObject {
property string marker: "own"
property var manifest: null
}
AuthStoreOwner { id: authStoreOwner }
AuthStoreReader { id: authStoreReader }
Component {
id: apiComponent
PluginShellApi { }
}
FileView {
id: resultFile
path: Quickshell.env("OMARCHY_QML_TEST_RESULT")
atomicWrites: true
}
Component.onCompleted: {
var caller = "example.safe"
authStoreOwner.retain("omarchy.lock", root.ownService)
authStoreOwner.updateManifest("omarchy.lock", { version: "kept" })
var api = apiComponent.createObject(null, {
pluginId: caller,
idleConfig: { screensaver: 60, lock: 120 },
_serviceLookup: function(requestedId) {
return requestedId === caller ? root.ownService : null
},
_summon: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["summon"])
return true
},
_hide: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["hide"])
return true
},
_toggle: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["toggle"])
return true
},
_isOpen: function(requestedId) { return requestedId === caller },
_updateSettings: function(requestedId) {
if (requestedId !== caller) return false
root.calls = root.calls.concat(["settings"])
return true
}
})
var own = api.serviceFor(caller)
var result = {
detached: api.parent === undefined || api.parent === null,
ownService: own && own.marker === "own",
foreignService: api.serviceFor("omarchy.lock") === null,
firstPartyService: api.firstPartyServiceFor("omarchy.polkit") === null,
ownSummon: api.summon(caller, "{}") === true,
foreignSummon: api.summon("omarchy.lock", "{}") === false,
ownHide: api.hide(caller) === true,
foreignHide: api.hide("omarchy.lock") === false,
ownToggle: api.toggle(caller, "{}") === true,
foreignToggle: api.toggle("omarchy.lock", "{}") === false,
ownOpen: api.isPluginOpen(caller) === true,
foreignOpen: api.isPluginOpen("omarchy.lock") === false,
ownSettings: api.updateEntryInline(caller, {}) === true,
foreignSettings: api.updateEntryInline("omarchy.lock", {}) === false,
detachedIdleConfig: api.idleConfig.screensaver === 60 && api.idleConfig.lock === 120,
authStoreOwnerRetains: authStoreOwner.has("omarchy.lock") === true,
authStoreOwnerRemembersTrust: authStoreOwner.isTrusted("omarchy.lock") === true,
authStoreOwnerUpdatesManifest: root.ownService.manifest
&& root.ownService.manifest.version === "kept",
authStoreImportIsolated: authStoreReader.has("omarchy.lock") === false,
noGenericPluginShellFactory: typeof api.pluginShellForId !== "function",
calls: root.calls
}
result.ok = Object.keys(result).every(function(key) {
return key === "ok" || key === "calls" || result[key] === true
}) && JSON.stringify(result.calls) === JSON.stringify(["summon", "hide", "toggle", "settings"])
resultFile.setText(JSON.stringify(result))
}
}
@@ -83,6 +83,9 @@ ShellRoot {
scan += block("firstparty", "/first/bar", manifest("omarchy.bar", ["bar"], { bar: "Bar.qml" }))
scan += block("firstparty", "/first/panels/grouped", manifest("omarchy.grouped-panel", ["panel"], { panel: "Panel.qml" }))
scan += block("firstparty", "/first/hybrid", manifest("omarchy.hybrid", ["menu", "bar-widget"], { menu: "Menu.qml", barWidget: "Widget.qml" }))
var futureAuth = manifest("omarchy.future-auth", ["service"], { service: "Service.qml" })
futureAuth.omarchy = { capabilities: ["authentication"] }
scan += block("firstparty", "/first/future-auth", futureAuth)
scan += block("thirdparty", "/third/panel", manifest("third.panel", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/widget", manifest("third.widget", ["bar-widget"], { barWidget: "Widget.qml" }, { defaultSection: "left" }))
scan += block("thirdparty", "/third/center-widget", manifest("third.center-widget", ["bar-widget"], { barWidget: "Widget.qml" }))
@@ -103,6 +106,12 @@ ShellRoot {
localBar.omarchy = { clonedFrom: "omarchy.bar" }
scan += block("thirdparty", "/third/local-bar", localBar)
scan += block("thirdparty", "/third/bar", manifest("third.bar", ["bar"], { bar: "Bar.qml" }))
var localFutureAuth = manifest("local.future-auth", ["service"], { service: "Service.qml" })
localFutureAuth.omarchy = { clonedFrom: "omarchy.future-auth" }
scan += block("thirdparty", "/third/local-future-auth", localFutureAuth)
var spoofedAuth = manifest("third.spoofed-auth", ["service"], { service: "Service.qml" })
spoofedAuth.omarchy = { capabilities: ["authentication"] }
scan += block("thirdparty", "/third/spoofed-auth", spoofedAuth)
scan += block("thirdparty", "/third/shadow", manifest("omarchy.first-widget", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/reserved", manifest("omarchy.reserved", ["panel"], { panel: "Panel.qml" }))
scan += block("thirdparty", "/third/unsafe", manifest("third.unsafe", ["panel"], { panel: "../Panel.qml" }))
@@ -116,22 +125,28 @@ ShellRoot {
root.assertDeepEqual(pluginIds(), [
"local.bar",
"local.first-widget",
"local.future-auth",
"local.grouped-panel",
"local.hybrid",
"local.weather",
"omarchy.bar",
"omarchy.first-widget",
"omarchy.future-auth",
"omarchy.grouped-panel",
"omarchy.hybrid",
"third.bar",
"third.center-widget",
"third.panel",
"third.right-widget",
"third.spoofed-auth",
"third.widget"
], "registry merges valid first-party and third-party manifests")
root.assertTrue(registry.installedPlugins["omarchy.first-widget"].__isFirstParty === true, "first-party manifests are stamped")
root.assertTrue(registry.installedPlugins["third.panel"].__isFirstParty === false, "third-party manifests are stamped")
root.assertDeepEqual(registry.installedPlugins["omarchy.future-auth"].__hostCapabilities, ["authentication"], "trusted manifests stamp authentication capability")
root.assertDeepEqual(registry.installedPlugins["local.future-auth"].__hostCapabilities, ["authentication"], "clones inherit trusted host capabilities")
root.assertDeepEqual(registry.installedPlugins["third.spoofed-auth"].__hostCapabilities, [], "third-party manifests cannot self-grant host capabilities")
root.assertEqual(registry.installedPlugins["omarchy.grouped-panel"].__sourceDir, "/first/panels/grouped", "grouped plugin source paths are preserved")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.panel"], "panel"), "file:///third/panel/Panel.qml", "entryPointUrl resolves plugin-relative paths")
root.assertEqual(registry.entryPointUrl(registry.installedPlugins["third.widget"], "barWidget"), "file:///third/widget/Widget.qml", "entryPointUrl resolves bar widget paths")
+359
View File
@@ -0,0 +1,359 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
for command in git jq python3; do require_command "$command"; done
test_tmp=$(mktemp -d)
trap 'rm -rf -- "$test_tmp"' EXIT
export OMARCHY_TEST_ROOT="$test_tmp"
mkdir -p "$test_tmp/bin" "$test_tmp/package/resources" "$test_tmp/share" "$test_tmp/seed"
# Real Git exercises patch checks and preservation; all package, desktop and
# service commands are mocks. No command reaches the live user installation.
git -C "$test_tmp/seed" init -q -b main
printf 'venv/\n.hermes-bootstrap-complete\napps/desktop/release/\n__pycache__/\n' >"$test_tmp/seed/.gitignore"
printf 'before\n' >"$test_tmp/seed/runtime.txt"
mkdir -p "$test_tmp/seed/apps/desktop/src"
printf 'desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
mkdir -p "$test_tmp/seed/hermes_cli"
cat >"$test_tmp/seed/hermes_cli/main.py" <<'PY'
import os
from pathlib import Path
def _write_desktop_build_stamp(project_root, *, source_mode):
home = Path(os.environ['HERMES_HOME'])
assert project_root == home / 'hermes-agent'
assert source_mode is False
assert (project_root / 'apps/desktop/release/linux-unpacked/resources/app.asar').is_file()
(home / 'desktop-build-stamp.json').write_text('upstream build stamp')
with (Path(os.environ['OMARCHY_TEST_ROOT']) / 'events').open('a') as log:
log.write('build-stamp\n')
PY
git -C "$test_tmp/seed" add .
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm fixture
release_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
printf 'after\n' >"$test_tmp/seed/runtime.txt"
git -C "$test_tmp/seed" diff >"$test_tmp/share/runtime.patch"
printf 'before\n' >"$test_tmp/seed/runtime.txt"
printf 'newer desktop source\n' >"$test_tmp/seed/apps/desktop/src/main.js"
git -C "$test_tmp/seed" add apps/desktop/src/main.js
git -C "$test_tmp/seed" -c user.name=Test -c user.email=test@example.invalid commit -qm newer-main
origin_commit=$(git -C "$test_tmp/seed" rev-parse HEAD)
export OMARCHY_TEST_RELEASE_COMMIT="$release_commit"
printf '{"branch":"main","commit":"%s"}\n' "$release_commit" >"$test_tmp/package/resources/install-stamp.json"
printf 'packaged app\n' >"$test_tmp/package/resources/app.asar"
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/package/Hermes"
touch "$test_tmp/package/chrome-sandbox"
chmod 755 "$test_tmp/package/Hermes"
chmod 4755 "$test_tmp/package/chrome-sandbox"
cat >"$test_tmp/share/install.sh" <<'MOCK'
#!/bin/bash
set -e
printf 'bootstrap\n' >>"$OMARCHY_TEST_ROOT/events"
printf '%s\n' "$@" >"$OMARCHY_TEST_ROOT/install-args"
[[ ${OMARCHY_TEST_INSTALL_FAIL:-0} != 1 ]] || exit 7
commit=$OMARCHY_TEST_RELEASE_COMMIT
force=false
while (( $# )); do
case "$1" in
--dir) runtime=$2; shift ;;
--commit) commit=$2; shift ;;
--force-commit) force=true ;;
--hermes-home) [[ $2 == "$HERMES_HOME" ]] ;;
esac
shift
done
mkdir -p -- "${runtime%/*}"
if [[ ! -d $runtime ]]; then
git clone -q --depth 1 "file://$OMARCHY_TEST_ROOT/seed" "$runtime"
else
git -C "$runtime" checkout -q main
git -C "$runtime" pull -q --ff-only origin main
fi
git -C "$runtime" fetch -q origin "$commit"
if [[ $force == true ]] || ! git -C "$runtime" merge-base --is-ancestor "$commit" HEAD; then
git -C "$runtime" checkout -q --detach "$commit"
fi
mkdir -p "$runtime/venv/bin"
git -C "$runtime" rev-parse HEAD >"$runtime/venv/dependency-commit"
printf '#!/bin/bash\nexit 0\n' >"$runtime/venv/bin/hermes"
chmod +x "$runtime/venv/bin/hermes"
printf '#!/bin/bash\nexec /usr/bin/python3 "$@"\n' >"$runtime/venv/bin/python"
chmod +x "$runtime/venv/bin/python"
[[ ${OMARCHY_TEST_NO_MARKER:-0} == 1 ]] || touch "$runtime/.hermes-bootstrap-complete"
mkdir -p "$HOME/.local/bin"
for command in hermes hermes-agent hermes-acp; do
rm -f "$HOME/.local/bin/$command"
printf 'native runtime shim\n' >"$HOME/.local/bin/$command"
done
MOCK
cat >"$test_tmp/bin/omarchy-pkg-add" <<'MOCK'
#!/bin/bash
printf 'package %s\n' "$*" >>"$OMARCHY_TEST_ROOT/events"
[[ ${OMARCHY_TEST_PACKAGE_FAIL:-0} != 1 ]]
MOCK
cat >"$test_tmp/bin/git" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_FETCH_FAIL:-0} == 1 && " $* " == *" --unshallow "* ]]; then exit 8; fi
exec /usr/bin/git "$@"
MOCK
cat >"$test_tmp/bin/omarchy-install-hermes-cli" <<'MOCK'
#!/bin/bash
printf 'handoff\n' >>"$OMARCHY_TEST_ROOT/events"
exit 1
MOCK
cat >"$test_tmp/bin/setsid" <<'MOCK'
#!/bin/bash
exec "$@"
MOCK
cat >"$test_tmp/bin/cp" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_COPY_FAIL:-0} == 1 ]]; then
touch "${@: -1}/partial-copy"
exit 9
fi
exec /usr/bin/cp "$@"
MOCK
cat >"$test_tmp/bin/mv" <<'MOCK'
#!/bin/bash
if [[ ${OMARCHY_TEST_COPY_RACE:-0} == 1 && $1 == -T ]]; then
mkdir -p "${@: -1}"
fi
exec /usr/bin/mv "$@"
MOCK
cat >"$test_tmp/bin/uwsm-app" <<'MOCK'
#!/bin/bash
[[ $1 == -- ]] || exit 1
shift
exec "$@"
MOCK
cat >"$test_tmp/bin/hermes-desktop" <<'MOCK'
#!/bin/bash
sleep 0.05
native="$HERMES_HOME/hermes-agent/apps/desktop/release/linux-unpacked"
if [[ -x $native/Hermes && -f $native/resources/app.asar ]]; then
printf 'launch\n' >>"$OMARCHY_TEST_ROOT/events"
else
printf 'launch-before-copy\n' >>"$OMARCHY_TEST_ROOT/events"
fi
MOCK
cat >"$test_tmp/bin/systemctl" <<'MOCK'
#!/bin/bash
printf 'theme-stop\n' >>"$OMARCHY_TEST_ROOT/events"
MOCK
cat >"$test_tmp/bin/systemd-run" <<'MOCK'
#!/bin/bash
printf 'theme-start\n' >>"$OMARCHY_TEST_ROOT/events"
# Join the mock asynchronous launch so every test owns its full lifetime.
for (( attempt=0; attempt<100; attempt++ )); do
if grep -q '^launch' "$OMARCHY_TEST_ROOT/events"; then exit 0; fi
sleep 0.01
done
exit 1
MOCK
chmod +x "$test_tmp/bin/"*
# Substitute only system package paths in a scratch copy of the actual script.
python3 - "$ROOT/bin/omarchy-install-ai-hermes" "$test_tmp" <<'PY'
from pathlib import Path
import sys
source, scratch = Path(sys.argv[1]), Path(sys.argv[2])
script = source.read_text()
for original, replacement in {
'/opt/hermes-desktop': str(scratch / 'package'),
'/usr/share/hermes-desktop': str(scratch / 'share'),
'/usr/bin/hermes-desktop': str(scratch / 'bin/hermes-desktop'),
}.items():
script = script.replace(original, replacement)
(scratch / 'installer').write_text(script)
PY
new_home() {
test_home="$test_tmp/$1"
hermes_home="$test_home/.hermes"
runtime="$hermes_home/hermes-agent"
native="$runtime/apps/desktop/release/linux-unpacked"
mkdir -p "$test_home"
: >"$test_tmp/events"
}
run_installer() {
HOME="$test_home" HERMES_HOME="${OMARCHY_TEST_HOME:-$hermes_home}" PATH="$test_tmp/bin:$PATH" \
bash "$test_tmp/installer" >"$test_tmp/output" 2>&1
}
assert_stopped() {
if grep -Eq '^(launch|theme-|build-stamp)' "$test_tmp/events"; then fail "$1"; fi
}
new_home fresh
run_installer || fail "fresh setup succeeds" "$(cat "$test_tmp/output")"
expected=$(printf '%s\n' --skip-setup --branch main --commit "$release_commit" --force-commit --dir "$runtime" --hermes-home "$hermes_home")
[[ $(cat "$test_tmp/install-args") == "$expected" ]] || fail "upstream installer receives the pinned main arguments"
[[ $(head -3 "$test_tmp/events") == $'package hermes-desktop\nhandoff\nbootstrap' ]] || fail "package and CLI handoff precede runtime bootstrap"
grep -qx launch "$test_tmp/events" || fail "native app is copied before launch"
[[ $(sed -n '4p' "$test_tmp/events") == build-stamp ]] || fail "upstream build stamp follows the app copy and precedes launch"
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'upstream build stamp' ]] || fail "the upstream helper records the completed packaged build"
[[ $(cat "$runtime/runtime.txt") == after ]] || fail "the release runtime receives its patch"
[[ $(stat -c %a "$native/chrome-sandbox") == 755 ]] || fail "the user sandbox is not setuid"
[[ $(stat -c %a "$test_tmp/package/chrome-sandbox") == 4755 ]] || fail "package sandbox permissions remain unchanged"
[[ $(git -C "$runtime" symbolic-ref --short HEAD) == main && $(git -C "$runtime" rev-parse main) == "$release_commit" ]] || fail "main starts at the release rather than the clone tip"
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "dependencies are installed for the release"
[[ $(git -C "$runtime" rev-parse --is-shallow-repository) == false ]] || fail "first update has connected history"
# Reproduce the updater's checkout/count/pull sequence while origin stays put.
git clone -q "$runtime" "$test_tmp/first-update"
git -C "$test_tmp/first-update" remote set-url origin "file://$test_tmp/seed"
git -C "$test_tmp/first-update" fetch -q origin main
git -C "$test_tmp/first-update" checkout -q main
[[ $(git -C "$test_tmp/first-update" rev-list HEAD..origin/main --count) == 1 ]] || fail "first update detects work even when origin has not moved since install"
git -C "$test_tmp/first-update" pull -q --ff-only origin main
[[ $(git -C "$test_tmp/first-update" rev-parse HEAD) == "$origin_commit" ]] || fail "first update fast-forwards to origin"
pass "fresh setup pins main, patches the matching runtime and copies the complete app before launch"
printf 'user app\n' >"$native/resources/app.asar"
printf 'user build stamp\n' >"$hermes_home/desktop-build-stamp.json"
: >"$test_tmp/events"
run_installer || fail "repeat setup succeeds" "$(cat "$test_tmp/output")"
! grep -qx bootstrap "$test_tmp/events" || fail "repeat setup does not bootstrap again"
! grep -qx build-stamp "$test_tmp/events" || fail "existing app never reruns the build stamp writer"
[[ $(cat "$hermes_home/desktop-build-stamp.json") == 'user build stamp' ]] || fail "existing native build stamp remains unchanged"
[[ $(cat "$native/resources/app.asar") == 'user app' ]] || fail "existing native app remains unchanged"
pass "repeat setup accepts the applied patch and preserves the existing native app"
# Advancing the runtime must never reinstall the release or reapply its patch.
printf 'new main\n' >"$runtime/runtime.txt"
git -C "$runtime" add runtime.txt
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm update
: >"$test_tmp/events"
run_installer || fail "a complete updated runtime and native app are reused"
[[ $(cat "$runtime/runtime.txt") == 'new main' ]] || fail "updated runtime is not release-patched"
mv "$native" "$test_tmp/saved-native"
: >"$test_tmp/events"
run_installer && fail "a newer runtime cannot receive an older native app"
[[ ! -e $native ]] || fail "no mismatched native app was copied"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "missing newer native app has actionable guidance"
assert_stopped "a missing updated app prevents launch and theme setup"
pass "updated runtimes are preserved and never seeded with the old packaged app"
new_home dirty-desktop
HOME="$test_home" HERMES_HOME="$hermes_home" bash "$test_tmp/share/install.sh" --dir "$runtime" --hermes-home "$hermes_home"
printf 'local desktop edit\n' >"$runtime/apps/desktop/src/main.js"
: >"$test_tmp/events"
run_installer && fail "modified desktop sources cannot be certified as the packaged build"
[[ ! -e $native && ! -e $hermes_home/desktop-build-stamp.json ]] || fail "modified desktop sources receive neither packaged app nor build stamp"
[[ $(cat "$runtime/apps/desktop/src/main.js") == 'local desktop edit' ]] || fail "desktop source edits are preserved"
grep -q 'hermes desktop --build-only' "$test_tmp/output" || fail "modified desktop sources have build guidance"
assert_stopped "modified desktop sources prevent stamping, launch and theme setup"
pass "a matching commit with modified desktop sources is preserved without seeding or stamping"
for failure in package install marker; do
new_home "$failure-failure"
case "$failure" in
package) OMARCHY_TEST_PACKAGE_FAIL=1 run_installer && fail "package failure stops setup" ;;
install) OMARCHY_TEST_INSTALL_FAIL=1 run_installer && fail "installer failure stops setup" ;;
marker) OMARCHY_TEST_NO_MARKER=1 run_installer && fail "missing marker stops setup" ;;
esac
[[ ! -e $native ]] || fail "failed setup does not seed the app"
assert_stopped "failed setup prevents launch and theme setup"
done
pass "package, upstream installer and readiness failures stop before launch"
for failure in copy race; do
new_home "$failure-failure"
if [[ $failure == "copy" ]]; then
OMARCHY_TEST_COPY_FAIL=1 run_installer && fail "copy failure stops setup"
[[ ! -e $native ]] || fail "partial copy is never published"
else
OMARCHY_TEST_COPY_RACE=1 run_installer && fail "concurrent native app stops publication"
[[ -d $native && -z $(ls -A "$native") ]] || fail "concurrent empty app directory is preserved"
fi
[[ -z $(find "${native%/*}" -maxdepth 1 -name '.linux-unpacked.*' -print) ]] || fail "owned staging directory is cleaned up"
assert_stopped "publication failure prevents launch"
done
pass "failed copies and concurrent app creation preserve existing work and clean only staging"
new_home incomplete-native
run_installer || fail "incomplete native fixture sets up"
rm "$native/resources/app.asar"
: >"$test_tmp/events"
run_installer && fail "incomplete existing app requires repair"
[[ ! -e $native/resources/app.asar ]] || fail "incomplete existing app is not overwritten"
assert_stopped "incomplete native app prevents launch"
pass "an incomplete existing native app is preserved"
new_home patch-conflict
run_installer || fail "patch conflict fixture sets up"
printf 'local edit\n' >"$runtime/runtime.txt"
: >"$test_tmp/events"
run_installer && fail "unexpected patch conflict stops setup"
[[ $(cat "$runtime/runtime.txt") == 'local edit' ]] || fail "conflicting runtime changes are preserved"
assert_stopped "patch conflict prevents launch"
rm "$runtime/.hermes-bootstrap-complete"
: >"$test_tmp/events"
run_installer && fail "incomplete modified runtime cannot be reset by upstream installer"
! grep -qx bootstrap "$test_tmp/events" || fail "modified runtime never reaches upstream installer"
pass "patch conflicts and incomplete modified runtimes retain local changes and stop safely"
new_home full-history-retry
git clone -q "$test_tmp/seed" "$runtime"
git -C "$runtime" checkout -q --detach "$release_commit"
run_installer || fail "clean incomplete full-history release checkout is repaired" "$(cat "$test_tmp/output")"
[[ $(cat "$runtime/venv/dependency-commit") == "$release_commit" ]] || fail "full-history retry pins before installing dependencies"
[[ $(git -C "$runtime" rev-parse HEAD) == "$release_commit" && -f $native/resources/app.asar ]] || fail "full-history retry seeds the matching release"
pass "full-history retries force the guarded release pin before dependency setup"
new_home local-main
git clone -q "$test_tmp/seed" "$runtime"
printf 'local branch work\n' >"$runtime/keep"
git -C "$runtime" add keep
git -C "$runtime" -c user.name=Test -c user.email=test@example.invalid commit -qm local-work
local_main=$(git -C "$runtime" rev-parse main)
git -C "$runtime" checkout -q --detach "$release_commit"
run_installer && fail "local main commits cannot be reset by upstream installation"
! grep -qx bootstrap "$test_tmp/events" || fail "local main is checked before upstream installer"
[[ $(git -C "$runtime" rev-parse main) == "$local_main" ]] || fail "local main commit stays referenced"
pass "detached release checkouts do not hide local main work from the installer guard"
new_home deepen-retry
OMARCHY_TEST_FETCH_FAIL=1 run_installer && fail "history fetch failure stops setup"
[[ ! -e $native ]] || fail "failed history fetch does not seed the app"
assert_stopped "failed history fetch prevents launch"
: >"$test_tmp/events"
run_installer || fail "history fetch can be retried after runtime setup" "$(cat "$test_tmp/output")"
! grep -qx bootstrap "$test_tmp/events" || fail "history retry does not repeat upstream installation"
pass "a history fetch failure can be retried without reinstalling the ready runtime"
new_home existing-commands
mkdir -p "$test_home/.local/bin"
printf 'foreign wrapper\n' >"$test_home/.local/bin/hermes"
printf 'symlink target\n' >"$test_home/target"
ln -s "$test_home/target" "$test_home/.local/bin/hermes-agent"
ln -s "$test_home/missing" "$test_home/.local/bin/hermes-acp"
run_installer || fail "existing commands are preserved before upstream replaces them" "$(cat "$test_tmp/output")"
backups=("$test_home/.local/bin/".hermes-before-desktop.*)
[[ ${#backups[@]} == 1 && -d ${backups[0]} ]] || fail "one backup directory preserves existing command names"
[[ $(cat "${backups[0]}/hermes") == 'foreign wrapper' ]] || fail "foreign wrapper bytes are saved"
[[ $(readlink "${backups[0]}/hermes-agent") == "$test_home/target" && $(readlink "${backups[0]}/hermes-acp") == "$test_home/missing" ]] || fail "working and broken symlinks are saved as links"
[[ $(cat "$test_home/target") == 'symlink target' ]] || fail "upstream does not overwrite the original symlink target"
grep -qF "${backups[0]}" "$test_tmp/output" || fail "backup location is reported"
pass "pre-existing command files and symlinks are backed up before replacement"
new_home old-package
mv "$test_tmp/package/resources/install-stamp.json" "$test_tmp/saved-install-stamp.json"
run_installer && fail "an old installed package cannot bootstrap"
grep -q 'omarchy update' "$test_tmp/output" || fail "old package has actionable upgrade guidance"
! grep -qx handoff "$test_tmp/events" || fail "old package is rejected before CLI handoff"
! grep -qx bootstrap "$test_tmp/events" || fail "old package never reaches upstream installer"
mv "$test_tmp/saved-install-stamp.json" "$test_tmp/package/resources/install-stamp.json"
pass "old package fails with upgrade guidance before changing the runtime or CLI"
new_home custom-profile
hermes_home="$test_home/custom home"
runtime="$hermes_home/hermes-agent"
OMARCHY_TEST_HOME="$hermes_home/PrOfIlEs/coder/../coder/" run_installer || fail "profile setup succeeds"
[[ -x $runtime/apps/desktop/release/linux-unpacked/Hermes ]] || fail "profile uses the canonical root runtime"
grep -qxF "$hermes_home" "$test_tmp/install-args" || fail "canonical custom home reaches upstream installer"
pass "custom profile paths normalize to the shared Hermes home"
+155 -2
View File
@@ -11,6 +11,14 @@ mock_bin="$test_tmp/bin"
test_home="$test_tmp/home"
mkdir -p "$mock_bin"
# Keep package-path checks scoped to the fixture, even with a live app open.
python3 - "$ROOT/bin/omarchy-remove-ai-hermes" "$test_tmp" <<'PY'
from pathlib import Path
import sys
source, scratch = map(Path, sys.argv[1:])
(scratch / 'remover').write_text(source.read_text().replace('/opt/hermes-desktop', str(scratch / 'package')))
PY
cat >"$mock_bin/omarchy-pkg-drop" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_DROP_LOG"
@@ -32,6 +40,14 @@ SH
cat >"$mock_bin/gum" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >>"$OMARCHY_TEST_GUM_LOG"
if [[ -n ${OMARCHY_TEST_PROMPT_GATE:-} ]]; then
touch "$OMARCHY_TEST_PROMPT_GATE.started"
for (( attempt=0; attempt<500; attempt++ )); do
[[ ! -e $OMARCHY_TEST_PROMPT_GATE.continue ]] || exit 0
sleep 0.01
done
exit 1
fi
exit "${OMARCHY_TEST_GUM_STATUS:-1}"
SH
cat >"$mock_bin/systemctl" <<'SH'
@@ -70,7 +86,7 @@ remove() {
OMARCHY_TEST_SYSTEMCTL_LOG="$test_tmp/systemctl-log" \
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
bash "$ROOT/bin/omarchy-remove-ai-hermes" </dev/null >/dev/null 2>&1
bash "$test_tmp/remover" </dev/null >"$test_tmp/output" 2>&1
}
# script(1) puts the remover on a pty, which is the only way -t 0 answers true
@@ -85,7 +101,7 @@ remove_tty() {
OMARCHY_TEST_GUM_LOG="$test_tmp/gum-log" \
OMARCHY_TEST_GUM_STATUS="${OMARCHY_TEST_GUM_STATUS:-1}" \
HOME="$test_home" PATH="$mock_bin:$PATH" \
script -qec "bash '$ROOT/bin/omarchy-remove-ai-hermes'" /dev/null >/dev/null 2>&1
script -qec "bash '$test_tmp/remover'" /dev/null >"$test_tmp/output" 2>&1
}
# The app brings its own uv and its own node; both are runtime, not data.
@@ -229,3 +245,140 @@ OMARCHY_TEST_INSTALLER_STATUS=1 remove && fail "a failed CLI teardown surfaces i
[[ ! -d $test_home/.hermes/hermes-agent ]] ||
fail "a failed CLI teardown does not stop the runtime removal"
pass "a failed CLI teardown is reported after the runtime is handled"
# Real SQLite writers exercise the kernel's live/deleted file descriptors.
# Package, service and confirmation commands remain confined to the mocks.
python3 - "$test_tmp" <<'PY'
import os
from pathlib import Path
import pty
import subprocess
import sys
import time
scratch = Path(sys.argv[1])
writer_code = '''import os, sqlite3, sys
c = sqlite3.connect(os.environ['TEST_DB'])
c.execute('pragma journal_mode=wal')
c.execute('create table fixture(value)')
c.execute("insert into fixture values ('keep')")
c.commit()
print('ready', flush=True)
sys.stdin.readline()
c.close()
'''
def setup(name):
home = scratch / name
runtime = home / '.hermes/hermes-agent'
runtime.mkdir(parents=True)
(runtime / '.hermes-bootstrap-complete').touch()
(home / '.config/Hermes').mkdir(parents=True)
env = {**os.environ, 'HOME': str(home), 'PATH': f"{scratch / 'bin'}:/usr/bin:/bin",
'OMARCHY_TEST_GUM_STATUS': '0'}
for key in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'):
log = home / (key + '.log')
log.touch()
env['OMARCHY_TEST_' + key + '_LOG'] = str(log)
return home, runtime, env
def writer(db):
child = subprocess.Popen([sys.executable, '-u', '-c', writer_code],
env={**os.environ, 'TEST_DB': str(db)},
stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True)
assert child.stdout.readline().strip() == 'ready'
return child
def stop(child):
if child.poll() is None:
child.stdin.write('\n')
child.stdin.flush()
child.wait(timeout=5)
def remove(env):
master, slave = pty.openpty()
try:
return subprocess.run(['bash', str(scratch / 'remover')], env=env,
stdin=slave, capture_output=True, text=True, timeout=10)
finally:
os.close(master)
os.close(slave)
def blocked(result, home, runtime, child):
assert result.returncode != 0 and str(child.pid) in result.stderr, result
assert 'Close Hermes' in result.stderr, result.stderr
assert (runtime / '.hermes-bootstrap-complete').exists()
assert all((home / (name + '.log')).stat().st_size == 0
for name in ('DROP', 'INSTALLER', 'SYSTEMCTL', 'GUM'))
assert child.poll() is None, 'remover must not kill sessions'
for deleted in (False, True):
home, runtime, env = setup('deleted-writer' if deleted else 'live-writer')
db = home / '.hermes/state.db'
child = writer(db)
try:
if deleted:
for suffix in ('', '-wal', '-shm'):
Path(str(db) + suffix).unlink()
db.write_bytes(b'new database generation')
blocked(remove(env), home, runtime, child)
if deleted:
assert db.read_bytes() == b'new database generation'
finally:
stop(child)
assert remove(env).returncode == 0, 'removal succeeds once the writer closes'
assert not (home / '.hermes').exists()
print('ok - live and deleted SQLite holders block removal before any side effects; closing them allows retry')
for kind in ('terminal', 'desktop', 'working-directory'):
home, runtime, env = setup(kind)
executable_name = str(scratch / 'package/Hermes') if kind == 'desktop' else str(runtime / 'hermes')
args = ['sleep', '30'] if kind == 'working-directory' else [executable_name, '30']
child = subprocess.Popen(args, executable='/usr/bin/sleep',
cwd=runtime if kind == 'working-directory' else scratch)
try:
blocked(remove(env), home, runtime, child)
finally:
child.terminate()
child.wait(timeout=5)
print('ok - terminal, packaged desktop and runtime working-directory processes are detected without a database')
home, runtime, env = setup('unrelated-writer')
sibling = home / '.hermes-other'
sibling.mkdir()
child = writer(sibling / 'state.db')
try:
assert remove(env).returncode == 0, 'a sibling database does not block Hermes removal'
assert child.poll() is None
finally:
stop(child)
print('ok - unrelated database holders are left alone')
home, runtime, env = setup('prompt-race')
gate = home / 'prompt'
env['OMARCHY_TEST_PROMPT_GATE'] = str(gate)
master, slave = pty.openpty()
remover = subprocess.Popen(['bash', str(scratch / 'remover')], env=env, stdin=slave,
stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
os.close(slave)
child = None
try:
deadline = time.monotonic() + 5
while not Path(str(gate) + '.started').exists():
assert remover.poll() is None and time.monotonic() < deadline, 'prompt was not reached'
time.sleep(0.01)
child = writer(home / '.hermes/state.db')
Path(str(gate) + '.continue').touch()
stdout, stderr = remover.communicate(timeout=10)
assert remover.returncode != 0 and str(child.pid) in stderr, (stdout, stderr)
assert (home / '.hermes/state.db-wal').exists()
assert (home / '.config/Hermes').exists()
finally:
if child is not None:
stop(child)
if remover.poll() is None:
remover.terminate()
remover.wait(timeout=5)
os.close(master)
print('ok - a writer started during confirmation blocks data deletion')
PY
+154
View File
@@ -0,0 +1,154 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
test_home="$test_dir/home"
kitty_config="$test_home/.config/kitty/kitty.conf"
legacy="$ROOT/test/shell.d/fixtures/kitty/legacy.conf"
migration="$ROOT/migrations/1788745941.sh"
mkdir -p "$(dirname "$kitty_config")" "$test_dir/bin"
run_migration() {
env HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$ROOT/bin:$PATH" bash -euo pipefail "$migration"
}
cp "$legacy" "$kitty_config"
output=$(run_migration)
cmp -s "$ROOT/config/kitty/kitty.conf" "$kitty_config" || fail "stock config becomes the user template"
backups=("$kitty_config".bak.*)
cmp -s "$legacy" "${backups[0]}" || fail "refresh backs up the original config"
[[ $output == *"Close and reopen all Kitty windows"* ]] || fail "migration requires a full restart"
pass "stock config is refreshed with a backup and restart guidance"
output=$(run_migration)
cmp -s "$ROOT/config/kitty/kitty.conf" "$kitty_config" || fail "stock migration is idempotent"
[[ $output != *"Close and reopen"* ]] || fail "rerun does not repeat restart guidance"
pass "stock migration is idempotent"
cat >"$kitty_config" <<'CONF'
# Keep this comment and my theme choice
include my-theme.conf
font_family My Font
font_size 13
map ctrl+insert
include shortcuts.conf
map shift+insert paste_from_clipboard
allow_remote_control yes
allow_remote_control y
allow_remote_control true
# allow_remote_control yes
listen_on unix:/tmp/my-kitty
CONF
printf 'allow_remote_control yes \n' >>"$kitty_config"
cp "$kitty_config" "$test_dir/custom-original"
cat >"$test_dir/expected" <<'CONF'
# Keep this comment and my theme choice
include my-theme.conf
font_family My Font
font_size 13
map ctrl+insert
include shortcuts.conf
map shift+insert paste_from_clipboard
# allow_remote_control yes
# allow_remote_control y
# allow_remote_control true
# allow_remote_control yes
listen_on unix:/tmp/my-kitty
CONF
printf '# allow_remote_control yes \n' >>"$test_dir/expected"
chmod 600 "$kitty_config"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "customizations survive the security repair"
[[ $(stat -c %a "$kitty_config") == "600" ]] || fail "migration preserves config permissions"
backup=$(rg -l 'allow_remote_control true' "$kitty_config".bak.* | tail -1)
cmp -s "$test_dir/custom-original" "$backup" || fail "custom config is backed up"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "custom migration is idempotent"
pass "custom config repair preserves ordering, mappings, theme, permissions, and original backup"
for mode in no n false socket-only socket password; do
printf 'allow_remote_control %s\nfont_size 13\n' "$mode" >"$kitty_config"
cp "$kitty_config" "$test_dir/expected"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "migration preserves $mode"
done
pass "explicit restricted remote-control modes are preserved"
printf 'font_size 13\n' >"$kitty_config"
cp "$kitty_config" "$test_dir/expected"
run_migration >/dev/null
cmp -s "$test_dir/expected" "$kitty_config" || fail "omitted setting stays omitted"
rm "$kitty_config"
run_migration >/dev/null
[[ ! -e $kitty_config ]] || fail "absent config stays absent"
pass "migration leaves omitted settings and absent user configs alone"
printf 'allow_remote_control yes\nfont_size 13\n' >"$test_dir/dotfiles.conf"
ln -s "$test_dir/dotfiles.conf" "$kitty_config"
run_migration >/dev/null
[[ -L $kitty_config ]] || fail "migration preserves a dotfile symlink"
grep -qx '# allow_remote_control yes' "$test_dir/dotfiles.conf" || fail "symlink target is repaired"
pass "custom dotfile symlinks survive the repair"
rm "$kitty_config"
# Exercise the real font commands without changing the running desktop.
for command in pkill omarchy-restart-shell omarchy-hook omarchy-notification-send; do
printf '#!/bin/bash\nexit 0\n' >"$test_dir/bin/$command"
done
printf '#!/bin/bash\nexit 1\n' >"$test_dir/bin/pgrep"
printf '#!/bin/bash\nprintf "Test Font\\n"\n' >"$test_dir/bin/fc-list"
printf '#!/bin/bash\nexit 0\n' >"$test_dir/bin/kitty"
cat >"$test_dir/bin/gsettings" <<'SH'
#!/bin/bash
if [[ $1 == "get" ]]; then
if [[ $3 == "font-name" ]]; then
echo "'Sans 11'"
else
echo 1.0
fi
fi
SH
chmod +x "$test_dir/bin/"*
run_command() {
env HOME="$test_home" OMARCHY_PATH="$ROOT" PATH="$test_dir/bin:$ROOT/bin:$PATH" "$ROOT/bin/$@"
}
cp "$ROOT/config/kitty/kitty.conf" "$kitty_config"
output=$(run_command omarchy-display-text-size)
[[ $output == *"terminal font: 9 pt"* ]] || fail "size report accounts for inherited Kitty default"
run_command omarchy-font-set 'Test Font'
run_command omarchy-display-text-size 16
grep -qx 'font_family Test Font' "$kitty_config" || fail "font command creates family override"
run_command omarchy-font-set Font
grep -qx 'font_family Font' "$kitty_config" || fail "font command updates family override"
[[ $(grep -c '^font_family ' "$kitty_config") == "1" ]] || fail "font update avoids duplicate overrides"
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command creates size override"
grep -qx '# font_size 12' "$kitty_config" || fail "font commands keep commented instructions"
run_command omarchy-display-text-size 18
[[ $(grep -c '^font_size ' "$kitty_config") == "1" ]] || fail "size update avoids duplicate overrides"
run_command omarchy-display-text-size reset
grep -qx 'font_size 9.0' "$kitty_config" || fail "size reset restores default"
pass "font controls add and update overrides in the minimal template"
rm "$kitty_config"
output=$(run_command omarchy-display-text-size)
[[ $output == *"terminal font: 9 pt"* ]] || fail "size report handles absent Kitty config"
run_command omarchy-font-set 'Test Font'
run_command omarchy-display-text-size 16
grep -qx 'font_family Test Font' "$kitty_config" || fail "font command handles absent config"
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command handles absent setting"
! grep -q '^include ' "$kitty_config" || fail "font controls must not opt users back into theming"
rm "$kitty_config"
run_command omarchy-display-text-size 16
grep -qx 'font_size 12.0' "$kitty_config" || fail "size command handles absent config"
pass "font controls create missing Kitty overrides without restoring the theme include"
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"
fi
+86 -157
View File
@@ -4,166 +4,95 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
config_script="$ROOT/install/config/locate.sh"
require_command python3
require_command updatedb
require_command plocate
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
python3 - <<'PY'
import os
from pathlib import Path
import shlex
import subprocess
import tempfile
stock_conf() {
cat >"$1" <<'CONF'
PRUNE_BIND_MOUNTS = "yes"
PRUNEFS = "9p afs autofs cifs fuse nfs nfs4 proc sysfs tmpfs"
PRUNENAMES = ".git .hg .svn"
PRUNEPATHS = "/afs /media /mnt /net /sfs /tmp /udev /var/cache /var/lib/pacman/local /var/lock /var/run /var/spool /var/tmp"
CONF
}
root = Path(os.environ["ROOT"])
# updatedb dies on a config that defines a variable twice, so hand every
# rewritten file to the real parser rather than trusting the greps below.
empty_tree="$test_tmp/empty-tree"
mkdir -p "$empty_tree"
def check(condition, description):
if not condition:
raise SystemExit("not ok - " + description)
print("ok - " + description, flush=True)
assert_conf_parses() {
command -v updatedb >/dev/null || return 0
drop_in = root / "default/systemd/system/plocate-updatedb.service.d/10-omarchy.conf"
directives = [line.strip() for line in drop_in.read_text().splitlines() if line.strip() and not line.startswith("#")]
check(len(directives) == 3 and directives[:2] == ["[Service]", "ExecStart="] and directives[2].startswith("ExecStart="),
"locate drop-in replaces the command and preserves upstream service restrictions")
command = shlex.split(directives[2].removeprefix("ExecStart="))
options = ["--prune-bind-mounts=no", "--add-prunepaths=/.snapshots"]
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")
local errors
errors=$(updatedb --config-file "$1" -U "$empty_tree" -o "$test_tmp/plocate.db" 2>&1 >/dev/null | grep -F "$1:" || true)
[[ -z $errors ]] || fail "updatedb accepts the rewritten config" "$errors"
}
with tempfile.TemporaryDirectory(prefix="omarchy-locate-") as scratch:
scratch = Path(scratch)
fake_bin = scratch / "bin"
fake_bin.mkdir()
stubs = {
"updatedb": 'printf "%s\\n" "$@" >"$TEST_CALLS"',
"sudo": 'exec "$@"',
"fzf": 'cat >/dev/null\nprintf "%s\\n" test-package',
"yay": 'if [[ ${1:-} == "-Slqa" ]]; then printf "%s\\n" test-package; fi',
"omarchy-sudo-keepalive": ':',
"omarchy-show-done": ':',
}
for name, body in stubs.items():
path = fake_bin / name
path.write_text("#!/bin/bash\n" + body + "\n")
path.chmod(0o755)
calls = scratch / "updatedb-arguments"
env = dict(os.environ, PATH=str(fake_bin) + ":" + os.environ["PATH"], TEST_CALLS=str(calls))
for relative in ("install/post-install/localdb.sh", "bin/omarchy-pkg-aur-install"):
subprocess.run(["bash", "-euo", "pipefail", str(root / relative)], env=env, check=True)
check(calls.read_text().splitlines() == options,
relative + " passes the scheduled service options directly")
calls.unlink()
conf="$test_tmp/updatedb.conf"
stock_conf "$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config indexes Btrfs subvolume mounts like /home"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate config prunes /.snapshots"
assert_conf_parses "$conf"
pass "locate config skips Btrfs snapshots and indexes Btrfs subvolumes"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
[[ $(grep -o '/\.snapshots' "$conf" | wc -l) -eq 1 ]] || fail "locate config is idempotent"
assert_conf_parses "$conf"
pass "locate config leaves an already-configured file alone"
OMARCHY_UPDATEDB_CONF_PATH="$test_tmp/missing.conf" bash -euo pipefail "$config_script" >/dev/null
pass "locate config tolerates a missing updatedb.conf"
# A hand-edited updatedb.conf may drop the settings entirely, or write them
# without the spaces around the "=" or the quotes that the stock Arch file uses.
conf="$test_tmp/sparse-updatedb.conf"
printf '%s\n' 'PRUNENAMES = ".git .hg .svn"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config adds a missing PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config adds a missing PRUNEPATHS"
assert_conf_parses "$conf"
pass "locate config adds settings a hand-edited updatedb.conf is missing"
conf="$test_tmp/unspaced-updatedb.conf"
printf '%s\n' 'PRUNE_BIND_MOUNTS="yes"' 'PRUNEPATHS="/tmp /var/tmp"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an unspaced PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp /var/tmp"' "$conf" || fail "locate config prunes /.snapshots in an unspaced PRUNEPATHS"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config keeps a single PRUNEPATHS setting"
assert_conf_parses "$conf"
pass "locate config handles updatedb.conf written without spaces around ="
# updatedb allows a comment after a value and indented settings, and defining
# either setting twice makes it refuse to run at all.
conf="$test_tmp/commented-updatedb.conf"
printf '%s\n' ' PRUNE_BIND_MOUNTS = "yes" # subvolumes look like bind mounts' \
'PRUNEPATHS = "/tmp" # scratch' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate config rewrites an indented PRUNE_BIND_MOUNTS"
grep -qFx 'PRUNEPATHS = "/.snapshots /tmp"' "$conf" || fail "locate config keeps the paths a commented PRUNEPATHS already prunes"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces a commented PRUNEPATHS instead of adding a second one"
assert_conf_parses "$conf"
pass "locate config handles indented settings and trailing comments"
# A hand-edited file may have dropped the quotes updatedb requires, which
# leaves it unparseable until something writes the setting out properly.
conf="$test_tmp/unquoted-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = /tmp' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNEPATHS = "/.snapshots"' "$conf" || fail "locate config repairs an unquoted PRUNEPATHS"
[[ $(grep -c 'PRUNEPATHS' "$conf") -eq 1 ]] || fail "locate config replaces an unquoted PRUNEPATHS instead of adding a second one"
assert_conf_parses "$conf"
pass "locate config handles updatedb.conf written without quotes"
# A path that merely ends in /.snapshots is not the root snapshot directory.
conf="$test_tmp/nested-snapshots-updatedb.conf"
printf '%s\n' 'PRUNEPATHS = "/var/lib/machines/.snapshots"' >"$conf"
OMARCHY_UPDATEDB_CONF_PATH="$conf" bash -euo pipefail "$config_script" >/dev/null
grep -qFx 'PRUNEPATHS = "/.snapshots /var/lib/machines/.snapshots"' "$conf" || fail "locate config prunes /.snapshots alongside a path that ends in it"
assert_conf_parses "$conf"
pass "locate config tells /.snapshots apart from a path that ends in it"
locate_migration=$(grep -rl 'Configure locate to skip Btrfs snapshots' "$ROOT/migrations" | head -n 1 || true)
[[ -n $locate_migration ]] || fail "locate migration exists"
fake_bin="$test_tmp/bin"
mkdir -p "$fake_bin"
cat >"$fake_bin/sudo" <<'STUB'
#!/bin/bash
exec "$@"
STUB
chmod +x "$fake_bin/sudo"
cat >"$fake_bin/systemctl" <<'STUB'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$TEST_LOG"
STUB
chmod +x "$fake_bin/systemctl"
conf="$test_tmp/migration-updatedb.conf"
stock_conf "$conf"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null
grep -qFx 'PRUNE_BIND_MOUNTS = "no"' "$conf" || fail "locate migration rewrites updatedb.conf"
grep -qF 'PRUNEPATHS = "/.snapshots /afs' "$conf" || fail "locate migration prunes /.snapshots"
grep -qFx 'systemctl restart --no-block plocate-updatedb.service' "$test_tmp/calls.log" || fail "locate migration replaces an in-flight run and rebuilds the index without blocking"
pass "locate migration fixes existing installs and rebuilds the index"
: >"$test_tmp/calls.log"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$ROOT" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration skips already-configured installs"
pass "locate migration is a no-op once updatedb.conf is configured"
# A dev checkout carries migrations from a release whose install scripts the
# checked-out tree may not have yet, and omarchy-migrate runs under set -e.
: >"$test_tmp/calls.log"
conf="$test_tmp/no-config-script-updatedb.conf"
stock_conf "$conf"
TEST_LOG="$test_tmp/calls.log" \
PATH="$fake_bin:$PATH" \
OMARCHY_PATH="$test_tmp/empty" \
OMARCHY_UPDATEDB_CONF_PATH="$conf" \
bash -euo pipefail "$locate_migration" >/dev/null ||
fail "locate migration survives a tree without the locate config script"
[[ ! -s $test_tmp/calls.log ]] || fail "locate migration touches nothing without the locate config script"
pass "locate migration is a no-op when the locate config script is missing"
tree = scratch / "tree"
visible = tree / "home/current-file"
excluded = tree / "private&pipe|directory"
hidden = excluded / "private-file"
visible.parent.mkdir(parents=True)
excluded.mkdir()
visible.touch()
hidden.touch()
conf = scratch / "updatedb.conf"
conf.write_text('PRUNE_BIND_MOUNTS = "yes"\nPRUNEPATHS = "' + str(excluded) + '"\n')
conf.chmod(0o640)
original = conf.read_bytes()
metadata = conf.stat()
database = scratch / "plocate.db"
run = [*command, "--config-file", str(conf), "--database-root", str(tree),
"--output", str(database), "--require-visibility", "no", "--debug-pruning"]
result = subprocess.run(run, capture_output=True, text=True, check=True)
debug = result.stdout + result.stderr
check("prune_bind_mounts\\000\n0\\000" in debug and "/.snapshots\\000" in debug,
"real updatedb overrides bind-mount pruning and adds root snapshots to exclusions")
entries = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines()
check(str(visible) in entries and str(hidden) not in entries,
"real locate indexes current files and preserves literal administrator exclusions")
check(conf.read_bytes() == original and (conf.stat().st_mode, conf.stat().st_uid, conf.stat().st_gid, conf.stat().st_mtime_ns)
== (metadata.st_mode, metadata.st_uid, metadata.st_gid, metadata.st_mtime_ns),
"indexing preserves configuration bytes, permissions, ownership, and modification time")
subprocess.run(run, capture_output=True, check=True)
repeated = subprocess.check_output(["plocate", "--database", str(database), ""], text=True).splitlines()
check(repeated == entries, "repeated indexing retains the same results and exclusions")
PY
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT
home="$tmpdir/home"
stub_bin="$tmpdir/bin"
mkdir -p "$home" "$stub_bin"
# Stands in for the real mise so a generated wrapper can be run and asked what
# arguments it passed on.
cat >"$stub_bin/mise" <<'SH'
#!/bin/bash
printf 'mise' >>"$OMARCHY_MISE_TEST_LOG"
for arg in "$@"; do
printf '\t%s' "$arg" >>"$OMARCHY_MISE_TEST_LOG"
done
printf '\n' >>"$OMARCHY_MISE_TEST_LOG"
SH
chmod +x "$stub_bin/mise"
install_wrapper() {
HOME="$home" "$ROOT/bin/omarchy-mise-install" "$@"
}
# The ordinary case still works, and every call site in install/user/mise.sh
# passes names of this shape.
install_wrapper npm:playwright playwright >/dev/null
[[ -x $home/.local/bin/playwright ]] ||
fail "a normal install writes an executable wrapper"
log="$tmpdir/normal.log"
: >"$log"
OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/playwright" >/dev/null
grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:playwright' "$log" ||
fail "the wrapper asks mise for the package it was given" "$(cat "$log")"
pass "a normal install writes a wrapper that names its package"
# A package name is data. Quoted with %q it reaches mise as one argument
# instead of being read as shell source when the wrapper runs.
install_wrapper 'npm:pkg$(touch '"$tmpdir"'/PWNED)end' hostile >/dev/null
log="$tmpdir/hostile.log"
: >"$log"
OMARCHY_MISE_TEST_LOG="$log" PATH="$stub_bin:$PATH" "$home/.local/bin/hostile" >/dev/null
[[ -e $tmpdir/PWNED ]] &&
fail "a package name with shell characters does not run when the wrapper does" \
"wrapper: $(cat "$home/.local/bin/hostile")"
grep -Fqx $'mise\tuse\t-g\t--quiet\tnpm:pkg$(touch '"$tmpdir"'/PWNED)end' "$log" ||
fail "the package reaches mise whole" "$(cat "$log")"
pass "a package name with shell characters reaches mise as one argument"
# The command name is a file name under ~/.local/bin. These shapes escape it,
# hide it, make something that reads as an option, or carry characters that have
# no business in a file name. Labelled so a newline in the value does not end up
# inside the test output.
refused=(
"a slash" "../escaped"
"a leading dot" ".hidden"
"a leading dash" "-dash"
"a newline" $'with\nnewline'
"a tab" $'with\ttab'
)
for (( i = 0; i < ${#refused[@]}; i += 2 )); do
label=${refused[i]}
name=${refused[i + 1]}
if install_wrapper somepkg "$name" >/dev/null 2>"$tmpdir/err"; then
fail "a command name with $label is refused"
fi
grep -Fq 'is not usable as a command name' "$tmpdir/err" ||
fail "the refusal says why for a command name with $label" "$(cat "$tmpdir/err")"
done
pass "command names that are not plain file names are refused"
# The refusal has to land before the rm, which would otherwise delete the
# escaped path on its way to failing.
victim="$tmpdir/victim"
printf 'keep me\n' >"$victim"
if install_wrapper somepkg "../../../..$victim" >/dev/null 2>&1; then
fail "an escaping command name is refused"
fi
[[ -f $victim ]] ||
fail "an escaping command name removes nothing outside ~/.local/bin"
pass "an escaping command name removes nothing outside ~/.local/bin"
+123
View File
@@ -0,0 +1,123 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
script="$ROOT/bin/omarchy-sudo-passwordless"
tmpfiles_file="$ROOT/etc/tmpfiles.d/omarchy-nopasswd-sudo.conf"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mock_bin="$test_tmp/bin"
grant="$test_tmp/grant"
calls="$test_tmp/calls"
mkdir -p "$mock_bin"
cat >"$mock_bin/gum" <<'SH'
#!/bin/bash
exit 0
SH
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
printf 'systemctl %s\n' "$*" >>"$TEST_CALLS"
[[ ${1:-} == "is-active" && ${TEST_TIMER_ACTIVE:-false} == "true" ]]
SH
cat >"$mock_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$TEST_CALLS"
case ${1:-} in
test)
[[ ${2:-} == "-f" && -f $TEST_GRANT ]]
;;
tee)
/usr/bin/tee "$TEST_GRANT"
;;
chmod)
/usr/bin/chmod "$2" "$TEST_GRANT"
;;
systemd-run)
[[ ${TEST_FAIL_SYSTEMD_RUN:-false} != "true" ]]
;;
rm)
/usr/bin/rm -f -- "$TEST_GRANT"
;;
systemctl)
exit 0
;;
*)
echo "unexpected sudo command: $*" >&2
exit 90
;;
esac
SH
chmod +x "$mock_bin/gum" "$mock_bin/sudo" "$mock_bin/systemctl"
run_command() {
TEST_CALLS="$calls" TEST_GRANT="$grant" PATH="$mock_bin:$PATH" USER=alice \
"$script" "$@"
}
: >"$calls"
enable_output=$(run_command 15)
[[ -f $grant ]] || fail "successful timer setup leaves the passwordless sudo grant enabled"
[[ $(cat "$grant") == "alice ALL=(ALL) NOPASSWD: ALL" ]] ||
fail "the enabled grant belongs to the current user" "$(cat "$grant")"
grep -q '^sudo systemd-run --on-active=15m .* rm -f -- /etc/sudoers.d/99-omarchy-nopasswd-alice$' "$calls" ||
fail "enabling arms the expiry timer" "$(cat "$calls")"
[[ $enable_output == *"automatically disable in 15 minutes"* ]] ||
fail "success is reported after the timer is armed" "$enable_output"
pass "enabling arms expiry before reporting success"
: >"$calls"
rm -f "$grant"
if failure_output=$(TEST_FAIL_SYSTEMD_RUN=true run_command 15 2>&1); then
fail "enabling fails when the expiry timer cannot be armed"
fi
[[ ! -e $grant ]] || fail "timer setup failure revokes the new passwordless sudo grant"
[[ $failure_output == *"Revoking access now"* ]] ||
fail "timer setup failure explains the fail-closed revocation" "$failure_output"
[[ $failure_output != *"Passwordless sudo has been ENABLED"* ]] ||
fail "timer setup failure does not report that passwordless sudo was enabled" "$failure_output"
pass "timer setup failure revokes a new grant"
: >"$calls"
printf 'alice ALL=(ALL) NOPASSWD: ALL\n' >"$grant"
if update_output=$(TEST_TIMER_ACTIVE=true TEST_FAIL_SYSTEMD_RUN=true run_command 30 2>&1); then
fail "updating fails when the replacement expiry timer cannot be armed"
fi
[[ ! -e $grant ]] || fail "timer update failure revokes the existing passwordless sudo grant"
[[ $update_output != *"timer updated"* ]] ||
fail "timer update failure does not report success" "$update_output"
pass "timer update failure revokes the existing grant"
mapfile -t tmpfiles_rules < <(grep -vE '^[[:space:]]*(#|$)' "$tmpfiles_file")
(( ${#tmpfiles_rules[@]} == 1 )) ||
fail "passwordless sudo ships one tmpfiles rule" "${tmpfiles_rules[*]}"
fake_root="$test_tmp/root"
sudoers_dir="$fake_root/etc/sudoers.d"
mkdir -p "$sudoers_dir"
grant_names=(alice buildbot-2 user.123 'service$')
for grant_name in "${grant_names[@]}"; do
touch "$sudoers_dir/99-omarchy-nopasswd-$grant_name"
done
touch "$sudoers_dir/omarchy-dns"
systemd-tmpfiles --root="$fake_root" --remove --inline "${tmpfiles_rules[@]}"
[[ -f $sudoers_dir/99-omarchy-nopasswd-alice ]] ||
fail "boot-only cleanup leaves a live grant alone outside boot"
systemd-tmpfiles --root="$fake_root" --remove --boot --inline "${tmpfiles_rules[@]}"
for grant_name in "${grant_names[@]}"; do
stale_grant="$sudoers_dir/99-omarchy-nopasswd-$grant_name"
[[ ! -e $stale_grant ]] || fail "boot cleanup removes every generated grant" "$stale_grant"
done
[[ -f $sudoers_dir/omarchy-dns ]] || fail "boot cleanup preserves unrelated sudoers rules"
pass "systemd-tmpfiles removes generated grants only during boot"
+201
View File
@@ -0,0 +1,201 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
QS_PID=""
cleanup() {
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
kill "$QS_PID" 2>/dev/null || true
wait "$QS_PID" 2>/dev/null || true
fi
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
rm -rf "$TMPDIR"
fi
}
trap cleanup EXIT
shell_qml="$ROOT/shell/shell.qml"
bar_qml="$ROOT/shell/plugins/bar/Bar.qml"
plugin_shell_api="$ROOT/shell/services/PluginShellApi.qml"
idle_service="$ROOT/shell/plugins/services/idle/Service.qml"
# Normalize horizontal and vertical whitespace so the wiring assertions survive
# harmless QML reflow. The runtime fixture below behaviorally covers
# PluginShellApi and AuthServiceStore; these checks remain the guard for their
# integration through shell.qml and Bar.qml, including without a compositor.
qml_matches() {
local file=$1
local pattern=$2
tr '\n\r\t' ' ' < "$file" | grep -Eq "$pattern"
}
qml_matches "$shell_qml" 'comp\.createObject\( *manifest\.__isFirstParty *&& *!authenticationService *\? *serviceHost *: *null *\)' ||
fail "third-party and authentication services are detached from the host object tree"
qml_matches "$shell_qml" 'AuthServiceStore\.put\( *key, *inst *\)' ||
fail "authentication services are retained outside the host service map"
qml_matches "$shell_qml" 'AuthServiceStore\.isTrusted\( *key *\)' ||
fail "live authentication classification survives public manifest mutation"
qml_matches "$shell_qml" 'AuthServiceStore\.updateManifest\( *id, *shell\.publicPluginManifest\( *m *\) *\)' ||
fail "kept authentication services receive only a public manifest snapshot"
qml_matches "$shell_qml" 'if *\( *!serviceKeepLoaded\( *authenticationId *\) *\) *AuthServiceStore\.destroy\( *authenticationId *\)' ||
fail "keepLoaded authentication services survive plugin rescans"
pass "third-party and authentication services are detached from the host object tree"
run_node_test <<'JS'
const fs = require('fs')
const vm = require('vm')
const store = {}
vm.createContext(store)
vm.runInContext(
fs.readFileSync(path.join(root, 'shell/services/AuthServiceStore.js'), 'utf8'),
store
)
const service = { destroy() {} }
store.put('omarchy.lock', service)
store.destroy('omarchy.lock')
assert(
!store.has('omarchy.lock') && store.isTrusted('omarchy.lock'),
'authentication classification survives service teardown'
)
JS
qml_matches "$shell_qml" 'inst\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "service plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'item\.shell *= *shell\.pluginShellFor\( *panelEntry\.manifest *\)' ||
fail "panel plugins receive a scoped shell facade"
qml_matches "$shell_qml" 'target\.shell *= *shell\.pluginShellFor\( *manifest *\)' ||
fail "full-bar plugins receive a scoped shell facade"
pass "third-party entry points receive scoped shell facades"
if qml_matches "$plugin_shell_api" 'function +pluginShellForId\('; then
fail "replacement-bar facade exposes a generic plugin-shell factory"
fi
qml_matches "$bar_qml" 'else if *\( *root\.shell *&& *typeof root\.shell\.pluginShellForBarEntry *=== *"function" *\) *\{[^}]*pluginShell *= *root\.shell\.pluginShellForBarEntry\( *key, *moduleName *\)' ||
fail "replacement bars do not fall back to a service-less entry facade"
pass "replacement bars cannot manufacture another plugin's service facade"
qml_matches "$shell_qml" 'target\.barConfig *= *shell\.barConfigFor\( *manifest *\)' ||
fail "initial replacement-bar configuration is not detached"
qml_matches "$shell_qml" 'bar\.barConfig *= *shell\.barConfigFor\( *shell\.activeBarManifest *\)' ||
fail "replacement-bar configuration updates are not detached"
pass "replacement bars receive detached configuration snapshots"
qml_matches "$bar_qml" 'target\.bar *= *firstParty *\? *root *: *root\.pluginBarApiFor\( *pluginApiId, *moduleName, *registered *\)' ||
fail "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$bar_qml" 'api\.clickTargets *= *root\.pluginClickTargets\( *api\.pluginId *\)' ||
fail "third-party bar facades exclude other widgets from their object graph"
pass "third-party widgets receive a bar facade instead of the host bar"
qml_matches "$shell_qml" 'widgets: *shell\.publicBarWidgetSnapshot\( *\)' ||
fail "third-party widget registries receive detached snapshots"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *target, *"clickTarget" *\)' ||
fail "third-party bar-object ownership is stamped by the host callback"
qml_matches "$bar_qml" 'root\.markPluginObject\( *pluginId, *owner, *"popout" *\)' ||
fail "owner-less popouts receive trusted ownership before activation"
qml_matches "$shell_qml" 'manifest\.__hostCapabilities\.indexOf\( *"authentication" *\)' ||
fail "authentication isolation follows host-stamped capabilities"
pass "registry mutation and ownership boundaries are host-controlled"
qml_matches "$bar_qml" 'root\.moduleWidgets\( *moduleName *\)' ||
fail "custom bar module widget lookups use their real module name"
qml_matches "$shell_qml" 'shell\.pluginShellForBarEntry\( *cacheKey *\+ *":" *\+ *ownerId, *moduleName *\)' ||
fail "full-bar plugins receive a scoped settings facade for custom modules"
pass "custom bar modules retain settings and popout identity"
if qml_matches "$bar_qml" 'on(Foreground|BarForeground|Background|Urgent|FontFamily|Vertical|BarSize|Transparent)Changed: *sync'; then
fail "animated scalar properties still trigger full facade resyncs"
fi
qml_matches "$bar_qml" 'api\.foreground *= *Qt\.binding\( *function\( *\) *\{ *return root\.foreground *\} *\)' ||
fail "third-party bar scalar mirrors use bindings"
qml_matches "$shell_qml" 'shell\.prunePluginApis\( *\)' ||
fail "disabled plugin facade caches are pruned"
pass "plugin facade synchronization is bounded"
qml_matches "$shell_qml" 'descriptor\.profile *!== *expectedProfile[^}]*shell\.revokePluginShellApi\( *shellKey *\)' ||
fail "manifest capability changes do not revoke cached plugin facades"
qml_matches "$shell_qml" 'shell\.barPluginMayControl\( *currentManifest\( *\), *requestedId *\)' ||
fail "bar lifecycle callbacks do not validate the current manifest"
qml_matches "$shell_qml" 'return hasCurrentBarCapabilities\( *\) *\? *shell\.mutatePluginBarConfig\( *mutator *\) *: *false' ||
fail "bar configuration mutation does not validate the current manifest"
pass "manifest changes revoke cached facade capabilities"
qml_matches "$shell_qml" 'idleConfig: *shell\.publicIdleConfigFor\( *manifest *\)' ||
fail "cloned idle services do not receive their configured timeouts"
qml_matches "$shell_qml" 'shellApi\.idleConfig *= *shell\.publicIdleConfigFor\( *shellManifest *\)' ||
fail "cloned idle service configuration does not refresh"
qml_matches "$idle_service" 'shell *&& *shell\.idleConfig *\? *shell\.idleConfig *: *\(\{\}\)' ||
fail "the idle service does not consume its scoped configuration"
bar_entry_shell=$(sed -n '/^ function pluginShellForBarEntry(/,/^ function pluginShellFor(/p' "$shell_qml")
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'var id *= *shell\.pluginRegistry\.resolveEnabledId\( *target *\)[^}]*return shell\.pluginRegistry\.installedPlugins\[id\] *\|\| *null' ||
fail "replacement-bar clone authorization does not follow the enabled implementation"
tr '\n\r\t' ' ' <<<"$bar_entry_shell" |
grep -Eq 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones in replacement bars cannot summon their existing auxiliary UI"
qml_matches "$shell_qml" 'shell\.pluginCloneMaySummon\( *currentManifest\( *\), *requestedId *\)' ||
fail "built-in clones cannot summon their existing auxiliary UI"
qml_matches "$shell_qml" '"omarchy\.media": *\["omarchy\.osd"\]' ||
fail "media clones cannot summon their existing OSD target"
qml_matches "$shell_qml" '"omarchy\.network": *\["omarchy\.speedtest", *"omarchy\.wifiqr"\]' ||
fail "network clones cannot summon their existing auxiliary panels"
pass "built-in service and widget clones retain narrow configuration and UI integration"
qml_matches "$shell_qml" 'shell\.serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *id *\) *\)' ||
fail "narrow first-party service proxies do not resolve enabled clones"
qml_matches "$shell_qml" 'return serviceFor\( *shell\.pluginRegistry\.resolveEnabledId\( *pluginId *\) *\)' ||
fail "trusted first-party service lookups do not resolve enabled clones"
qml_matches "$shell_qml" 'allowOwnService *&& *shell\.pluginOwnsTarget\( *key, *requestedId *\)[^}]*return shell\.pluginServiceFor\( *key, *requestedId *\)' ||
fail "cloned widgets cannot use a source id to reach their own service"
pass "service facades resolve enabled clones without widening replacement-bar access"
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"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/plugin-auth-boundary"
mkdir -p "$config_dir" "$TMPDIR/home"
cp "$SHELL_TEST_DIR/fixtures/plugin-auth-boundary/"*.qml "$config_dir/"
ln -s "$ROOT/shell/services" "$config_dir/services"
OMARCHY_QML_TEST_RESULT="$result" \
HOME="$TMPDIR/home" \
XDG_CONFIG_HOME="$TMPDIR/home/.config" \
XDG_CACHE_HOME="$TMPDIR/home/.cache" \
XDG_STATE_HOME="$TMPDIR/home/.local/state" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..80}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary fixture exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
jq . "$result" >&2
sed -n '1,220p' "$log" >&2
fail "plugin authentication boundary runtime behavior"
fi
pass "plugin authentication boundary runtime behavior"
+313
View File
@@ -112,6 +112,200 @@ Item {
}
QML
# A replacement bar must not receive a generic factory for another plugin's
# live service, and its barConfig must be a detached snapshot on both initial
# injection and later host-config updates.
victim_service_id="acme.victim-service"
victim_service_dir="$test_home/.config/omarchy/plugins/$victim_service_id"
mkdir -p "$victim_service_dir"
cat >"$victim_service_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$victim_service_id",
"name": "Victim Service",
"version": "1.0.0",
"kinds": ["service"],
"entryPoints": {"service": "Service.qml"}
}
JSON
cat >"$victim_service_dir/Service.qml" <<'QML'
import QtQuick
Item {
property string privateValue: "victim-secret"
}
QML
# A clone of the built-in media service exercises both supported service paths:
# its own widget receives the raw companion service under the trusted bar, while
# a replacement bar receives only the narrow media proxy resolved to the clone.
media_clone_id="acme.media-clone"
media_clone_dir="$test_home/.config/omarchy/plugins/$media_clone_id"
mkdir -p "$media_clone_dir"
cat >"$media_clone_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$media_clone_id",
"name": "Media Clone",
"version": "1.0.0",
"kinds": ["service", "bar-widget"],
"entryPoints": {"service": "Service.qml", "barWidget": "BarWidget.qml"},
"barWidget": {"defaultSection": "center"},
"omarchy": {"clonedFrom": "omarchy.media"}
}
JSON
cat >"$media_clone_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property string marker: "clone-service"
property bool enabled: true
property var activePlayer: null
property var sourcePlayers: []
property var shell: null
function runAction(action, showFeedback, targetKey) {}
function playerKey(player) { return "" }
function selectPlayer(playerKey) {}
IpcHandler {
target: "acme-media-clone-service"
function ping(): string { return marker }
function summonOsd(): string {
return root.shell && root.shell.summon("omarchy.osd", "{}") ? "true" : "false"
}
}
}
QML
cat >"$media_clone_dir/BarWidget.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var bar: null
IpcHandler {
target: "acme-media-clone-widget"
function probeOwnService(): string {
var service = root.bar && root.bar.shell
? root.bar.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({
reachable: !!service,
marker: service ? String(service.marker || "") : ""
})
}
}
}
QML
review_bar_id="acme.review-bar"
review_bar_dir="$test_home/.config/omarchy/plugins/$review_bar_id"
mkdir -p "$review_bar_dir"
cat >"$review_bar_dir/manifest.json" <<JSON
{
"schemaVersion": 1,
"id": "$review_bar_id",
"name": "Review Bar",
"version": "1.0.0",
"kinds": ["bar", "service"],
"keepLoaded": true,
"entryPoints": {"bar": "Bar.qml", "service": "Service.qml"}
}
JSON
cat >"$review_bar_dir/Bar.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var barConfig: ({})
IpcHandler {
target: "acme-review-bar"
function probeVictim(): string {
var genericFactory = root.shell
&& typeof root.shell.pluginShellForId === "function"
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe", "acme.victim-service") : null
var victim = entryFacade && typeof entryFacade.serviceFor === "function"
? entryFacade.serviceFor("acme.victim-service") : null
return JSON.stringify({
genericFactory: !!genericFactory,
entryFacade: !!entryFacade,
victimServiceReachable: !!victim
})
}
function snapshot(): string {
return JSON.stringify(root.barConfig || {})
}
function probeMediaProxy(): string {
var service = root.shell
? root.shell.firstPartyServiceFor("omarchy.media") : null
return JSON.stringify({ reachable: !!service, enabled: service ? service.enabled === true : false })
}
function probeMediaWidgetSummon(): string {
var entryFacade = root.shell
&& typeof root.shell.pluginShellForBarEntry === "function"
? root.shell.pluginShellForBarEntry("probe-media", "acme.media-clone") : null
return JSON.stringify({
entryFacade: !!entryFacade,
osdSummoned: entryFacade ? entryFacade.summon("omarchy.osd", "{}") : false,
foreignSummoned: entryFacade ? entryFacade.summon("omarchy.lock", "{}") : false
})
}
function mutateSnapshot(): string {
if (root.barConfig && root.barConfig.layout
&& root.barConfig.layout.left && root.barConfig.layout.left.length > 0)
root.barConfig.layout.left[0].id = "tampered.by.review-bar"
return snapshot()
}
}
}
QML
cat >"$review_bar_dir/Service.qml" <<'QML'
import QtQuick
import Quickshell.Io
Item {
id: root
property var shell: null
property var retainedShell: null
onShellChanged: if (!retainedShell && shell) retainedShell = shell
function mutationAllowed(candidate) {
if (!candidate) return false
try {
return typeof candidate.mutateShellConfig === "function"
&& candidate.mutateShellConfig(function(config) {}) === true
} catch (e) {
return false
}
}
IpcHandler {
target: "acme-review-capability"
function probe(): string {
return JSON.stringify({
currentAllowed: root.mutationAllowed(root.shell),
retainedAllowed: root.mutationAllowed(root.retainedShell)
})
}
}
}
QML
cat >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
@@ -434,3 +628,122 @@ jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.keyboard-layout")' \
<<<"$(shell_ipc shell listShellConfig)" >/dev/null ||
fail_with_log "bar put added a second copy of a widget already on the bar"
pass "bar put leaves a widget already on the bar alone"
# Run the replacement-bar probes last: switching bar loaders can transiently
# leave bar-aware panels without a visual host, which should not add noise to
# the default-bar assertions above.
[[ $(shell_ipc shell setPluginEnabled "$media_clone_id" true) == "ok" ]] ||
fail_with_log "media clone fixture could not be enabled"
clone_widget_probe=""
for _ in {1..80}; do
clone_widget_probe=$(shell_ipc acme-media-clone-widget probeOwnService 2>/dev/null || true)
if jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e '.reachable == true and .marker == "clone-service"' \
<<<"$clone_widget_probe" >/dev/null || {
printf 'Clone own-service probe: %s\n' "$clone_widget_probe" >&2
fail_with_log "a cloned widget resolves its source id to its own companion service"
}
pass "trusted bar gives a cloned widget its own companion service"
[[ $(shell_ipc acme-media-clone-service summonOsd) == "true" ]] ||
fail_with_log "a cloned media service cannot summon its existing OSD target"
pass "a cloned built-in service retains its auxiliary UI integration"
[[ $(shell_ipc shell setPluginEnabled "$victim_service_id" true) == "ok" ]] ||
fail_with_log "victim service fixture could not be enabled"
[[ $(shell_ipc shell enablePlugin "$review_bar_id" '{}') == "ok" ]] ||
fail_with_log "replacement-bar fixture could not be enabled"
review_probe=""
for _ in {1..80}; do
review_probe=$(shell_ipc acme-review-bar probeVictim 2>/dev/null || true)
if jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while loading the replacement-bar fixture"
fi
sleep 0.1
done
jq -e '.genericFactory == false and .entryFacade == false and .victimServiceReachable == false' \
<<<"$review_probe" >/dev/null || {
printf 'Replacement-bar service probe: %s\n' "$review_probe" >&2
fail_with_log "replacement bar cannot recover another plugin's live service"
}
media_proxy_probe=$(shell_ipc acme-review-bar probeMediaProxy)
jq -e '.reachable == true and .enabled == true' <<<"$media_proxy_probe" >/dev/null || {
printf 'Replacement-bar media proxy probe: %s\n' "$media_proxy_probe" >&2
fail_with_log "replacement-bar service proxies resolve enabled clones"
}
media_summon_probe=$(shell_ipc acme-review-bar probeMediaWidgetSummon)
jq -e '.entryFacade == true and .osdSummoned == true and .foreignSummoned == false' \
<<<"$media_summon_probe" >/dev/null || {
printf 'Replacement-bar media summon probe: %s\n' "$media_summon_probe" >&2
fail_with_log "replacement-bar clone facades retain only their auxiliary UI integration"
}
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated the initially injected host configuration"
[[ $(shell_ipc shell setBarWidget omarchy.clock format '"HH:mm:ss"' '{}') == "ok" ]] ||
fail_with_log "host bar configuration could not be updated for snapshot testing"
updated_snapshot=""
for _ in {1..80}; do
updated_snapshot=$(shell_ipc acme-review-bar snapshot 2>/dev/null || true)
if jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null 2>&1; then
break
fi
sleep 0.1
done
jq -e 'any(.layout.center[]; (.id // .) == "omarchy.clock" and .format == "HH:mm:ss")' \
<<<"$updated_snapshot" >/dev/null ||
fail_with_log "replacement bar did not receive the refreshed configuration snapshot"
bar_config_before=$(shell_ipc shell listShellConfig | jq -c '.bar')
shell_ipc acme-review-bar mutateSnapshot >/dev/null
bar_config_after=$(shell_ipc shell listShellConfig | jq -c '.bar')
[[ $bar_config_after == "$bar_config_before" ]] ||
fail_with_log "replacement bar mutated a refreshed host configuration"
pass "replacement-bar service and configuration boundaries hold at runtime"
capability_before=$(shell_ipc acme-review-capability probe)
jq -e '.currentAllowed == true and .retainedAllowed == true' \
<<<"$capability_before" >/dev/null ||
fail_with_log "bar service fixture did not initially receive bar capabilities"
# Keep the same enabled plugin ID and service instance while dropping the bar
# kind. Both the currently injected facade and a reference retained by the
# plugin must lose the old configuration capability after the manifest rescan.
jq '.kinds = ["service"] | .entryPoints = {"service": "Service.qml"}' \
"$review_bar_dir/manifest.json" >"$review_bar_dir/manifest.json.tmp"
mv "$review_bar_dir/manifest.json.tmp" "$review_bar_dir/manifest.json"
capability_after=""
for _ in {1..80}; do
capability_after=$(shell_ipc acme-review-capability probe 2>/dev/null || true)
if jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited while revoking changed manifest capabilities"
fi
sleep 0.1
done
jq -e '.currentAllowed == false and .retainedAllowed == false' \
<<<"$capability_after" >/dev/null || {
printf 'Capability revocation probe: %s\n' "$capability_after" >&2
fail_with_log "cached plugin facades revoke capabilities removed from the manifest"
}
pass "manifest reload revokes cached facade capabilities"
@@ -0,0 +1,705 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
migration="$ROOT/migrations/1788662350.sh"
test_tmp=$(mktemp -d -p /tmp)
trap 'rm -rf "$test_tmp"' EXIT
mock_omarchy="$test_tmp/omarchy"
sleep_dir="$test_tmp/system-sleep"
systemd_dir="$test_tmp/systemd"
drop_in="$systemd_dir/supergfxd.service.d/delay-start.conf"
quarantine="$test_tmp/quarantine"
reload_needed_marker="$test_tmp/reload-needed"
migration_copy="$test_tmp/migration.sh"
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$mock_omarchy/default/systemd/system-sleep" \
"$mock_omarchy/default/systemd/system/supergfxd.service.d" \
"$sleep_dir" "${drop_in%/*}" "$stub_bin"
cp "$ROOT/default/systemd/system-sleep/keyboard-backlight" \
"$mock_omarchy/default/systemd/system-sleep/keyboard-backlight"
cp "$ROOT/default/systemd/system-sleep/force-igpu" \
"$mock_omarchy/default/systemd/system-sleep/force-igpu"
cp "$ROOT/default/systemd/system/supergfxd.service.d/delay-start.conf" \
"$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf"
[[ $(grep -Fxc 'system_sleep_dir=/usr/lib/systemd/system-sleep' "$migration") == 1 ]] ||
fail "migration fixes one literal system-sleep directory"
[[ $(grep -Fxc 'supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf' "$migration") == 1 ]] ||
fail "migration fixes one literal supergfxd drop-in"
sed \
-e "s|system_sleep_dir=/usr/lib/systemd/system-sleep|system_sleep_dir=$sleep_dir|" \
-e "s|supergfxd_drop_in=/etc/systemd/system/supergfxd.service.d/delay-start.conf|supergfxd_drop_in=$drop_in|" \
-e "s|quarantine_root=/var/lib/omarchy/migrations/1788662350-system-sleep|quarantine_root=$quarantine|" \
-e "s|/var/lib/omarchy/migrations/1788662350-systemd-reload-needed|$reload_needed_marker|" \
-e "s|/usr/bin/stat|$stub_bin/stat|g" \
-e "s|/usr/bin/readlink|$stub_bin/readlink|g" \
"$migration" >"$migration_copy"
cat >"$stub_bin/stat" <<'SH'
#!/bin/bash
path=${!#}
if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then
exit 13
fi
actual_file_mode=$(/usr/bin/stat -c '%f' -- "$path") || exit 1
actual_mode=$(/usr/bin/stat -c '%a' -- "$path") || exit 1
if [[ :${FAKE_ROOT_DIRS:-}: == *":$path:"* ]]; then
uid=0
gid=0
mode=$(printf '%o' "$((8#$actual_mode & ~8#022))")
elif [[ :${FAKE_ROOT_FILES:-}: == *":$path:"* ]]; then
uid=0
gid=${FAKE_ROOT_GID:-0}
mode=${FAKE_ROOT_MODE:-$actual_mode}
else
exec /usr/bin/stat "$@"
fi
file_type=$((16#$actual_file_mode & 16#f000))
file_mode=$(printf '%x' "$((file_type | 8#$mode))")
case "$*" in
*"%f %u %g %a"*) printf '%s %s %s %s\n' "$file_mode" "$uid" "$gid" "$mode" ;;
*"%u %g %a"*) printf '%s %s %s\n' "$uid" "$gid" "$mode" ;;
*"%a"*) printf '%s\n' "$mode" ;;
*) exec /usr/bin/stat "$@" ;;
esac
SH
cat >"$stub_bin/readlink" <<'SH'
#!/bin/bash
path=${!#}
if [[ :${INACCESSIBLE_AS_USER:-}: == *":$path:"* && ${FAKE_SUDO:-0} == 0 ]]; then
exit 13
fi
exec /usr/bin/readlink "$@"
SH
chmod +x "$stub_bin/stat" "$stub_bin/readlink"
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
set -euo pipefail
printf 'sudo' >>"$CALLS"
printf '\t%s' "$@" >>"$CALLS"
printf '\n' >>"$CALLS"
case "$1" in
*/stat | */readlink)
FAKE_SUDO=1 exec "$@"
;;
/usr/bin/test)
shift
if [[ $1 == "-x" && :${FAKE_ROOT_DIRS:-}: == *":$2:"* ]]; then
exit 0
else
exec /usr/bin/test "$@"
fi
;;
/usr/bin/mktemp | /usr/bin/mv | /usr/bin/chmod | /usr/bin/cp | /usr/bin/rm)
exec "$@"
;;
/usr/bin/systemctl)
if [[ -n ${SYSTEMCTL_FAIL_ONCE_FILE:-} && -e $SYSTEMCTL_FAIL_ONCE_FILE ]]; then
/usr/bin/rm -f -- "$SYSTEMCTL_FAIL_ONCE_FILE"
exit 1
fi
exit 0
;;
/usr/bin/install)
shift
args=()
while (($#)); do
case "$1" in
-o | -g)
shift 2
;;
*)
args+=("$1")
shift
;;
esac
done
exec /usr/bin/install "${args[@]}"
;;
*)
printf 'unexpected sudo command: %s\n' "$*" >&2
exit 97
;;
esac
SH
chmod +x "$stub_bin/sudo"
run_migration() {
local fake_root_dirs
: >"$calls"
fake_root_dirs="/:/tmp:$test_tmp:$sleep_dir:$systemd_dir:${drop_in%/*}"
[[ -z ${EXTRA_FAKE_ROOT_DIRS:-} ]] || fake_root_dirs+=":$EXTRA_FAKE_ROOT_DIRS"
CALLS="$calls" \
FAKE_ROOT_DIRS="$fake_root_dirs" \
FAKE_ROOT_FILES="${FAKE_ROOT_FILES:-${2:-}}" \
FAKE_ROOT_MODE="${FAKE_ROOT_MODE:-${3:-}}" \
FAKE_ROOT_GID="${FAKE_ROOT_GID:-0}" \
INACCESSIBLE_AS_USER="${INACCESSIBLE_AS_USER:-}" \
SYSTEMCTL_FAIL_ONCE_FILE="${SYSTEMCTL_FAIL_ONCE_FILE:-}" \
OMARCHY_PATH="$mock_omarchy" \
PATH="$stub_bin:$PATH" bash -euo pipefail "$migration_copy" >/dev/null
}
printf 'attacker keyboard\n' >"$sleep_dir/keyboard-backlight"
printf 'attacker gpu\n' >"$sleep_dir/force-igpu"
printf 'attacker drop-in\n' >"$drop_in"
chmod 0777 "$sleep_dir/keyboard-backlight" "$sleep_dir/force-igpu"
chmod 0666 "$drop_in"
exec 9>>"$sleep_dir/keyboard-backlight"
run_migration Integrated
printf 'write through stale attacker descriptor\n' >&9
exec 9>&-
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration replaces the user-owned keyboard hook with trusted content"
cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" ||
fail "migration replaces the user-owned GPU hook with trusted content"
cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" ||
fail "migration replaces the user-owned root service drop-in with trusted content"
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration activates the repaired keyboard hook"
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration activates force-igpu only in Integrated mode"
[[ $(stat -c '%a' "$drop_in") == 644 ]] ||
fail "migration installs the service drop-in as configuration"
grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null ||
fail "migration reloads systemd after repairing its root service drop-in"
[[ ! -e $reload_needed_marker ]] ||
fail "migration leaves a reload marker after systemd accepted the repaired drop-in"
[[ $(stat -c '%a' "$quarantine") == 700 ]] ||
fail "migration keeps preserved unsafe custom content in a root-only directory"
keyboard_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type f -print -quit)
force_backup=$(find "$quarantine" -path '*/force-igpu.*/original' -type f -print -quit)
drop_in_backup=$(find "$quarantine" -path '*/delay-start.conf.*/original' -type f -print -quit)
grep -Fxq 'attacker keyboard' "$keyboard_backup" ||
fail "migration preserves unknown keyboard-hook content before replacing it"
grep -Fxq 'attacker gpu' "$force_backup" ||
fail "migration preserves unknown force-iGPU content before replacing it"
grep -Fxq 'attacker drop-in' "$drop_in_backup" ||
fail "migration preserves unknown service-drop-in content before replacing it"
pass "migration replaces writable privileged files with trusted root-owned copies"
backup_count=$(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l)
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -s $calls ]] ||
fail "migration changes already-repaired privileged files on a second run" "$(<"$calls")"
[[ $(find "$quarantine" -mindepth 2 -maxdepth 2 -name original | wc -l) == "$backup_count" ]] ||
fail "migration creates duplicate quarantines on a second run"
pass "migration is idempotent after repairing unsafe privileged files"
printf 'attacker drop-in\n' >"$drop_in"
chmod 0666 "$drop_in"
reload_failure="$test_tmp/fail-systemd-reload-once"
touch "$reload_failure"
set +e
SYSTEMCTL_FAIL_ONCE_FILE="$reload_failure" \
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu" \
run_migration Integrated
reload_status=$?
set -e
(( reload_status != 0 )) ||
fail "migration reports success after systemd rejects the repaired drop-in"
cmp -s "$mock_omarchy/default/systemd/system/supergfxd.service.d/delay-start.conf" "$drop_in" ||
fail "migration does not repair the drop-in before the simulated reload failure"
[[ -e $reload_needed_marker && $(stat -c '%a' "$reload_needed_marker") == 644 ]] ||
fail "migration does not persist the reload requirement before replacing the drop-in"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
grep -Fx $'sudo\t/usr/bin/systemctl\tdaemon-reload' "$calls" >/dev/null ||
fail "migration does not retry a failed reload after the drop-in is already safe"
[[ ! -e $reload_needed_marker ]] ||
fail "migration does not clear the reload requirement after a successful retry"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -s $calls ]] ||
fail "migration repeats a successfully completed reload repair" "$(<"$calls")"
pass "migration persists and retries systemd reload after failure or interruption"
keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l)
cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/force-igpu:$drop_in" run_migration Integrated
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration does not safely activate a user-owned canonical hook"
[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] ||
fail "migration quarantines an exact legacy artifact as administrator content"
pass "migration replaces exact vulnerable installer artifacts without inventing backups"
legacy_keyboard="$test_tmp/legacy-keyboard-backlight"
cat >"$legacy_keyboard" <<'SH'
#!/bin/bash
# Turn off keyboard backlight before hibernate to prevent hang on power-off.
# The ASUS keyboard controller can block S4 shutdown if LEDs are active.
if [[ $1 == "pre" && $2 == "hibernate" ]]; then
device=""
for candidate in /sys/class/leds/*kbd_backlight*; do
if [[ -e "$candidate" ]]; then
device="$(basename "$candidate")"
break
fi
done
if [[ -n "$device" ]]; then
brightnessctl -d "$device" set 0 >/dev/null 2>&1
fi
fi
SH
[[ $(sha256sum "$legacy_keyboard" | cut -d' ' -f1) == f313a81e47401f0d38b8602e5997f52c5286d5e97f74027564ddd515b3d16511 ]] ||
fail "keyboard-backlight legacy fixture no longer matches the migration fingerprint"
keyboard_backup_count=$(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l)
cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration does not upgrade the released keyboard-backlight hook"
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration leaves the released keyboard-backlight hook non-executable"
[[ $(find "$quarantine" -path '*/keyboard-backlight.*/original' | wc -l) == "$keyboard_backup_count" ]] ||
fail "migration quarantines the released keyboard hook as administrator content"
pass "migration activates the released root-owned keyboard-backlight hook"
cp "$legacy_keyboard" "$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight" ||
fail "migration mistakes executable released hook bytes for a current artifact"
pass "migration refreshes recognized legacy hook contents at the final mode"
printf 'attacker gpu\n' >"$sleep_dir/force-igpu"
chmod 0777 "$sleep_dir/force-igpu"
run_migration Hybrid
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration does not activate the trusted self-guarding force-igpu hook"
pass "migration repairs force-igpu without depending on a live GPU-mode query"
printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 755
grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" ||
fail "migration preserves a secure administrator-owned custom hook"
cp "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" "$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 644
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 755 ]] ||
fail "migration leaves an exact packaged keyboard hook non-executable"
printf 'administrator customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0644 "$sleep_dir/keyboard-backlight"
run_migration Integrated "$sleep_dir/keyboard-backlight" 644
[[ $(stat -c '%a' "$sleep_dir/keyboard-backlight") == 644 ]] ||
fail "migration changes the mode of a safe noncanonical administrator hook"
grep -Fxq 'administrator customization' "$sleep_dir/keyboard-backlight" ||
fail "migration replaces a safe noncanonical administrator hook"
pass "migration activates only exact packaged hooks while preserving safe custom files"
legacy_force_igpu="$test_tmp/legacy-force-igpu"
cat >"$legacy_force_igpu" <<'SH'
#!/bin/bash
# Use the Vfio to Integrated trick to turn off NVIDIA dgpu when in integrated mode
# without needing to restart the computer. This is needed because computers like the Asus G14
# will wake after suspend in Hybrid mode, even if the system was in Integrated mode before
# suspending.
case "$1" in
pre)
# Before hibernating, switch to Vfio so the nvidia driver is detached from the dGPU.
# Without this, hibernate resume fails because the nvidia driver can't freeze a
# powered-off dGPU (returns -EIO), which aborts the entire resume.
if [[ $2 == "hibernate" ]]; then
/usr/bin/supergfxctl -m Vfio
sleep 1
fi
;;
post)
# small delay so the device is fully re-enumerated
sleep 4
# force-bind dGPU to vfio (fully detached from nvidia)
/usr/bin/supergfxctl -m Vfio
sleep 1
# then go back to Integrated, which powers it off again
/usr/bin/supergfxctl -m Integrated
;;
esac
SH
[[ $(sha256sum "$legacy_force_igpu" | cut -d' ' -f1) == d604e7c4903829563e45fc52188fc5602c3f1bc66e247f0a2cc0a974ed6e57db ]] ||
fail "force-igpu legacy fixture no longer matches the migration fingerprint"
cp "$legacy_force_igpu" "$sleep_dir/force-igpu"
chmod 0644 "$sleep_dir/force-igpu"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
cmp -s "$mock_omarchy/default/systemd/system-sleep/force-igpu" "$sleep_dir/force-igpu" ||
fail "migration does not upgrade the exact legacy force-igpu hook"
[[ $(stat -c '%a' "$sleep_dir/force-igpu") == 755 ]] ||
fail "migration leaves the exact legacy force-igpu hook non-executable"
pass "migration activates the exact legacy force-igpu artifact with its new guard"
printf 'wheel-managed customization\n' >"$sleep_dir/keyboard-backlight"
chmod 0755 "$sleep_dir/keyboard-backlight"
FAKE_ROOT_FILES="$sleep_dir/keyboard-backlight:$sleep_dir/force-igpu:$drop_in" \
FAKE_ROOT_GID=10 run_migration Integrated
grep -Fxq 'wheel-managed customization' "$sleep_dir/keyboard-backlight" ||
fail "migration replaces a safe root:wheel administrator hook"
[[ ! -s $calls ]] ||
fail "migration escalates while preserving safe root:wheel entries"
pass "migration treats non-writable root-owned files as safe regardless of group"
admin_dir="$test_tmp/admin-hooks"
admin_keyboard="$admin_dir/keyboard"
admin_delay="$admin_dir/delay.conf"
mkdir -p "$admin_dir"
printf 'protected keyboard customization\n' >"$admin_keyboard"
printf 'protected delay customization\n' >"$admin_delay"
chmod 0755 "$admin_keyboard"
chmod 0644 "$admin_delay"
rm -f "$sleep_dir/keyboard-backlight" "$drop_in"
ln -s "$admin_keyboard" "$sleep_dir/keyboard-backlight"
ln -s "$admin_delay" "$drop_in"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \
FAKE_ROOT_GID=10 run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$admin_keyboard" ]] ||
fail "migration replaces a safe administrator-managed keyboard-hook symlink"
[[ -L $drop_in && $(readlink "$drop_in") == "$admin_delay" ]] ||
fail "migration replaces a safe administrator-managed service-drop-in symlink"
[[ ! -s $calls ]] ||
fail "migration escalates while preserving safe administrator symlinks"
pass "migration preserves symlinks whose full target paths are root-controlled"
dangling_target="$admin_dir/future-keyboard"
rm -f "$sleep_dir/keyboard-backlight" "$dangling_target"
ln -s "$dangling_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$dangling_target" ]] ||
fail "migration replaces a safe dangling administrator symlink"
[[ ! -s $calls ]] ||
fail "migration asks for sudo to verify an absent target below a searchable root-controlled directory"
pass "migration handles safe dangling administrator symlinks without sudo"
escaping_user_dir="$test_tmp/escaping-user-hooks"
escaping_user_hook="$escaping_user_dir/keyboard"
escaping_missing_dir="$admin_dir/future"
escaping_target="$escaping_missing_dir/../../escaping-user-hooks/keyboard"
mkdir -p "$escaping_user_dir"
printf 'future unsafe keyboard customization\n' >"$escaping_user_hook"
chmod 0755 "$escaping_user_hook"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$escaping_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration trusts a dangling symlink whose unresolved suffix escapes to a user-controlled path"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not replace a future user-controlled dangling symlink"
pass "migration resolves the full dangling-symlink suffix before trusting it"
protected_dir="$test_tmp/root-only-hooks"
protected_target="$protected_dir/target"
protected_bridge="$protected_dir/bridge"
mkdir -p "$protected_dir"
printf 'root-only administrator customization\n' >"$protected_target"
ln -s "$protected_target" "$protected_bridge"
chmod 0700 "$protected_dir"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$protected_bridge" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dir" \
FAKE_ROOT_FILES="$protected_target:$admin_delay:$sleep_dir/force-igpu:$drop_in" \
INACCESSIBLE_AS_USER="$protected_bridge:$protected_target" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_bridge" ]] ||
fail "migration replaces a safe symlink whose target is hidden by a root-only directory"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/bridge$' "$calls" ||
fail "migration does not inspect inaccessible symlink metadata with privilege"
grep -q $'^sudo\t.*/readlink\t--\t.*/root-only-hooks/bridge$' "$calls" ||
fail "migration does not resolve an inaccessible administrator symlink with privilege"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-hooks/target$' "$calls" ||
fail "migration does not inspect an inaccessible administrator target with privilege"
pass "migration preserves root-controlled symlink chains hidden from the invoking user"
protected_dangling_dir="$test_tmp/root-only-dangling"
protected_dangling_target="$protected_dangling_dir/future-keyboard"
mkdir -p "$protected_dangling_dir"
chmod 0000 "$protected_dangling_dir"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$protected_dangling_target" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir:$protected_dangling_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu:$drop_in" \
INACCESSIBLE_AS_USER="$protected_dangling_target" \
run_migration Integrated
[[ -L $sleep_dir/keyboard-backlight && $(readlink "$sleep_dir/keyboard-backlight") == "$protected_dangling_target" ]] ||
fail "migration replaces a safe dangling symlink below a root-only directory"
grep -q $'^sudo\t.*/stat\t-c\t%f %u %g %a\t--\t.*/root-only-dangling/future-keyboard$' "$calls" ||
fail "migration does not inspect a protected dangling target with privilege"
grep -q $'^sudo\t/usr/bin/test\t-x\t.*/root-only-dangling$' "$calls" ||
fail "migration does not distinguish a protected missing target from an inaccessible parent"
pass "migration preserves dangling administrator symlinks below root-only directories"
user_dir="$test_tmp/user-hooks"
user_keyboard="$user_dir/keyboard"
mkdir -p "$user_dir"
printf 'unsafe symlink customization\n' >"$user_keyboard"
chmod 0755 "$user_keyboard"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$user_keyboard" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_delay:$sleep_dir/force-igpu" run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration leaves a user-controlled keyboard-hook symlink active"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not replace an unsafe symlink with trusted hook content"
symlink_backup=$(find "$quarantine" -path '*/keyboard-backlight.*/original' -type l -print -quit)
[[ -n $symlink_backup && $(readlink "$symlink_backup") == "$user_keyboard" ]] ||
fail "migration discards an unsafe custom symlink instead of preserving it"
pass "migration quarantines unsafe symlinks outside the active systemd directory"
bridge="$user_dir/bridge"
ln -s "$admin_keyboard" "$bridge"
rm -f "$sleep_dir/keyboard-backlight"
ln -s "$bridge" "$sleep_dir/keyboard-backlight"
EXTRA_FAKE_ROOT_DIRS="$admin_dir" \
FAKE_ROOT_FILES="$admin_keyboard:$admin_delay:$sleep_dir/force-igpu" \
run_migration Integrated
[[ ! -L $sleep_dir/keyboard-backlight ]] ||
fail "migration trusts a symlink chain routed through a user-controlled directory"
cmp -s "$mock_omarchy/default/systemd/system-sleep/keyboard-backlight" \
"$sleep_dir/keyboard-backlight" ||
fail "migration does not repair an indirectly user-controlled symlink"
pass "migration checks every intermediate component in a symlink chain"
hook_copy="$test_tmp/force-igpu-hook"
hook_calls="$test_tmp/force-igpu-calls"
hook_queries="$test_tmp/force-igpu-queries"
hook_config="$test_tmp/supergfxd.conf"
hook_marker="$test_tmp/force-igpu-restore"
hook_pending="$test_tmp/force-igpu-pending"
sed \
-e "s|/usr/bin/supergfxctl|$stub_bin/hook-supergfxctl|g" \
-e "s|/usr/bin/install|$stub_bin/hook-install|g" \
-e "s|/etc/supergfxd.conf|$hook_config|g" \
-e "s|/run/omarchy-force-igpu-integrated|$hook_marker|g" \
"$ROOT/default/systemd/system-sleep/force-igpu" >"$hook_copy"
cat >"$stub_bin/hook-supergfxctl" <<'SH'
#!/bin/bash
case "$1" in
-m)
printf '%s\n' "$*" >>"$HOOK_CALLS"
if [[ ${HOOK_BLOCK_MODE:-} == "$2" ]]; then
trap '' TERM
/usr/bin/sleep 30
fi
current=$(sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG")
if [[ $current != "$2" ]]; then
printf '%s %s\n' "$2" "${HOOK_CONFIRM_AFTER:-1}" >"$HOOK_PENDING"
fi
;;
-g)
printf '%s\n' "$*" >>"$HOOK_QUERIES"
if [[ -f $HOOK_PENDING ]]; then
read -r pending remaining <"$HOOK_PENDING"
if [[ ${HOOK_FAIL_MODE:-} != "$pending" ]]; then
remaining=$((remaining - 1))
if (( remaining <= 0 )); then
sed -i "s/\"mode\"[[:space:]]*:[[:space:]]*\"[^\"]*\"/\"mode\": \"$pending\"/" "$HOOK_CONFIG"
rm -f -- "$HOOK_PENDING"
else
printf '%s %s\n' "$pending" "$remaining" >"$HOOK_PENDING"
fi
fi
fi
sed -n 's/.*"mode"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' "$HOOK_CONFIG"
;;
esac
SH
cat >"$stub_bin/hook-install" <<'SH'
#!/bin/bash
args=()
while (($#)); do
case "$1" in
-o | -g)
shift 2
;;
*)
args+=("$1")
shift
;;
esac
done
exec /usr/bin/install "${args[@]}"
SH
cat >"$stub_bin/sleep" <<'SH'
#!/bin/bash
:
SH
chmod +x "$stub_bin/hook-supergfxctl" "$stub_bin/hook-install" "$stub_bin/sleep"
hook_env=(
"HOOK_CALLS=$hook_calls"
"HOOK_QUERIES=$hook_queries"
"HOOK_CONFIG=$hook_config"
"HOOK_PENDING=$hook_pending"
"PATH=$stub_bin:$PATH"
)
printf '{ "mode": "Hybrid" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_calls ]] || fail "force-igpu runs while the root-owned config says Hybrid"
[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent while configured for Hybrid mode"
rm -f "$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_calls ]] || fail "force-igpu runs when its mode config is unavailable"
[[ ! -e $hook_marker ]] || fail "force-igpu records restore intent without a mode config"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
[[ -f $hook_marker && $(stat -c '%a' "$hook_marker") == 600 ]] ||
fail "force-igpu does not securely record Integrated restore intent during pre-suspend"
HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ $(wc -l <"$hook_calls") == 2 ]] ||
fail "force-igpu does not run both GPU transitions in Integrated mode"
grep -Fqx -- '-m Integrated' "$hook_calls" ||
fail "force-igpu does not restore Integrated mode after suspend"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after suspend"
(( $(wc -l <"$hook_queries") >= 4 )) ||
fail "force-igpu does not wait for asynchronous GPU transitions"
pass "force-igpu confirms asynchronous transitions for Integrated sleep cycles"
: >"$hook_calls"
: >"$hook_queries"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
set +e
HOOK_CONFIRM_AFTER=2 HOOK_FAIL_MODE=Integrated env "${hook_env[@]}" \
bash "$hook_copy" post suspend >/dev/null 2>&1
restore_status=$?
set -e
(( restore_status != 0 )) || fail "force-igpu reports success without confirming Integrated mode"
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu failure test does not leave the transition in Vfio mode"
[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after an asynchronous transition failure"
HOOK_CONFIRM_AFTER=2 env "${hook_env[@]}" bash "$hook_copy" post suspend
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu does not recover the Integrated transition on the next sleep cycle"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves restore intent after a confirmed retry"
pass "force-igpu retains restore intent until Integrated mode is confirmed"
: >"$hook_calls"
: >"$hook_queries"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre suspend
set +e
HOOK_BLOCK_MODE=Vfio env "${hook_env[@]}" \
bash "$hook_copy" post suspend >/dev/null 2>&1
blocked_request_status=$?
set -e
(( blocked_request_status != 0 )) || fail "force-igpu waits forever for a blocked GPU transition request"
[[ -f $hook_marker ]] || fail "force-igpu discards restore intent after a blocked transition request"
[[ ! -s $hook_queries ]] || fail "force-igpu polls before a blocked transition request returns"
env "${hook_env[@]}" bash "$hook_copy" post suspend
[[ ! -e $hook_marker ]] || fail "force-igpu cannot retry after a blocked transition request"
pass "force-igpu bounds blocked transition requests and retains retry intent"
: >"$hook_calls"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
env "${hook_env[@]}" bash "$hook_copy" pre hibernate
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu test double does not model the pre-hibernate Vfio persistence"
[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during the Vfio transition"
env "${hook_env[@]}" bash "$hook_copy" post hibernate
[[ $(wc -l <"$hook_calls") == 3 ]] ||
fail "force-igpu skips the post-hibernate transitions after Vfio changes the config"
[[ $(tail -1 "$hook_calls") == "-m Integrated" ]] ||
fail "force-igpu does not finish post-hibernate restoration in Integrated mode"
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu leaves supergfxd configured for Vfio after hibernation"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after hibernation"
pass "force-igpu restores Integrated mode after pre-hibernate persists Vfio"
: >"$hook_calls"
printf '{ "mode": "Integrated" }\n' >"$hook_config"
SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate
SYSTEMD_SLEEP_ACTION=suspend env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 2 ]] ||
fail "force-igpu does not complete the initial suspend phase of suspend-then-hibernate"
SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" pre suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 3 && $(tail -1 "$hook_calls") == "-m Vfio" ]] ||
fail "force-igpu skips the Vfio transition before compound hibernation"
grep -Fq '"mode": "Vfio"' "$hook_config" ||
fail "force-igpu does not detach the dGPU during the hibernate phase"
[[ -f $hook_marker ]] || fail "force-igpu loses restore intent during compound hibernation"
SYSTEMD_SLEEP_ACTION=hibernate env "${hook_env[@]}" bash "$hook_copy" post suspend-then-hibernate
[[ $(wc -l <"$hook_calls") == 5 && $(tail -1 "$hook_calls") == "-m Integrated" ]] ||
fail "force-igpu does not restore Integrated mode after compound hibernation"
grep -Fq '"mode": "Integrated"' "$hook_config" ||
fail "force-igpu leaves supergfxd configured for Vfio after compound hibernation"
[[ ! -e $hook_marker ]] || fail "force-igpu leaves stale restore intent after compound hibernation"
pass "force-igpu handles both phases of suspend-then-hibernate"
keyboard_hook_copy="$test_tmp/keyboard-backlight-hook"
keyboard_calls="$test_tmp/keyboard-backlight-calls"
keyboard_led_dir="$test_tmp/leds"
mkdir -p "$keyboard_led_dir/asus::kbd_backlight"
sed "s|/sys/class/leds/\*kbd_backlight\*|$keyboard_led_dir/*kbd_backlight*|" \
"$ROOT/default/systemd/system-sleep/keyboard-backlight" >"$keyboard_hook_copy"
cat >"$stub_bin/brightnessctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$KEYBOARD_CALLS"
SH
chmod +x "$stub_bin/brightnessctl"
SYSTEMD_SLEEP_ACTION=suspend KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \
bash "$keyboard_hook_copy" pre suspend-then-hibernate
[[ ! -e $keyboard_calls ]] || fail "keyboard-backlight runs during the suspend phase of compound sleep"
SYSTEMD_SLEEP_ACTION=hibernate KEYBOARD_CALLS="$keyboard_calls" PATH="$stub_bin:$PATH" \
bash "$keyboard_hook_copy" pre suspend-then-hibernate
grep -Fqx -- '-d asus::kbd_backlight set 0' "$keyboard_calls" ||
fail "keyboard-backlight skips the hibernate phase of compound sleep"
pass "keyboard-backlight handles the hibernate phase of suspend-then-hibernate"
+44
View File
@@ -132,3 +132,47 @@ if problems:
PYTHON
pass "no Omarchy script writes a path under /usr that no package owns"
for script in bin/omarchy-hibernation-setup bin/omarchy-toggle-hybrid-gpu; do
grep -F '"${destination%/*}/.${destination##*/}.omarchy.XXXXXX"' "$ROOT/$script" >/dev/null ||
fail "$script reserves a hidden sibling for the privileged replacement"
grep -F 'sudo /usr/bin/install -m "$mode" -o root -g root -T "$source" "$stage"' "$ROOT/$script" >/dev/null ||
fail "$script prepares privileged files with final root ownership and mode"
grep -F 'sudo /usr/bin/mv -Tf -- "$stage" "$destination"' "$ROOT/$script" >/dev/null ||
fail "$script atomically replaces the privileged destination"
if grep -F 'sudo /usr/bin/chmod "$mode" "$destination"' "$ROOT/$script" >/dev/null; then
fail "$script changes mode after publishing the privileged destination"
fi
done
grep -F ' /usr/lib/systemd/system-sleep/keyboard-backlight 0755' "$ROOT/bin/omarchy-hibernation-setup" >/dev/null ||
fail "hibernation setup installs keyboard-backlight as a root-owned executable"
hook_install_line=$(rg -n '^if ! install_root_file .*keyboard-backlight' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1)
resume_marker_line=$(rg -n '^echo "HOOKS\+=\(resume\)"' "$ROOT/bin/omarchy-hibernation-setup" | cut -d: -f1)
[[ -n $hook_install_line && -n $resume_marker_line ]] ||
fail "hibernation setup keeps recognizable hook-install and resume-marker steps"
(( hook_install_line < resume_marker_line )) ||
fail "hibernation setup marks completion before a failed hook install can be retried"
grep -F ' /usr/lib/systemd/system-sleep/force-igpu 0755' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null ||
fail "hybrid GPU setup installs force-igpu as a root-owned executable"
grep -F ' /etc/systemd/system/supergfxd.service.d/delay-start.conf 0644' "$ROOT/bin/omarchy-toggle-hybrid-gpu" >/dev/null ||
fail "hybrid GPU setup installs its root service drop-in as root-owned configuration"
delay_install_line=$(rg -n '^ if ! install_root_file .*delay-start\.conf' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1)
force_install_line=$(rg -n '^ if ! install_root_file .*force-igpu' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | cut -d: -f1)
config_switch_line=$(rg -n '^ sudo sed -i \\' "$ROOT/bin/omarchy-toggle-hybrid-gpu" | tail -1 | cut -d: -f1)
[[ -n $delay_install_line && -n $force_install_line && -n $config_switch_line ]] ||
fail "hybrid GPU setup keeps recognizable support-file and config-switch steps"
(( delay_install_line < config_switch_line && force_install_line < config_switch_line )) ||
fail "hybrid GPU setup switches config before every required file is installed"
grep -Fq '/usr/bin/grep -Eq' "$ROOT/default/systemd/system-sleep/force-igpu" ||
fail "force-igpu does not guard execution with the configured GPU mode"
if rg -n 'cp -p.*(system-sleep|supergfxd\.service\.d)' "$ROOT/bin/omarchy-hibernation-setup" "$ROOT/bin/omarchy-toggle-hybrid-gpu"; then
fail "privileged sleep and hybrid GPU files are never copied with source ownership"
fi
pass "system-sleep hooks and the hybrid GPU drop-in enforce root ownership"